diff --git a/backend/common/database/model/models.py b/backend/common/database/model/models.py index 7b7aecb..be2a365 100644 --- a/backend/common/database/model/models.py +++ b/backend/common/database/model/models.py @@ -85,6 +85,9 @@ class items(MAIN_BASE): vat_yn = Column(Boolean, nullable=True) # 부가세 포함 여부 delivery_fee_yn = Column(Boolean, nullable=True) # 배송비 포함 여부 internet_lowest_price_yn = Column(Boolean, nullable=False, server_default=text("false")) # 최저가 솔루션 보조 컬럼 + internet_lowest_price = Column(BigInteger, nullable=True) # 인터넷 최저가 실값(원). 목표가 산정용(KTC: ×(1−수수료)) + purchase_price = Column(BigInteger, nullable=True) # 매입가(원). 목표가 후보(그대로). KTC items.purchase_price + selling_price = Column(BigInteger, nullable=True) # 판매가(원). 목표가 후보(×(1−마진율)), 유통형만. KTC items.selling_price category_type = Column(Integer, nullable=False, server_default=text("1")) # 카테고리 조회용 자동 증가 숫자 created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신) @@ -108,6 +111,7 @@ class sessions(MAIN_BASE): qt_round = Column(Integer, nullable=False) # 견적 라운드(스냅샷) qt_type = Column(SmallInteger, nullable=False) # 견적 유형: 1=재협상, 2=재견적 (QtType) target_price = Column(BigInteger, nullable=False) # 목표가(원) + target_anchoring_price = Column(BigInteger, nullable=True) # 앵커링가(원)=floor(목표가×(1−anchoring_value)). KTC sessions.target_anchoring_price status = Column(SmallInteger, nullable=False) # 진행 상태 (SessionStatus 코드) bid_price = Column(BigInteger, nullable=True) # 입찰가(원) bid_at = Column(DateTime(timezone=True), nullable=True) # 입찰 시각 @@ -144,6 +148,8 @@ class quotations(MAIN_BASE): manager_email = Column(String(255), nullable=True) # 담당자 이메일 manager_contact_number = Column(String(20), nullable=True) # 담당자 연락처 memo = Column(String(100), nullable=True) # 메모 + md_price = Column(BigInteger, nullable=True) # MD 제시가(원). 목표가 산정 최우선값 + supplier_type = Column(SmallInteger, nullable=True) # 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록 iteration = Column(Integer, nullable=False, server_default=text("0")) # 반복 횟수 preferred_sp_yn = Column(Boolean, nullable=True) # 선호 공급사 지정 여부 preferred_sp_id = Column(UUID(as_uuid=True), nullable=True) # 선호 공급사(partner.suppliers.supplier_id) @@ -168,6 +174,7 @@ class quotation_settings(MAIN_BASE): user_id = Column(UUID(as_uuid=True), nullable=False) # 생성 유저(company.users.user_id) target_margin_rate = Column(Numeric(8, 6), nullable=False) # 목표 마진율 anchoring_value = Column(Numeric(8, 6), nullable=False, server_default=text("0.01")) # 앵커링 값(비율) — anchor=round(target*(1-value)) + internet_average_fee = Column(Numeric(8, 6), nullable=False, server_default=text("0.078")) # 인터넷 평균 수수료율. 목표가=인터넷최저가×(1−값). KTC QuotationPrice.internet_average_fee(Float) card_count = Column(Integer, nullable=False, server_default=text("3")) # 협상 내 협상카드 사용 횟수 created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 생성 시각(UTC) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("(now() AT TIME ZONE 'utc')")) # 수정 시각(UTC, 앱에서 갱신) diff --git a/backend/common/enums.py b/backend/common/enums.py index 7811a10..314905c 100644 --- a/backend/common/enums.py +++ b/backend/common/enums.py @@ -107,10 +107,13 @@ class TokenType(Enum): class QtType(Enum): - """견적/세션 유형 코드. quotation.quotations.type / negotiation.sessions.qt_type.""" + """견적/세션 유형 코드. quotation.quotations.type / negotiation.sessions.qt_type. + 신규/재 × 협상(1:1)/견적(1:N). 1·2(재)는 기존 데이터 보존 위해 고정, 신규는 3·4.""" - RENEGO = 1 # 재협상(1:1) - REQUOTE = 2 # 재견적(1:N) + RENEGO = 1 # 재협상(1:1) + REQUOTE = 2 # 재견적(1:N) + NEW_NEGO = 3 # 신규협상(1:1) + NEW_QUOTE = 4 # 신규견적(1:N) class SessionStatus(Enum): diff --git a/negodata/backend/common/database/model/models.py b/negodata/backend/common/database/model/models.py index d0cf970..9367ee1 100644 --- a/negodata/backend/common/database/model/models.py +++ b/negodata/backend/common/database/model/models.py @@ -86,6 +86,9 @@ class items(MainTableMixin, MAIN_BASE): price = Column(BigInteger, nullable=True) # 금액(원), 스키마 BIGINT internet_lowest_price_yn = Column(Boolean, nullable=False, default=False) # 최저가 솔루션 원자성 보존용 + internet_lowest_price = Column(BigInteger, nullable=True) # 인터넷 최저가 실값(원). 목표가 산정용(KTC: ×(1−수수료)) + purchase_price = Column(BigInteger, nullable=True) # 매입가(원). 목표가 후보(그대로). KTC items.purchase_price(Integer) + selling_price = Column(BigInteger, nullable=True) # 판매가(원). 목표가 후보(×(1−마진율)), 유통형만. KTC items.selling_price(Integer) moq = Column(String(50), nullable=True) # 최소 주문 수량 lead_time = Column(SmallInteger, nullable=True) # 주문 후 배송 도착까지 시간 @@ -121,6 +124,7 @@ class nego_cards(MainTableMixin, MAIN_BASE): number = Column(String(10), nullable=True) # 식별번호(카드코드) script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기) edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON) + usage_type = Column(SmallInteger, nullable=False, default=1) # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 class wild_cards(MainTableMixin, MAIN_BASE): @@ -133,6 +137,7 @@ class wild_cards(MainTableMixin, MAIN_BASE): number = Column(String(10), nullable=True) # 식별번호(카드코드) script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기) edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON) + usage_type = Column(SmallInteger, nullable=False, default=1) # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 condition = Column(String(255), nullable=True) # 사용 조건(트리거) available = Column(Boolean, nullable=False, default=False) # 수동 협상 적용 여부(ACTIVE/INACTIVE 매핑) memo = Column(String(255), nullable=True) # 자유 메모 @@ -174,6 +179,7 @@ class quotation_settings(MainTableMixin, MAIN_BASE): 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) + internet_average_fee = Column(Numeric(8, 6), nullable=False, default=0.078) # 인터넷 평균 수수료율. 목표가=인터넷최저가×(1−값). KTC QuotationPrice.internet_average_fee(Float) card_count = Column(Integer, nullable=False, default=3) # 한 협상 내 협상카드 사용 횟수 @@ -198,6 +204,8 @@ class quotations(MainTableMixin, MAIN_BASE): manager_email = Column(String(255), nullable=True) manager_contact_number = Column(String(20), nullable=True) memo = Column(String(100), nullable=True) + md_price = Column(BigInteger, nullable=True) # MD 제시가(원). 목표가 산정 최우선값 — 입력은 견적생성 모달 + supplier_type = Column(SmallInteger, nullable=True) # 협력사 유형(SupplierType). 재견적은 1:1이라 견적에 박는다. 입력은 견적생성 모달 iteration = Column(Integer, nullable=False, default=0) preferred_sp_yn = Column(Boolean, nullable=True) @@ -220,6 +228,7 @@ class sessions(MainTableMixin, MAIN_BASE): qt_round = Column(Integer, nullable=False) # 견적 라운드 스냅샷 qt_type = Column(SmallInteger, nullable=False) # QuotationType 스냅샷 target_price = Column(BigInteger, nullable=False) # 목표가(원) + target_anchoring_price = Column(BigInteger, nullable=True) # 앵커링가(원)=floor(목표가×(1−anchoring_value)). KTC sessions.target_anchoring_price(BigInteger) status = Column(SmallInteger, nullable=False) # SessionStatus 코드 bid_price = Column(BigInteger, nullable=True) # 입찰가(원) bid_at = Column(DateTime(timezone=True), nullable=True) # 입찰 시각 diff --git a/negodata/backend/common/enums.py b/negodata/backend/common/enums.py index 3c17d82..4b9f862 100644 --- a/negodata/backend/common/enums.py +++ b/negodata/backend/common/enums.py @@ -58,6 +58,7 @@ class ErrorType(Enum): # 견적 관련 에러 QUOTATION_NOT_FOUND = 1500 QUOTATION_NOT_LATEST_ROUND = auto() # 마지막 차수가 아닌 견적을 재생성하려 함 + QUOTATION_TARGET_PRICE_UNAVAILABLE = auto() # md_price·인터넷최저가 둘 다 없어 목표가 산정 불가 # 견적 설정 관련 에러 QUOTATION_SETTING_NOT_FOUND = 1600 @@ -118,10 +119,14 @@ class CompanyStatus(CodeEnum): class QuotationType(CodeEnum): - """quotations.type 코드값. 1=renego(재협상 1:1), 2=requote(재견적 1:N).""" + """quotations.type 코드값. 신규/재 × 협상(1:1)/견적(1:N). + 1=재협상(1:1), 2=재견적(1:N), 3=신규협상(1:1), 4=신규견적(1:N). + 기존 데이터 보존 위해 재협상/재견적 코드(1·2)는 고정, 신규는 3·4로 추가.""" - RENEGO = 1 - REQUOTE = 2 + RENEGO = 1 # 재협상(1:1) + REQUOTE = 2 # 재견적(1:N) + NEW_NEGO = 3 # 신규협상(1:1) + NEW_QUOTE = 4 # 신규견적(1:N) class QuotationStatus(CodeEnum): @@ -179,3 +184,22 @@ class CardType(CodeEnum): NEGO = 1 WILD = 2 + + +class SupplierType(CodeEnum): + """quotations.supplier_type 코드값. 없음(0,미지정)/유통(1)/제조(2)/총판(3). + 없음은 프론트 폼에 '없음'으로 노출. KTC 앵커링 코드(기타=0)와 매핑 시 0↔없음 대응.""" + + NONE = 0 # 없음(미지정) + DISTRIBUTION = 1 # 유통 + MANUFACTURE = 2 # 제조 + SOLE_AGENCY = 3 # 총판 + + +class CardUsageType(CodeEnum): + """nego_cards/wild_cards.usage_type 코드값. 협상카드 사용 범위(신규/재 견적·협상 양쪽 적용). + 공통=모두 적용(기본), 신규견적전용, 재견적전용.""" + + COMMON = 1 # 공통(모두) — 기본 + NEW = 2 # 신규견적전용 + REUSE = 3 # 재견적전용 diff --git a/negodata/backend/crud/quotation_crud.py b/negodata/backend/crud/quotation_crud.py index dd9482a..9358bea 100644 --- a/negodata/backend/crud/quotation_crud.py +++ b/negodata/backend/crud/quotation_crud.py @@ -40,7 +40,11 @@ class IQuotationCRUD(ABC): pass @abstractmethod - async def get_target_margin(self, cdb: AsyncSession, qt_setting_id) -> Tuple[ErrorType, Optional[float]]: + async def get_last_supplier_type(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, Optional[tuple]]: + pass + + @abstractmethod + async def get_setting_rates(self, cdb: AsyncSession, qt_setting_id) -> Tuple[ErrorType, dict]: pass @abstractmethod @@ -313,35 +317,71 @@ class QuotationCRUD(IQuotationCRUD): return ErrorType.DB_RUN_FAILED, [] async def get_item_prices(self, cdb: AsyncSession, item_ids) -> Tuple[ErrorType, dict]: - """item_id -> price(원, NULL 가능) 매핑. 세션 목표가 계산 입력.""" + """item_id -> (internet_lowest_price, purchase_price, selling_price)(원, NULL 가능) 매핑. 세션 목표가 계산 입력.""" try: if not item_ids: return ErrorType.SUCCESS, {} - query = select(items.item_id, items.price).where( + query = select( + items.item_id, items.internet_lowest_price, items.purchase_price, items.selling_price + ).where( items.item_id.in_(item_ids), 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, {r[0]: r[1] for r in rows} + return ErrorType.SUCCESS, {r[0]: (r[1], r[2], r[3]) for r in rows} except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, {} - async def get_target_margin(self, cdb: AsyncSession, qt_setting_id) -> Tuple[ErrorType, Optional[float]]: - """견적 세팅의 목표 마진율. 세션 목표가 = price / (1 + margin).""" + async def get_last_supplier_type(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, Optional[tuple]]: + """협력사의 직전 견적 supplier_type. (supplier_type, qt_number) | None. + sessions(supplier_id) ⨝ quotations 에서 supplier_type 가 있는 최신 견적 1건.""" try: - query = select(quotation_settings.target_margin_rate).where( - quotation_settings.qt_setting_id == qt_setting_id - ).limit(1) + query = ( + select(quotations.supplier_type, quotations.number) + .join(sessions, sessions.quotation_id == quotations.qt_id) + .where( + sessions.supplier_id == supplier_id, + quotations.supplier_type.isnot(None), + quotations.deleted == False, # noqa: E712 + ) + .order_by(quotations.created_at.desc()) + .limit(1) + ) err_type, rows = await DB_SESSION_MNG.execute(cdb, query) if err_type != ErrorType.SUCCESS: return err_type, None - return ErrorType.SUCCESS, (float(rows[0]) if rows and rows[0] is not None else None) + if not rows: + return ErrorType.SUCCESS, None + return ErrorType.SUCCESS, (rows[0][0], rows[0][1]) except Exception as ex: LOG.e_no_callstack(ex) return ErrorType.DB_RUN_FAILED, None + async def get_setting_rates(self, cdb: AsyncSession, qt_setting_id) -> Tuple[ErrorType, dict]: + """견적 세팅의 율: {margin, fee, anchoring}. 목표가·앵커링가 산정 입력.""" + try: + query = select( + quotation_settings.target_margin_rate, + quotation_settings.internet_average_fee, + quotation_settings.anchoring_value, + ).where(quotation_settings.qt_setting_id == qt_setting_id).limit(1) + err_type, rows = await DB_SESSION_MNG.execute(cdb, query) + if err_type != ErrorType.SUCCESS: + return err_type, {} + if not rows: + return ErrorType.SUCCESS, {} + r = rows[0] + return ErrorType.SUCCESS, { + "margin": float(r[0]) if r[0] is not None else None, + "fee": float(r[1]) if r[1] is not None else None, + "anchoring": float(r[2]) if r[2] is not None else None, + } + 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: diff --git a/negodata/backend/router/v1/card/protocol.py b/negodata/backend/router/v1/card/protocol.py index 2bf3e19..bd54f39 100644 --- a/negodata/backend/router/v1/card/protocol.py +++ b/negodata/backend/router/v1/card/protocol.py @@ -4,7 +4,7 @@ from typing import Any, Optional from pydantic import ConfigDict -from common.enums import CardStatus +from common.enums import CardStatus, CardUsageType from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol @@ -18,6 +18,7 @@ class Req_CreateCard(CardProtocol): number: Optional[str] = None script: Optional[str] = None edit_script: Optional[Any] = None + usage_type: int = CardUsageType.COMMON.value # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 status: int = CardStatus.ACTIVE.value # 와일드카드 적용 여부(available 매핑). 일반카드는 무시. condition: Optional[str] = None # 와일드카드 전용 memo: Optional[str] = None # 와일드카드 전용 @@ -28,6 +29,7 @@ class Req_UpdateCard(CardProtocol): number: Optional[str] = None script: Optional[str] = None edit_script: Optional[Any] = None + usage_type: Optional[int] = None # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 status: Optional[int] = None condition: Optional[str] = None memo: Optional[str] = None @@ -44,6 +46,7 @@ class CardData(WebPacketProtocol): number: Optional[str] = None script: Optional[str] = None edit_script: Optional[Any] = None + usage_type: CardUsageType = CardUsageType.COMMON # 카드 적용 견적 구분: 1=공통 2=신규견적전용 3=재견적전용 status: CardStatus = CardStatus.ACTIVE condition: Optional[str] = None memo: Optional[str] = None diff --git a/negodata/backend/router/v1/item/protocol.py b/negodata/backend/router/v1/item/protocol.py index 1ef449c..fd8d7e6 100644 --- a/negodata/backend/router/v1/item/protocol.py +++ b/negodata/backend/router/v1/item/protocol.py @@ -24,6 +24,9 @@ class Req_CreateItem(ItemProtocol): made_in: Optional[str] = None price: Optional[int] = None internet_lowest_price_yn: bool = False + internet_lowest_price: Optional[int] = None # 인터넷 최저가 실값(원). 목표가 산정용 + purchase_price: Optional[int] = None # 매입가(원). 재견적 목표가 후보(그대로) + selling_price: Optional[int] = None # 판매가(원). 유통형 목표가 후보(×(1−마진율)) moq: Optional[str] = None lead_time: Optional[int] = None quantity_unit: Optional[str] = None @@ -44,6 +47,9 @@ class Req_UpdateItem(ItemProtocol): made_in: Optional[str] = None price: Optional[int] = None internet_lowest_price_yn: Optional[bool] = None + internet_lowest_price: Optional[int] = None # 인터넷 최저가 실값(원). 목표가 산정용 + purchase_price: Optional[int] = None # 매입가(원). 재견적 목표가 후보(그대로) + selling_price: Optional[int] = None # 판매가(원). 유통형 목표가 후보(×(1−마진율)) moq: Optional[str] = None lead_time: Optional[int] = None quantity_unit: Optional[str] = None @@ -69,6 +75,9 @@ class ItemData(WebPacketProtocol): made_in: Optional[str] = None price: Optional[int] = None internet_lowest_price_yn: bool = False + internet_lowest_price: Optional[int] = None # 인터넷 최저가 실값(원). 목표가 산정용 + purchase_price: Optional[int] = None # 매입가(원). 재견적 목표가 후보(그대로) + selling_price: Optional[int] = None # 판매가(원). 유통형 목표가 후보(×(1−마진율)) moq: Optional[str] = None lead_time: Optional[int] = None quantity_unit: Optional[str] = None diff --git a/negodata/backend/router/v1/quotation/protocol.py b/negodata/backend/router/v1/quotation/protocol.py index f302cf5..3c402a9 100644 --- a/negodata/backend/router/v1/quotation/protocol.py +++ b/negodata/backend/router/v1/quotation/protocol.py @@ -4,7 +4,7 @@ from typing import Any, Optional from pydantic import ConfigDict -from common.enums import CardType, ChatSender, DeliveryType, QuotationStatus, QuotationType, SessionStatus +from common.enums import CardType, ChatSender, DeliveryType, QuotationStatus, QuotationType, SessionStatus, SupplierType from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol @@ -25,6 +25,8 @@ class Req_CreateQuotation(QuotationProtocol): manager_email: Optional[str] = None manager_contact_number: Optional[str] = None memo: Optional[str] = None + md_price: Optional[int] = None # MD 제시가(원). 세션 목표가 산정 최우선값 + supplier_type: Optional[int] = None # 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록 item_ids: list[uuid.UUID] = [] # 협상 대상 상품. item×supplier 조합마다 세션 1개 생성 supplier_ids: list[uuid.UUID] = [] # 협상 초청 공급사 card_ids: list[uuid.UUID] = [] # 선택 협상카드. 버전을 만들어 묶고 quotation.version_id 로 연결 @@ -52,6 +54,8 @@ class QuotationData(WebPacketProtocol): manager_email: Optional[str] = None manager_contact_number: Optional[str] = None memo: Optional[str] = None + md_price: Optional[int] = None + supplier_type: Optional[SupplierType] = None iteration: int = 0 preferred_sp_yn: Optional[bool] = None preferred_sp_id: Optional[uuid.UUID] = None @@ -162,3 +166,8 @@ class QuotationCardData(WebPacketProtocol): class Res_QuotationCards(Res_WebPacketProtocol): qt_id: Optional[uuid.UUID] = None cards: list[QuotationCardData] = [] + + +class Res_LastSupplierType(Res_WebPacketProtocol): + supplier_type: Optional[SupplierType] = None # 협력사 직전 견적의 유형(없으면 None) + qt_number: Optional[str] = None # 그 견적의 번호(이전 견적 값임을 표시용) diff --git a/negodata/backend/router/v1/quotation/quotation.py b/negodata/backend/router/v1/quotation/quotation.py index 2c60663..488b840 100644 --- a/negodata/backend/router/v1/quotation/quotation.py +++ b/negodata/backend/router/v1/quotation/quotation.py @@ -11,6 +11,7 @@ from .protocol import ( Req_RegenerateQuotation, Res_CreateQuotation, Res_DeleteQuotation, + Res_LastSupplierType, Res_Quotation, Res_QuotationCards, Res_QuotationList, @@ -92,6 +93,11 @@ async def delete_quotation(qt_id: UUID, service: QuotationService = Depends(), u return RemoveNoneResponse(await service.delete_quotation(str(qt_id))) +@router.get(path="/supplier/{supplier_id}/last-type", response_model=Res_LastSupplierType, summary="협력사 직전 견적 유형") +async def get_supplier_last_type(supplier_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): + return RemoveNoneResponse(await service.get_last_supplier_type(str(supplier_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)): diff --git a/negodata/backend/router/v1/quotation_setting/protocol.py b/negodata/backend/router/v1/quotation_setting/protocol.py index 0518f40..2f90855 100644 --- a/negodata/backend/router/v1/quotation_setting/protocol.py +++ b/negodata/backend/router/v1/quotation_setting/protocol.py @@ -14,12 +14,14 @@ class QuotationSettingProtocol(WebPacketProtocol): class Req_CreateQuotationSetting(QuotationSettingProtocol): target_margin_rate: float anchoring_value: float = 0.01 + internet_average_fee: float = 0.078 # 인터넷 평균 수수료율(목표가 인터넷가 차감) card_count: int = 3 class Req_UpdateQuotationSetting(QuotationSettingProtocol): target_margin_rate: Optional[float] = None anchoring_value: Optional[float] = None + internet_average_fee: Optional[float] = None # 인터넷 평균 수수료율 card_count: Optional[int] = None @@ -30,6 +32,7 @@ class QuotationSettingData(WebPacketProtocol): user_id: Optional[uuid.UUID] = None target_margin_rate: float anchoring_value: float + internet_average_fee: float = 0.078 # 인터넷 평균 수수료율 card_count: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/negodata/backend/services/card_service.py b/negodata/backend/services/card_service.py index ff2f13c..513086d 100644 --- a/negodata/backend/services/card_service.py +++ b/negodata/backend/services/card_service.py @@ -29,6 +29,7 @@ class CardService: number=row.number, script=row.script, edit_script=row.edit_script, + usage_type=row.usage_type, status=CardStatus.ACTIVE.value, created_at=row.created_at, updated_at=row.updated_at, @@ -45,6 +46,7 @@ class CardService: number=row.number, script=row.script, edit_script=row.edit_script, + usage_type=row.usage_type, status=CardStatus.ACTIVE.value if row.available else CardStatus.INACTIVE.value, condition=row.condition, memo=row.memo, @@ -146,6 +148,7 @@ class CardService: number=req.number, script=req.script, edit_script=req.edit_script, + usage_type=req.usage_type, ) if is_wildcard: card = wild_cards( @@ -182,7 +185,7 @@ class CardService: return res # 해당 테이블에 있는 컬럼만 추린다(없는 필드는 무시). status → available(와일드 전용). - allowed = {"name", "number", "script", "edit_script"} + allowed = {"name", "number", "script", "edit_script", "usage_type"} if is_wild: allowed |= {"condition", "memo"} payload = {k: v for k, v in data.items() if k in allowed} diff --git a/negodata/backend/services/quotation_service.py b/negodata/backend/services/quotation_service.py index d5aa41d..8b35a29 100644 --- a/negodata/backend/services/quotation_service.py +++ b/negodata/backend/services/quotation_service.py @@ -21,6 +21,7 @@ from router.v1.quotation.protocol import ( Req_CreateQuotation, Res_CreateQuotation, Res_DeleteQuotation, + Res_LastSupplierType, Res_Quotation, Res_QuotationCards, Res_QuotationList, @@ -60,13 +61,29 @@ class QuotationService: return f"{base}/chat?session_id={session_id}" @staticmethod - def _calc_target_price(price, margin) -> int: - """세션 목표가(원). 단가 있으면 목표 마진율 적용가, 없으면 0.""" - if not price: - return 0 - if margin and margin > 0: - return int(int(price) / (1 + margin)) - return int(price) + def _calc_target_price(md_price=None, internet_lowest=None, purchase=None, selling=None, fee=0.0, margin=0.0, is_new=False) -> int: + """세션 목표가 (KTC 신규/재 분리 로직, 회사 데이터 풍부도에 graceful 적응) + ① md_price 있으면 → 그대로 + ② 없으면: + · 신규(NEW_NEGO/NEW_QUOTE) → 인터넷최저가 × (1 − fee) [인터넷최저가만] + · 재(RENEGO/REQUOTE) → 유효 후보 중 min: + - 인터넷최저가 × (1 − fee) ← fee=quotation_settings.internet_average_fee + - 매입가 (그대로) + - 판매가 × (1 − margin) ← margin=quotation_settings.target_margin_rate + ③ 후보 0개 → 견적 생성 불가(ValueError).""" + if md_price: + return int(md_price) + candidates = [] + if internet_lowest: + candidates.append(int(internet_lowest) * (1 - (fee or 0.0))) + if not is_new: # 재(협상·견적)만 매입가·판매가를 후보에 추가. 신규는 인터넷최저가만. + if purchase: + candidates.append(int(purchase)) + if selling: + candidates.append(int(selling) * (1 - (margin or 0.0))) + if not candidates: + raise ValueError("타겟 가격 계산 불가: md_price·인터넷최저가" + ("" if is_new else "·매입가·판매가") + " 모두 없음") + return int(min(candidates)) @staticmethod def _gen_number() -> str: @@ -136,6 +153,22 @@ class QuotationService: res.quotation = QuotationData.model_validate(quotation) return res + async def get_last_supplier_type(self, supplier_id: str) -> Res_LastSupplierType: + """협력사의 직전 견적 supplier_type(견적생성 모달 프리필용). 이력 없으면 비워서 반환.""" + res = Res_LastSupplierType() + err_type, got = await DB_SESSION_MNG.execute_lambda( + quotations.DBType(), + DBWRType.DB_READ.value, + lambda s: self.quotation_crud.get_last_supplier_type(s, uuid.UUID(supplier_id)), + ) + if err_type != ErrorType.SUCCESS: + res.result.SetResult(err_type) + return res + if got: + res.supplier_type = got[0] + res.qt_number = got[1] + return res + async def create_quotation(self, user_id: str, req: Req_CreateQuotation) -> Res_CreateQuotation: """[프론트] 신규 견적 생성. 요청값을 보정한 뒤 공통 빌더(_build_quotation)에 위임한다.""" return await self._build_quotation( @@ -153,6 +186,8 @@ class QuotationService: manager_email=req.manager_email, manager_contact_number=req.manager_contact_number, memo=req.memo, + md_price=req.md_price, + supplier_type=req.supplier_type, item_ids=req.item_ids, supplier_ids=req.supplier_ids, card_ids=req.card_ids, @@ -182,6 +217,8 @@ class QuotationService: lambda s: self.quotation_crud.list_sessions(s, original_qt_id), ) item_ids = list({r.item_id for r in rows}) if err_type == ErrorType.SUCCESS else [] + # 재생성은 목표가/앵커링가를 재계산하지 않고 직전 라운드 세션 값을 그대로 상속(KTC 방식). + inherited = {r.item_id: (r.target_price, r.target_anchoring_price) for r in rows} if err_type == ErrorType.SUCCESS else {} # 2) 타입 결정: 공급사 1곳 → 재협상 / 여러 곳 → 재견적 next_type = QuotationType.RENEGO.value if len(supplier_ids) <= 1 else QuotationType.REQUOTE.value @@ -218,22 +255,26 @@ class QuotationService: manager_email=original.manager_email, manager_contact_number=original.manager_contact_number, memo=original.memo, + md_price=original.md_price, + supplier_type=original.supplier_type, item_ids=item_ids, supplier_ids=list(supplier_ids), card_ids=[], # 새 버전 안 만듦(원본 version_id 재사용) + inherited=inherited, # 직전 라운드 목표가·앵커링가 상속(재계산 안 함) ) async def _build_quotation( self, *, user_id: str, qt_setting_id, version_id, name: str, number: str, type_: int, status: int, round_: int, start_time, end_time, - manager_name, manager_email, manager_contact_number, memo, + manager_name, manager_email, manager_contact_number, memo, md_price, supplier_type, item_ids: list, supplier_ids: list, card_ids: list, + inherited: Optional[dict] = None, # 재생성 시 {item_id: (target_price, target_anchoring_price)} 상속(KTC) — 있으면 재계산 안 함 ) -> Res_CreateQuotation: """견적 1건 + (상품×공급사) 세션들을 한 트랜잭션으로 생성하는 공통 빌더.""" res = Res_CreateQuotation() - # 세션 목표가 입력(상품 단가 + 견적 세팅 목표 마진율). 읽기 트랜잭션에서 먼저 조회. + # 세션 목표가 입력(상품별 인터넷최저가/매입가/판매가 + 세팅 율). 읽기 트랜잭션에서 먼저 조회. prices = {} if item_ids: _err, prices = await DB_SESSION_MNG.execute_lambda( @@ -242,12 +283,15 @@ class QuotationService: lambda s: self.quotation_crud.get_item_prices(s, item_ids), ) prices = prices if _err == ErrorType.SUCCESS else {} - _err, margin = await DB_SESSION_MNG.execute_lambda( + _err, rates = await DB_SESSION_MNG.execute_lambda( quotations.DBType(), DBWRType.DB_READ.value, - lambda s: self.quotation_crud.get_target_margin(s, qt_setting_id), + lambda s: self.quotation_crud.get_setting_rates(s, qt_setting_id), ) - margin = margin if _err == ErrorType.SUCCESS else None + rates = rates if _err == ErrorType.SUCCESS else {} + fee = rates.get("fee") or 0.0 # 인터넷가 차감 수수료율 + margin = rates.get("margin") or 0.0 # 판매가 차감 목표마진율 + anchoring = rates.get("anchoring") or 0.0 # 앵커링가 = 목표가×(1−값) # 선택 협상카드가 있으면 새 버전을 만들어 카드들을 묶고, quotation.version_id 로 연결한다. # (quotation↔card 는 version → version_nego_cards/version_wild_cards 로 연결.) @@ -293,27 +337,41 @@ class QuotationService: manager_email=manager_email, manager_contact_number=manager_contact_number, memo=memo, + md_price=md_price, + supplier_type=supplier_type, ) - # 상품 × 공급사 조합마다 세션 1개. + # 상품 × 공급사 조합마다 세션 1개. md/매입/판매/인터넷 후보가 하나도 없으면 목표가 산정 불가 → 생성 실패. + # 신규(NEW_NEGO/NEW_QUOTE)는 인터넷최저가만, 재(RENEGO/REQUOTE)는 매입가·판매가까지 후보(KTC 신규/재 분리). + is_new = type_ in (QuotationType.NEW_NEGO.value, QuotationType.NEW_QUOTE.value) session_objs = [] - for iid in item_ids: - tp = self._calc_target_price(prices.get(iid), margin) - for sid in supplier_ids: - session_objs.append( - sessions( - session_id=uuid.uuid4(), - quotation_id=qt_id, - item_id=iid, - supplier_id=sid, - qt_number=quotation.number, - qt_round=quotation.round, - qt_type=quotation.type, - target_price=tp, - status=SessionStatus.CREATED.value, - end_time=quotation.end_time, + try: + for iid in item_ids: + if inherited and iid in inherited: + tp, ap = inherited[iid] # 재생성: 직전 라운드 목표가·앵커링가 그대로 상속(KTC) — 재계산 안 함 + else: + internet, purchase, selling = prices.get(iid) or (None, None, None) + tp = self._calc_target_price(md_price, internet, purchase, selling, fee, margin, is_new=is_new) + ap = int(tp * (1 - anchoring)) # 앵커링가 = floor(목표가×(1−앵커링율)); 율 0이면 목표가와 동일 + for sid in supplier_ids: + session_objs.append( + sessions( + session_id=uuid.uuid4(), + quotation_id=qt_id, + item_id=iid, + supplier_id=sid, + qt_number=quotation.number, + qt_round=quotation.round, + qt_type=quotation.type, + target_price=tp, + target_anchoring_price=ap, + status=SessionStatus.CREATED.value, + end_time=quotation.end_time, + ) ) - ) + except ValueError: + res.result.SetResult(ErrorType.QUOTATION_TARGET_PRICE_UNAVAILABLE) + return res # 버전 → (버전-카드 매핑) → 견적 → 세션 순으로 한 트랜잭션에 insert(FK 순서 보장). ops = [] diff --git a/negodata/backend/services/quotation_setting_service.py b/negodata/backend/services/quotation_setting_service.py index e614324..69ecf84 100644 --- a/negodata/backend/services/quotation_setting_service.py +++ b/negodata/backend/services/quotation_setting_service.py @@ -66,6 +66,7 @@ class QuotationSettingService: user_id=uuid.UUID(user_id), target_margin_rate=req.target_margin_rate, anchoring_value=req.anchoring_value, + internet_average_fee=req.internet_average_fee, card_count=req.card_count, ) err_type = await DB_SESSION_MNG.execute_lambda_run( diff --git a/negodata/front/src/api/generated/model/cardData.ts b/negodata/front/src/api/generated/model/cardData.ts index c39fba8..ea26d31 100644 --- a/negodata/front/src/api/generated/model/cardData.ts +++ b/negodata/front/src/api/generated/model/cardData.ts @@ -9,6 +9,7 @@ import type { CardDataName } from './cardDataName'; import type { CardDataNumber } from './cardDataNumber'; import type { CardDataScript } from './cardDataScript'; import type { CardDataEditScript } from './cardDataEditScript'; +import type { CardUsageType } from './cardUsageType'; import type { CardStatus } from './cardStatus'; import type { CardDataCondition } from './cardDataCondition'; import type { CardDataMemo } from './cardDataMemo'; @@ -23,6 +24,7 @@ export interface CardData { number?: CardDataNumber; script?: CardDataScript; edit_script?: CardDataEditScript; + usage_type?: CardUsageType; status?: CardStatus; condition?: CardDataCondition; memo?: CardDataMemo; diff --git a/negodata/front/src/api/generated/model/cardUsageType.ts b/negodata/front/src/api/generated/model/cardUsageType.ts new file mode 100644 index 0000000..7db4d39 --- /dev/null +++ b/negodata/front/src/api/generated/model/cardUsageType.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * Negodata Api Server + * OpenAPI spec version: 0.1.0 + */ + +/** + * nego_cards/wild_cards.usage_type 코드값. 카드가 적용되는 견적 구분. +공통=신규·재 모두(기본), 신규전용=신규협상/신규견적(QuotationType 3·4), 재전용=재협상/재견적(1·2). + */ +export type CardUsageType = typeof CardUsageType[keyof typeof CardUsageType]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const CardUsageType = { + COMMON: 1, + NEW: 2, + REUSE: 3, +} as const; diff --git a/negodata/front/src/api/generated/model/index.ts b/negodata/front/src/api/generated/model/index.ts index 6501c93..36fbf55 100644 --- a/negodata/front/src/api/generated/model/index.ts +++ b/negodata/front/src/api/generated/model/index.ts @@ -18,6 +18,7 @@ export * from './cardDataUpdatedAt'; export * from './cardDataUserId'; export * from './cardStatus'; export * from './cardType'; +export * from './cardUsageType'; export * from './chatMessageData'; export * from './chatMessageDataCardId'; export * from './chatMessageDataCardType'; @@ -41,13 +42,16 @@ export * from './itemDataCreatedAt'; export * from './itemDataDeliveryFeeYn'; export * from './itemDataDeliveryType'; export * from './itemDataImageUrl'; +export * from './itemDataInternetLowestPrice'; export * from './itemDataLeadTime'; export * from './itemDataMadeIn'; export * from './itemDataManufacturer'; export * from './itemDataModelName'; export * from './itemDataMoq'; export * from './itemDataPrice'; +export * from './itemDataPurchasePrice'; export * from './itemDataQuantityUnit'; +export * from './itemDataSellingPrice'; export * from './itemDataSpec'; export * from './itemDataUpdatedAt'; export * from './itemDataVatYn'; @@ -75,10 +79,12 @@ export * from './quotationDataItemName'; export * from './quotationDataManagerContactNumber'; export * from './quotationDataManagerEmail'; export * from './quotationDataManagerName'; +export * from './quotationDataMdPrice'; export * from './quotationDataMemo'; export * from './quotationDataPreferredSpId'; export * from './quotationDataPreferredSpName'; export * from './quotationDataPreferredSpYn'; +export * from './quotationDataSupplierType'; export * from './quotationDataUpdatedAt'; export * from './quotationSettingData'; export * from './quotationSettingDataCreatedAt'; @@ -101,22 +107,27 @@ export * from './reqCreateItemCode'; export * from './reqCreateItemDeliveryFeeYn'; export * from './reqCreateItemDeliveryType'; export * from './reqCreateItemImageUrl'; +export * from './reqCreateItemInternetLowestPrice'; export * from './reqCreateItemLeadTime'; export * from './reqCreateItemMadeIn'; export * from './reqCreateItemManufacturer'; export * from './reqCreateItemModelName'; export * from './reqCreateItemMoq'; export * from './reqCreateItemPrice'; +export * from './reqCreateItemPurchasePrice'; export * from './reqCreateItemQuantityUnit'; +export * from './reqCreateItemSellingPrice'; export * from './reqCreateItemSpec'; export * from './reqCreateItemVatYn'; export * from './reqCreateQuotation'; export * from './reqCreateQuotationManagerContactNumber'; export * from './reqCreateQuotationManagerEmail'; export * from './reqCreateQuotationManagerName'; +export * from './reqCreateQuotationMdPrice'; export * from './reqCreateQuotationMemo'; export * from './reqCreateQuotationSetting'; export * from './reqCreateQuotationStartTime'; +export * from './reqCreateQuotationSupplierType'; export * from './reqCreateQuotationVersionId'; export * from './reqCreateSupplier'; export * from './reqCreateSupplierCode'; @@ -134,6 +145,7 @@ export * from './reqUpdateCardName'; export * from './reqUpdateCardNumber'; export * from './reqUpdateCardScript'; export * from './reqUpdateCardStatus'; +export * from './reqUpdateCardUsageType'; export * from './reqUpdateItem'; export * from './reqUpdateItemCategory'; export * from './reqUpdateItemCategoryType'; @@ -141,6 +153,7 @@ export * from './reqUpdateItemCode'; export * from './reqUpdateItemDeliveryFeeYn'; export * from './reqUpdateItemDeliveryType'; export * from './reqUpdateItemImageUrl'; +export * from './reqUpdateItemInternetLowestPrice'; export * from './reqUpdateItemInternetLowestPriceYn'; export * from './reqUpdateItemLeadTime'; export * from './reqUpdateItemMadeIn'; @@ -149,12 +162,15 @@ export * from './reqUpdateItemModelName'; export * from './reqUpdateItemMoq'; export * from './reqUpdateItemName'; export * from './reqUpdateItemPrice'; +export * from './reqUpdateItemPurchasePrice'; export * from './reqUpdateItemQuantityUnit'; +export * from './reqUpdateItemSellingPrice'; export * from './reqUpdateItemSpec'; export * from './reqUpdateItemVatYn'; export * from './reqUpdateQuotationSetting'; export * from './reqUpdateQuotationSettingAnchoringValue'; export * from './reqUpdateQuotationSettingCardCount'; +export * from './reqUpdateQuotationSettingInternetAverageFee'; export * from './reqUpdateQuotationSettingTargetMarginRate'; export * from './reqUpdateSupplier'; export * from './reqUpdateSupplierCode'; @@ -197,6 +213,10 @@ export * from './resItemItem'; export * from './resItemList'; export * from './resItemListMsg'; export * from './resItemMsg'; +export * from './resLastSupplierType'; +export * from './resLastSupplierTypeMsg'; +export * from './resLastSupplierTypeQtNumber'; +export * from './resLastSupplierTypeSupplierType'; export * from './resLogin'; export * from './resLoginMsg'; export * from './resLowestPriceResult'; @@ -260,6 +280,7 @@ export * from './supplierDataManagerEmail'; export * from './supplierDataManagerName'; export * from './supplierDataPriority'; export * from './supplierDataUpdatedAt'; +export * from './supplierType'; export * from './userRole'; export * from './validationError'; export * from './validationErrorCtx'; diff --git a/negodata/front/src/api/generated/model/itemData.ts b/negodata/front/src/api/generated/model/itemData.ts index 47c1448..1477144 100644 --- a/negodata/front/src/api/generated/model/itemData.ts +++ b/negodata/front/src/api/generated/model/itemData.ts @@ -12,6 +12,9 @@ import type { ItemDataSpec } from './itemDataSpec'; import type { ItemDataManufacturer } from './itemDataManufacturer'; import type { ItemDataMadeIn } from './itemDataMadeIn'; import type { ItemDataPrice } from './itemDataPrice'; +import type { ItemDataInternetLowestPrice } from './itemDataInternetLowestPrice'; +import type { ItemDataPurchasePrice } from './itemDataPurchasePrice'; +import type { ItemDataSellingPrice } from './itemDataSellingPrice'; import type { ItemDataMoq } from './itemDataMoq'; import type { ItemDataLeadTime } from './itemDataLeadTime'; import type { ItemDataQuantityUnit } from './itemDataQuantityUnit'; @@ -36,6 +39,9 @@ export interface ItemData { made_in?: ItemDataMadeIn; price?: ItemDataPrice; internet_lowest_price_yn?: boolean; + internet_lowest_price?: ItemDataInternetLowestPrice; + purchase_price?: ItemDataPurchasePrice; + selling_price?: ItemDataSellingPrice; moq?: ItemDataMoq; lead_time?: ItemDataLeadTime; quantity_unit?: ItemDataQuantityUnit; diff --git a/negodata/front/src/api/generated/model/itemDataInternetLowestPrice.ts b/negodata/front/src/api/generated/model/itemDataInternetLowestPrice.ts new file mode 100644 index 0000000..5b60282 --- /dev/null +++ b/negodata/front/src/api/generated/model/itemDataInternetLowestPrice.ts @@ -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 ItemDataInternetLowestPrice = number | null; diff --git a/negodata/front/src/api/generated/model/itemDataPurchasePrice.ts b/negodata/front/src/api/generated/model/itemDataPurchasePrice.ts new file mode 100644 index 0000000..3ccaf6d --- /dev/null +++ b/negodata/front/src/api/generated/model/itemDataPurchasePrice.ts @@ -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 ItemDataPurchasePrice = number | null; diff --git a/negodata/front/src/api/generated/model/itemDataSellingPrice.ts b/negodata/front/src/api/generated/model/itemDataSellingPrice.ts new file mode 100644 index 0000000..af31fd1 --- /dev/null +++ b/negodata/front/src/api/generated/model/itemDataSellingPrice.ts @@ -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 ItemDataSellingPrice = number | null; diff --git a/negodata/front/src/api/generated/model/quotationData.ts b/negodata/front/src/api/generated/model/quotationData.ts index ae7106b..2952862 100644 --- a/negodata/front/src/api/generated/model/quotationData.ts +++ b/negodata/front/src/api/generated/model/quotationData.ts @@ -10,6 +10,8 @@ import type { QuotationDataManagerName } from './quotationDataManagerName'; import type { QuotationDataManagerEmail } from './quotationDataManagerEmail'; import type { QuotationDataManagerContactNumber } from './quotationDataManagerContactNumber'; import type { QuotationDataMemo } from './quotationDataMemo'; +import type { QuotationDataMdPrice } from './quotationDataMdPrice'; +import type { QuotationDataSupplierType } from './quotationDataSupplierType'; import type { QuotationDataPreferredSpYn } from './quotationDataPreferredSpYn'; import type { QuotationDataPreferredSpId } from './quotationDataPreferredSpId'; import type { QuotationDataPreferredSpName } from './quotationDataPreferredSpName'; @@ -36,6 +38,8 @@ export interface QuotationData { manager_email?: QuotationDataManagerEmail; manager_contact_number?: QuotationDataManagerContactNumber; memo?: QuotationDataMemo; + md_price?: QuotationDataMdPrice; + supplier_type?: QuotationDataSupplierType; iteration?: number; preferred_sp_yn?: QuotationDataPreferredSpYn; preferred_sp_id?: QuotationDataPreferredSpId; diff --git a/negodata/front/src/api/generated/model/quotationDataMdPrice.ts b/negodata/front/src/api/generated/model/quotationDataMdPrice.ts new file mode 100644 index 0000000..6115cca --- /dev/null +++ b/negodata/front/src/api/generated/model/quotationDataMdPrice.ts @@ -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 QuotationDataMdPrice = number | null; diff --git a/negodata/front/src/api/generated/model/quotationDataSupplierType.ts b/negodata/front/src/api/generated/model/quotationDataSupplierType.ts new file mode 100644 index 0000000..8f47105 --- /dev/null +++ b/negodata/front/src/api/generated/model/quotationDataSupplierType.ts @@ -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 { SupplierType } from './supplierType'; + +export type QuotationDataSupplierType = SupplierType | null; diff --git a/negodata/front/src/api/generated/model/quotationSettingData.ts b/negodata/front/src/api/generated/model/quotationSettingData.ts index 8c49ff9..35d46d0 100644 --- a/negodata/front/src/api/generated/model/quotationSettingData.ts +++ b/negodata/front/src/api/generated/model/quotationSettingData.ts @@ -13,6 +13,7 @@ export interface QuotationSettingData { user_id?: QuotationSettingDataUserId; target_margin_rate: number; anchoring_value: number; + internet_average_fee?: number; card_count: number; created_at?: QuotationSettingDataCreatedAt; updated_at?: QuotationSettingDataUpdatedAt; diff --git a/negodata/front/src/api/generated/model/quotationType.ts b/negodata/front/src/api/generated/model/quotationType.ts index 87f9dce..407907f 100644 --- a/negodata/front/src/api/generated/model/quotationType.ts +++ b/negodata/front/src/api/generated/model/quotationType.ts @@ -6,7 +6,9 @@ */ /** - * quotations.type 코드값. 1=renego(재협상 1:1), 2=requote(재견적 1:N). + * quotations.type 코드값. 신규/재 × 협상(1:1)/견적(1:N). +1=재협상(1:1), 2=재견적(1:N), 3=신규협상(1:1), 4=신규견적(1:N). +기존 데이터 보존 위해 재협상/재견적 코드(1·2)는 고정, 신규는 3·4로 추가. */ export type QuotationType = typeof QuotationType[keyof typeof QuotationType]; @@ -15,4 +17,6 @@ export type QuotationType = typeof QuotationType[keyof typeof QuotationType]; export const QuotationType = { RENEGO: 1, REQUOTE: 2, + NEW_NEGO: 3, + NEW_QUOTE: 4, } as const; diff --git a/negodata/front/src/api/generated/model/reqCreateCard.ts b/negodata/front/src/api/generated/model/reqCreateCard.ts index 30a465b..467e7da 100644 --- a/negodata/front/src/api/generated/model/reqCreateCard.ts +++ b/negodata/front/src/api/generated/model/reqCreateCard.ts @@ -17,6 +17,7 @@ export interface ReqCreateCard { number?: ReqCreateCardNumber; script?: ReqCreateCardScript; edit_script?: ReqCreateCardEditScript; + usage_type?: number; status?: number; condition?: ReqCreateCardCondition; memo?: ReqCreateCardMemo; diff --git a/negodata/front/src/api/generated/model/reqCreateItem.ts b/negodata/front/src/api/generated/model/reqCreateItem.ts index ccfb89a..6c90917 100644 --- a/negodata/front/src/api/generated/model/reqCreateItem.ts +++ b/negodata/front/src/api/generated/model/reqCreateItem.ts @@ -12,6 +12,9 @@ import type { ReqCreateItemSpec } from './reqCreateItemSpec'; import type { ReqCreateItemManufacturer } from './reqCreateItemManufacturer'; import type { ReqCreateItemMadeIn } from './reqCreateItemMadeIn'; import type { ReqCreateItemPrice } from './reqCreateItemPrice'; +import type { ReqCreateItemInternetLowestPrice } from './reqCreateItemInternetLowestPrice'; +import type { ReqCreateItemPurchasePrice } from './reqCreateItemPurchasePrice'; +import type { ReqCreateItemSellingPrice } from './reqCreateItemSellingPrice'; import type { ReqCreateItemMoq } from './reqCreateItemMoq'; import type { ReqCreateItemLeadTime } from './reqCreateItemLeadTime'; import type { ReqCreateItemQuantityUnit } from './reqCreateItemQuantityUnit'; @@ -31,6 +34,9 @@ export interface ReqCreateItem { made_in?: ReqCreateItemMadeIn; price?: ReqCreateItemPrice; internet_lowest_price_yn?: boolean; + internet_lowest_price?: ReqCreateItemInternetLowestPrice; + purchase_price?: ReqCreateItemPurchasePrice; + selling_price?: ReqCreateItemSellingPrice; moq?: ReqCreateItemMoq; lead_time?: ReqCreateItemLeadTime; quantity_unit?: ReqCreateItemQuantityUnit; diff --git a/negodata/front/src/api/generated/model/reqCreateItemInternetLowestPrice.ts b/negodata/front/src/api/generated/model/reqCreateItemInternetLowestPrice.ts new file mode 100644 index 0000000..af0c8c8 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqCreateItemInternetLowestPrice.ts @@ -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 ReqCreateItemInternetLowestPrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqCreateItemPurchasePrice.ts b/negodata/front/src/api/generated/model/reqCreateItemPurchasePrice.ts new file mode 100644 index 0000000..1f91c80 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqCreateItemPurchasePrice.ts @@ -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 ReqCreateItemPurchasePrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqCreateItemSellingPrice.ts b/negodata/front/src/api/generated/model/reqCreateItemSellingPrice.ts new file mode 100644 index 0000000..5183140 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqCreateItemSellingPrice.ts @@ -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 ReqCreateItemSellingPrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqCreateQuotation.ts b/negodata/front/src/api/generated/model/reqCreateQuotation.ts index 60dab99..e51bce6 100644 --- a/negodata/front/src/api/generated/model/reqCreateQuotation.ts +++ b/negodata/front/src/api/generated/model/reqCreateQuotation.ts @@ -10,6 +10,8 @@ import type { ReqCreateQuotationManagerName } from './reqCreateQuotationManagerN import type { ReqCreateQuotationManagerEmail } from './reqCreateQuotationManagerEmail'; import type { ReqCreateQuotationManagerContactNumber } from './reqCreateQuotationManagerContactNumber'; import type { ReqCreateQuotationMemo } from './reqCreateQuotationMemo'; +import type { ReqCreateQuotationMdPrice } from './reqCreateQuotationMdPrice'; +import type { ReqCreateQuotationSupplierType } from './reqCreateQuotationSupplierType'; export interface ReqCreateQuotation { qt_setting_id: string; @@ -24,6 +26,8 @@ export interface ReqCreateQuotation { manager_email?: ReqCreateQuotationManagerEmail; manager_contact_number?: ReqCreateQuotationManagerContactNumber; memo?: ReqCreateQuotationMemo; + md_price?: ReqCreateQuotationMdPrice; + supplier_type?: ReqCreateQuotationSupplierType; item_ids?: string[]; supplier_ids?: string[]; card_ids?: string[]; diff --git a/negodata/front/src/api/generated/model/reqCreateQuotationMdPrice.ts b/negodata/front/src/api/generated/model/reqCreateQuotationMdPrice.ts new file mode 100644 index 0000000..3b5e687 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqCreateQuotationMdPrice.ts @@ -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 ReqCreateQuotationMdPrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqCreateQuotationSetting.ts b/negodata/front/src/api/generated/model/reqCreateQuotationSetting.ts index 4bc40a5..6254a2e 100644 --- a/negodata/front/src/api/generated/model/reqCreateQuotationSetting.ts +++ b/negodata/front/src/api/generated/model/reqCreateQuotationSetting.ts @@ -8,5 +8,6 @@ export interface ReqCreateQuotationSetting { target_margin_rate: number; anchoring_value?: number; + internet_average_fee?: number; card_count?: number; } diff --git a/negodata/front/src/api/generated/model/reqCreateQuotationSupplierType.ts b/negodata/front/src/api/generated/model/reqCreateQuotationSupplierType.ts new file mode 100644 index 0000000..38bc008 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqCreateQuotationSupplierType.ts @@ -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 ReqCreateQuotationSupplierType = number | null; diff --git a/negodata/front/src/api/generated/model/reqUpdateCard.ts b/negodata/front/src/api/generated/model/reqUpdateCard.ts index 470eb5c..646a297 100644 --- a/negodata/front/src/api/generated/model/reqUpdateCard.ts +++ b/negodata/front/src/api/generated/model/reqUpdateCard.ts @@ -8,6 +8,7 @@ import type { ReqUpdateCardName } from './reqUpdateCardName'; import type { ReqUpdateCardNumber } from './reqUpdateCardNumber'; import type { ReqUpdateCardScript } from './reqUpdateCardScript'; import type { ReqUpdateCardEditScript } from './reqUpdateCardEditScript'; +import type { ReqUpdateCardUsageType } from './reqUpdateCardUsageType'; import type { ReqUpdateCardStatus } from './reqUpdateCardStatus'; import type { ReqUpdateCardCondition } from './reqUpdateCardCondition'; import type { ReqUpdateCardMemo } from './reqUpdateCardMemo'; @@ -17,6 +18,7 @@ export interface ReqUpdateCard { number?: ReqUpdateCardNumber; script?: ReqUpdateCardScript; edit_script?: ReqUpdateCardEditScript; + usage_type?: ReqUpdateCardUsageType; status?: ReqUpdateCardStatus; condition?: ReqUpdateCardCondition; memo?: ReqUpdateCardMemo; diff --git a/negodata/front/src/api/generated/model/reqUpdateCardUsageType.ts b/negodata/front/src/api/generated/model/reqUpdateCardUsageType.ts new file mode 100644 index 0000000..7b36b65 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqUpdateCardUsageType.ts @@ -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 ReqUpdateCardUsageType = number | null; diff --git a/negodata/front/src/api/generated/model/reqUpdateItem.ts b/negodata/front/src/api/generated/model/reqUpdateItem.ts index b295929..2b0dca9 100644 --- a/negodata/front/src/api/generated/model/reqUpdateItem.ts +++ b/negodata/front/src/api/generated/model/reqUpdateItem.ts @@ -15,6 +15,9 @@ import type { ReqUpdateItemManufacturer } from './reqUpdateItemManufacturer'; import type { ReqUpdateItemMadeIn } from './reqUpdateItemMadeIn'; import type { ReqUpdateItemPrice } from './reqUpdateItemPrice'; import type { ReqUpdateItemInternetLowestPriceYn } from './reqUpdateItemInternetLowestPriceYn'; +import type { ReqUpdateItemInternetLowestPrice } from './reqUpdateItemInternetLowestPrice'; +import type { ReqUpdateItemPurchasePrice } from './reqUpdateItemPurchasePrice'; +import type { ReqUpdateItemSellingPrice } from './reqUpdateItemSellingPrice'; import type { ReqUpdateItemMoq } from './reqUpdateItemMoq'; import type { ReqUpdateItemLeadTime } from './reqUpdateItemLeadTime'; import type { ReqUpdateItemQuantityUnit } from './reqUpdateItemQuantityUnit'; @@ -34,6 +37,9 @@ export interface ReqUpdateItem { made_in?: ReqUpdateItemMadeIn; price?: ReqUpdateItemPrice; internet_lowest_price_yn?: ReqUpdateItemInternetLowestPriceYn; + internet_lowest_price?: ReqUpdateItemInternetLowestPrice; + purchase_price?: ReqUpdateItemPurchasePrice; + selling_price?: ReqUpdateItemSellingPrice; moq?: ReqUpdateItemMoq; lead_time?: ReqUpdateItemLeadTime; quantity_unit?: ReqUpdateItemQuantityUnit; diff --git a/negodata/front/src/api/generated/model/reqUpdateItemInternetLowestPrice.ts b/negodata/front/src/api/generated/model/reqUpdateItemInternetLowestPrice.ts new file mode 100644 index 0000000..c4ff037 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqUpdateItemInternetLowestPrice.ts @@ -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 ReqUpdateItemInternetLowestPrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqUpdateItemPurchasePrice.ts b/negodata/front/src/api/generated/model/reqUpdateItemPurchasePrice.ts new file mode 100644 index 0000000..00a7f4b --- /dev/null +++ b/negodata/front/src/api/generated/model/reqUpdateItemPurchasePrice.ts @@ -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 ReqUpdateItemPurchasePrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqUpdateItemSellingPrice.ts b/negodata/front/src/api/generated/model/reqUpdateItemSellingPrice.ts new file mode 100644 index 0000000..8e410a0 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqUpdateItemSellingPrice.ts @@ -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 ReqUpdateItemSellingPrice = number | null; diff --git a/negodata/front/src/api/generated/model/reqUpdateQuotationSetting.ts b/negodata/front/src/api/generated/model/reqUpdateQuotationSetting.ts index 857bcd5..1235351 100644 --- a/negodata/front/src/api/generated/model/reqUpdateQuotationSetting.ts +++ b/negodata/front/src/api/generated/model/reqUpdateQuotationSetting.ts @@ -6,10 +6,12 @@ */ import type { ReqUpdateQuotationSettingTargetMarginRate } from './reqUpdateQuotationSettingTargetMarginRate'; import type { ReqUpdateQuotationSettingAnchoringValue } from './reqUpdateQuotationSettingAnchoringValue'; +import type { ReqUpdateQuotationSettingInternetAverageFee } from './reqUpdateQuotationSettingInternetAverageFee'; import type { ReqUpdateQuotationSettingCardCount } from './reqUpdateQuotationSettingCardCount'; export interface ReqUpdateQuotationSetting { target_margin_rate?: ReqUpdateQuotationSettingTargetMarginRate; anchoring_value?: ReqUpdateQuotationSettingAnchoringValue; + internet_average_fee?: ReqUpdateQuotationSettingInternetAverageFee; card_count?: ReqUpdateQuotationSettingCardCount; } diff --git a/negodata/front/src/api/generated/model/reqUpdateQuotationSettingInternetAverageFee.ts b/negodata/front/src/api/generated/model/reqUpdateQuotationSettingInternetAverageFee.ts new file mode 100644 index 0000000..7678e83 --- /dev/null +++ b/negodata/front/src/api/generated/model/reqUpdateQuotationSettingInternetAverageFee.ts @@ -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 ReqUpdateQuotationSettingInternetAverageFee = number | null; diff --git a/negodata/front/src/api/generated/model/resLastSupplierType.ts b/negodata/front/src/api/generated/model/resLastSupplierType.ts new file mode 100644 index 0000000..a6f3f29 --- /dev/null +++ b/negodata/front/src/api/generated/model/resLastSupplierType.ts @@ -0,0 +1,17 @@ +/** + * 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 { ResLastSupplierTypeMsg } from './resLastSupplierTypeMsg'; +import type { ResLastSupplierTypeSupplierType } from './resLastSupplierTypeSupplierType'; +import type { ResLastSupplierTypeQtNumber } from './resLastSupplierTypeQtNumber'; + +export interface ResLastSupplierType { + result?: ErrorInfo; + msg?: ResLastSupplierTypeMsg; + supplier_type?: ResLastSupplierTypeSupplierType; + qt_number?: ResLastSupplierTypeQtNumber; +} diff --git a/negodata/front/src/api/generated/model/resLastSupplierTypeMsg.ts b/negodata/front/src/api/generated/model/resLastSupplierTypeMsg.ts new file mode 100644 index 0000000..ee369fc --- /dev/null +++ b/negodata/front/src/api/generated/model/resLastSupplierTypeMsg.ts @@ -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 ResLastSupplierTypeMsg = string | null; diff --git a/negodata/front/src/api/generated/model/resLastSupplierTypeQtNumber.ts b/negodata/front/src/api/generated/model/resLastSupplierTypeQtNumber.ts new file mode 100644 index 0000000..01ac822 --- /dev/null +++ b/negodata/front/src/api/generated/model/resLastSupplierTypeQtNumber.ts @@ -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 ResLastSupplierTypeQtNumber = string | null; diff --git a/negodata/front/src/api/generated/model/resLastSupplierTypeSupplierType.ts b/negodata/front/src/api/generated/model/resLastSupplierTypeSupplierType.ts new file mode 100644 index 0000000..313d621 --- /dev/null +++ b/negodata/front/src/api/generated/model/resLastSupplierTypeSupplierType.ts @@ -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 { SupplierType } from './supplierType'; + +export type ResLastSupplierTypeSupplierType = SupplierType | null; diff --git a/negodata/front/src/api/generated/model/supplierType.ts b/negodata/front/src/api/generated/model/supplierType.ts new file mode 100644 index 0000000..6fbada4 --- /dev/null +++ b/negodata/front/src/api/generated/model/supplierType.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v7.21.0 🍺 + * Do not edit manually. + * Negodata Api Server + * OpenAPI spec version: 0.1.0 + */ + +/** + * suppliers.type 코드값(KTC 앵커링 기준). 0=기타는 매칭 실패 폴백용 — UI 선택지엔 미노출. + */ +export type SupplierType = typeof SupplierType[keyof typeof SupplierType]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SupplierType = { + DISTRIBUTION: 1, + MANUFACTURE: 2, + SOLE_AGENCY: 3, + ETC: 0, +} as const; diff --git a/negodata/front/src/api/generated/quotation/quotation.ts b/negodata/front/src/api/generated/quotation/quotation.ts index c20a974..05bc040 100644 --- a/negodata/front/src/api/generated/quotation/quotation.ts +++ b/negodata/front/src/api/generated/quotation/quotation.ts @@ -30,6 +30,7 @@ import type { ReqRegenerateQuotation, ResCreateQuotation, ResDeleteQuotation, + ResLastSupplierType, ResQuotation, ResQuotationCards, ResQuotationList, @@ -852,6 +853,98 @@ export const useDeleteQuotation = ,signal?: AbortSignal +) => { + + + return customFetch( + {url: `/v1/quotation/supplier/${supplierId}/last-type`, method: 'GET', signal + }, + options); + } + + + + +export const getGetSupplierLastTypeQueryKey = (supplierId?: string,) => { + return [ + `/v1/quotation/supplier/${supplierId}/last-type` + ] as const; + } + + +export const getGetSupplierLastTypeQueryOptions = >, TError = void | HTTPValidationError>(supplierId: string, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSupplierLastTypeQueryKey(supplierId); + + + + const queryFn: QueryFunction>> = ({ signal }) => getSupplierLastType(supplierId, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(supplierId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetSupplierLastTypeQueryResult = NonNullable>> +export type GetSupplierLastTypeQueryError = void | HTTPValidationError + + +export function useGetSupplierLastType>, TError = void | HTTPValidationError>( + supplierId: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetSupplierLastType>, TError = void | HTTPValidationError>( + supplierId: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetSupplierLastType>, TError = void | HTTPValidationError>( + supplierId: string, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary 협력사 직전 견적 유형 + */ + +export function useGetSupplierLastType>, TError = void | HTTPValidationError>( + supplierId: string, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetSupplierLastTypeQueryOptions(supplierId,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + +/** * @summary 견적 조회 */ export const getQuotation = ( diff --git a/negodata/front/src/features/cards/components/CardFormSheet.tsx b/negodata/front/src/features/cards/components/CardFormSheet.tsx index 8f136b7..e75a0cd 100644 --- a/negodata/front/src/features/cards/components/CardFormSheet.tsx +++ b/negodata/front/src/features/cards/components/CardFormSheet.tsx @@ -9,11 +9,13 @@ import { Input } from '@/components/ui/input'; import { Sheet } from '@/components/ui/sheet'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { type NegotiationCard, type CardTab, generateCardCode } from '../types'; +import { CardUsageType } from '@/api/generated/model'; import type { CardInput } from '../hooks/useCards'; import { CardScriptEditor, deserialize, serializeToText } from '../editor'; const schema = z.object({ isWildcard: z.boolean(), + usageType: z.number(), code: z.string().trim().min(1, '카드번호는 필수 기입 사항입니다.'), title: z.string().trim().min(1, '카드이름은 필수 기입 사항입니다.'), editorScript: z @@ -45,6 +47,7 @@ function buildDefaults( if (mode === 'edit' && card) { return { isWildcard: card.isWildcard, + usageType: card.usageType, code: card.code, title: card.title, // 저장된 Slate JSON 우선, 없으면 레거시 평문 script 를 변수 노드로 복원. @@ -57,6 +60,7 @@ function buildDefaults( const wild = activeTab === 'WILD'; return { isWildcard: wild, + usageType: CardUsageType.COMMON, // 기본: 공통(신규·재 모두) code: generateCardCode(wild), title: '', editorScript: deserialize(), // 새 빈 값(공용 상수 mutate 방지) @@ -100,6 +104,7 @@ export function CardFormSheet({ editorScript: v.editorScript, status: v.status, isWildcard: v.isWildcard, + usageType: v.usageType, triggerCondition: v.triggerCondition, memo: v.memo, }; @@ -220,6 +225,34 @@ export function CardFormSheet({ + {/* 카드 용도(usage_type): 공통 / 신규전용 / 재전용 */} +
+ 카드 용도 + ( + + )} + /> +
+ {/* Title */}
카드이름 diff --git a/negodata/front/src/features/cards/hooks/useCards.ts b/negodata/front/src/features/cards/hooks/useCards.ts index 3afbc4a..e2455f5 100644 --- a/negodata/front/src/features/cards/hooks/useCards.ts +++ b/negodata/front/src/features/cards/hooks/useCards.ts @@ -21,6 +21,7 @@ export type CardInput = { editorScript: Descendant[]; status: 'ACTIVE' | 'INACTIVE'; isWildcard: boolean; + usageType: number; // usage_type(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 triggerCondition?: string; memo?: string; }; @@ -36,6 +37,7 @@ function cardError(res: ResCard): string | null { function toReq(input: CardInput): ReqCreateCard { return { is_wildcard: input.isWildcard, + usage_type: input.usageType, name: input.title, number: input.code, script: serializeToText(input.editorScript), // 평문 미리보기({변수} 토큰 포함) diff --git a/negodata/front/src/features/cards/types.ts b/negodata/front/src/features/cards/types.ts index 19556c4..c885635 100644 --- a/negodata/front/src/features/cards/types.ts +++ b/negodata/front/src/features/cards/types.ts @@ -1,6 +1,6 @@ import type { NegotiationCard } from '@/types'; import type { CardData } from '@/api/generated/model/cardData'; -import { CardStatus } from '@/api/generated/model'; +import { CardStatus, CardUsageType } from '@/api/generated/model'; export type { NegotiationCard }; @@ -17,6 +17,7 @@ export function mapCardData(c: CardData): NegotiationCard { return { id: c.nego_card_id, isWildcard: c.is_wildcard ?? false, + usageType: c.usage_type ?? CardUsageType.COMMON, code: c.number ?? '', title: c.name ?? '', scriptPreview: c.script ?? '', diff --git a/negodata/front/src/features/products/components/ProductFormSheet.tsx b/negodata/front/src/features/products/components/ProductFormSheet.tsx index 26b1746..3556da7 100644 --- a/negodata/front/src/features/products/components/ProductFormSheet.tsx +++ b/negodata/front/src/features/products/components/ProductFormSheet.tsx @@ -33,6 +33,8 @@ const schema = z.object({ vatYn: z.boolean(), deliveryFeeYn: z.boolean(), internetLowestPriceYn: z.boolean(), + purchasePrice: z.number({ message: '숫자를 입력해 주십시오.' }).min(0, '매입가는 0 이상이어야 합니다.'), + sellingPrice: z.number({ message: '숫자를 입력해 주십시오.' }).min(0, '판매가는 0 이상이어야 합니다.'), }); type FormValues = z.infer; @@ -71,6 +73,8 @@ function buildDefaults(mode: 'create' | 'edit', product: Product | null): FormVa vatYn: product.vat_yn !== false, deliveryFeeYn: product.delivery_fee_yn || false, internetLowestPriceYn: product.internet_lowest_price_yn || false, + purchasePrice: product.purchase_price || 0, + sellingPrice: product.selling_price || 0, }; } return { @@ -91,6 +95,8 @@ function buildDefaults(mode: 'create' | 'edit', product: Product | null): FormVa vatYn: true, deliveryFeeYn: false, internetLowestPriceYn: true, + purchasePrice: 0, + sellingPrice: 0, }; } @@ -145,6 +151,8 @@ export function ProductFormSheet({ vat_yn: v.vatYn, delivery_fee_yn: v.deliveryFeeYn, internet_lowest_price_yn: v.internetLowestPriceYn, + purchase_price: v.purchasePrice, + selling_price: v.sellingPrice, }; if (mode === 'create') { @@ -262,6 +270,33 @@ export function ProductFormSheet({
+
+ {/* 매입가 */} +
+ 매입가 (₩) + + {errors.purchasePrice &&

{errors.purchasePrice.message}

} +
+ {/* 판매가 */} +
+ 판매가 (₩) + + {errors.sellingPrice &&

{errors.sellingPrice.message}

} +
+
+
{/* Model Name */}
diff --git a/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx b/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx index aacfa10..9dcc606 100644 --- a/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx +++ b/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx @@ -1,5 +1,7 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { X, PlusSquare, ArrowRight, Loader2 } from 'lucide-react'; +import { useGetSupplierLastType } from '@/api/generated/quotation/quotation'; +import { updateItem } from '@/api/generated/item/item'; import { Button } from '@/components/ui/button'; import { Typography } from '@/components/ui/typography'; import { Input } from '@/components/ui/input'; @@ -7,7 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import type { Product, Partner, QuotationSetting, NegotiationCard } from '../types'; import type { CreateQuotationInput } from '../hooks/useQuotations'; import { QuotationType } from '@/api/generated/model'; -import { QUOTATION_TYPE_OPTIONS } from '../types'; +import { QUOTATION_TYPE_OPTIONS, supplierTypeOptions } from '../types'; +import { showToast } from '@/lib/notify'; // datetime-local 디폴트값: 현재 한국시간(Asia/Seoul)의 'YYYY-MM-DDTHH:mm'. // sv-SE 로케일이 'YYYY-MM-DD HH:mm:ss' 를 주고, timeZone 명시로 브라우저 TZ 와 무관하게 KST 로 고정한다. @@ -44,9 +47,37 @@ export function QuotationCreateModal({ const [settingId, setSettingId] = useState(quotationSettings[0]?.qt_setting_id ?? ''); const [selectedCardIds, setSelectedCardIds] = useState([]); const [memo, setMemo] = useState(''); + const [mdPrice, setMdPrice] = useState(''); // MD 제시가(원). 비우면 미전송 → 서버가 마진식으로 목표가 산정 + const [purchaseInput, setPurchaseInput] = useState(''); // 재견적·재협상 매입가(상품 저장값 디폴트, 필수) + const [sellingInput, setSellingInput] = useState(''); // 재견적·재협상 판매가(상품 저장값 디폴트, 선택) + const [supplierType, setSupplierType] = useState(''); // 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록 const [submitting, setSubmitting] = useState(false); const typeOptions = QUOTATION_TYPE_OPTIONS; + // 협력사 유형은 재협상(1:1)에서만. 선택된 협력사의 직전 견적 supplier_type 을 조회해 디폴트로 채운다. + const renegoSupplierId = type === QuotationType.RENEGO ? (selectedPartnerIds[0] ?? '') : ''; + const lastTypeQuery = useGetSupplierLastType(renegoSupplierId, { + query: { enabled: !!renegoSupplierId }, + }); + const prevSupplierType = lastTypeQuery.data?.supplier_type ?? null; // 협력사 직전 견적 유형(없으면 null) + const prevQtNumber = lastTypeQuery.data?.qt_number ?? ''; + // 협력사가 정해지면 직전 견적 유형으로 디폴트(이후 사용자가 바꾸면 그 값 유지). + useEffect(() => { + setSupplierType(prevSupplierType != null ? String(prevSupplierType) : ''); + }, [renegoSupplierId, prevSupplierType]); + + // 재견적·재협상(RE)에서만 매입가/판매가 입력을 노출한다. 상품이 정해지면 그 상품 저장값으로 디폴트. + const selectedProduct = products.find((p) => p.id === productId); + const isReType = type === QuotationType.RENEGO || type === QuotationType.REQUOTE; + const showPrices = isReType && !!productId; + // 상품/유형이 바뀌면 그 상품의 저장된 매입가·판매가로 입력칸을 채운다(이후 사용자가 고치면 유지). + useEffect(() => { + if (!isReType || !selectedProduct) return; + setPurchaseInput(selectedProduct.purchase_price != null ? String(selectedProduct.purchase_price) : ''); + setSellingInput(selectedProduct.selling_price != null ? String(selectedProduct.selling_price) : ''); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [productId, isReType]); + if (!open) return null; const togglePartner = (id: string) => @@ -64,6 +95,17 @@ export function QuotationCreateModal({ if (submitting) return; setSubmitting(true); try { + // 재견적·재협상은 매입가 필수 — 입력된 매입가/판매가를 상품에 저장한 뒤 진행한다. + if (showPrices) { + if (!purchaseInput) { + showToast('재견적·재협상은 매입가가 필수입니다.', 'error'); + return; // finally 에서 submitting 해제 + } + await updateItem(productId, { + purchase_price: Number(purchaseInput), + selling_price: sellingInput ? Number(sellingInput) : undefined, + }); + } // 서버가 견적+세션 생성을 끝내고 응답할 때까지 기다린 뒤에 완료(닫기) 처리한다. const ok = await onCreate({ title, @@ -74,6 +116,8 @@ export function QuotationCreateModal({ settingId, cardIds: selectedCardIds, memo, + mdPrice: mdPrice ? Number(mdPrice) : null, + supplierType: supplierType ? Number(supplierType) : null, }); if (ok) onClose(); } finally { @@ -92,7 +136,7 @@ export function QuotationCreateModal({
)} -
+
{/* Header */}
@@ -107,11 +151,11 @@ export function QuotationCreateModal({ {/* Steps indicator */}
- = 1 ? 'text-primary' : 'text-muted-foreground'}`}>1. 기본 등록 + = 1 ? 'text-primary' : 'text-muted-foreground'}`}>1. 기본 정보 - = 2 ? 'text-primary' : 'text-muted-foreground'}`}>2. 협력사 선택 + = 2 ? 'text-primary' : 'text-muted-foreground'}`}>2. 협력사 초청 - = 3 ? 'text-primary' : 'text-muted-foreground'}`}>3. 설정 및 완료 + = 3 ? 'text-primary' : 'text-muted-foreground'}`}>3. 설정·완료
{/* Step content */} @@ -119,21 +163,10 @@ export function QuotationCreateModal({ {step === 1 && (
-
- 견적건명 - setTitle(e.target.value)} - placeholder="예: 6월 배터리 원부자재 견적 의뢰" - /> -
- -
+ {/* 견적 유형이 맨 위 — 신규/재 여부가 아래 매입가 필수 여부까지 결정한다 */} +
- 유형 + 견적 유형 setTitle(e.target.value)} + placeholder="예: 6월 배터리 원부자재 견적 의뢰" + /> +
+
상품
+ + {/* MD 제시가 — 신규·재 공통(입력 시 목표가로 사용) */} +
+ MD 제시가 (선택) + setMdPrice(e.target.value)} + placeholder="입력 시 목표가로 사용 · 미입력 시 자동 산정" + /> +
+ + {/* 재견적·재협상 — 매입가(필수)·판매가, 선택 상품의 저장값으로 디폴트 */} + {showPrices && ( +
+
+ 매입가 (필수) + setPurchaseInput(e.target.value)} + placeholder="상품 저장값 · 비우면 진행 불가" + /> +
+
+ 판매가 (선택) + setSellingInput(e.target.value)} + placeholder="상품 저장값 · 마진 상한 산정에 사용" + /> +
+
+ )}
)} @@ -226,6 +315,34 @@ export function QuotationCreateModal({ ); })}
+ + {/* 협력사 유형 — 재협상(1:1)에서 협력사 선택 후 노출. 직전 견적 값 자동 디폴트. */} + {type === QuotationType.RENEGO && selectedPartnerIds.length > 0 && ( +
+ 협력사 유형 + + {prevSupplierType != null && ( + + 이전 견적({prevQtNumber}) 값으로 자동 선택됨 · 수정 가능 + + )} +
+ )}
)} @@ -317,7 +434,17 @@ export function QuotationCreateModal({
{step < 3 ? ( - ) : ( diff --git a/negodata/front/src/features/quotations/components/QuotationSettingsModal.tsx b/negodata/front/src/features/quotations/components/QuotationSettingsModal.tsx index 3fc9ece..b13dc5d 100644 --- a/negodata/front/src/features/quotations/components/QuotationSettingsModal.tsx +++ b/negodata/front/src/features/quotations/components/QuotationSettingsModal.tsx @@ -24,16 +24,18 @@ export function QuotationSettingsModal({ }: QuotationSettingsModalProps) { const [targetMargin, setTargetMargin] = useState(''); const [anchoringValue, setAnchoringValue] = useState(''); + const [internetFee, setInternetFee] = useState('7.8'); const [cardUseCount, setCardUseCount] = useState(''); if (!open) return null; const handleAdd = (e: React.FormEvent) => { e.preventDefault(); - const ok = onAdd({ targetMargin, anchoringValue, cardUseCount }); + const ok = onAdd({ targetMargin, anchoringValue, internetFee, cardUseCount }); if (ok) { setTargetMargin(''); setAnchoringValue(''); + setInternetFee('7.8'); setCardUseCount(''); } }; @@ -99,7 +101,7 @@ export function QuotationSettingsModal({
신규 견적 세팅 추가 -
+
목표 마진율 (%) setTargetMargin(e.target.value)} placeholder="예: 12" /> @@ -108,6 +110,10 @@ export function QuotationSettingsModal({ 앵커링 값 setAnchoringValue(e.target.value)} placeholder="예: 0.01" />
+
+ 인터넷 수수료율 (%) + setInternetFee(e.target.value)} placeholder="예: 7.8" /> +
카드 사용 횟수 setCardUseCount(e.target.value)} placeholder="예: 3" /> diff --git a/negodata/front/src/features/quotations/hooks/useQuotations.ts b/negodata/front/src/features/quotations/hooks/useQuotations.ts index 372e4aa..63ae1b0 100644 --- a/negodata/front/src/features/quotations/hooks/useQuotations.ts +++ b/negodata/front/src/features/quotations/hooks/useQuotations.ts @@ -36,11 +36,14 @@ export type CreateQuotationInput = { settingId: string; cardIds: string[]; memo: string; + mdPrice?: number | null; // MD 제시가(원). 비우면 미전송 → 서버가 기존 마진식으로 목표가 산정 + supplierType?: number | null; // 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록 }; export type SettingInput = { targetMargin: string; anchoringValue: string; + internetFee: string; cardUseCount: string; }; @@ -116,13 +119,14 @@ export function useQuotations(params: ListQuotationsParams) { const addSetting = (input: SettingInput): boolean => { const marginPct = Number(String(input.targetMargin).replace('%', '').trim()); const anchoring = Number(String(input.anchoringValue).trim()); + const feePct = Number(String(input.internetFee).replace('%', '').trim()); const cardCount = parseInt(String(input.cardUseCount).replace(/[^0-9-]/g, ''), 10); - if (!Number.isFinite(marginPct) || !Number.isFinite(anchoring) || !Number.isInteger(cardCount)) { - showToast('목표 마진율·앵커링 값·카드 사용 횟수를 숫자로 입력해야 합니다.', 'error'); + if (!Number.isFinite(marginPct) || !Number.isFinite(anchoring) || !Number.isFinite(feePct) || !Number.isInteger(cardCount)) { + showToast('목표 마진율·앵커링 값·수수료율·카드 사용 횟수를 숫자로 입력해야 합니다.', 'error'); return false; } createSettingMutation.mutate( - { data: { target_margin_rate: marginPct / 100, anchoring_value: anchoring, card_count: cardCount } }, + { data: { target_margin_rate: marginPct / 100, anchoring_value: anchoring, internet_average_fee: feePct / 100, card_count: cardCount } }, { onSuccess: () => { invalidateSettings(); @@ -183,6 +187,8 @@ export function useQuotations(params: ListQuotationsParams) { manager_email: me?.email || undefined, manager_contact_number: me?.contact || undefined, memo: input.memo.trim() || undefined, + md_price: input.mdPrice && input.mdPrice > 0 ? input.mdPrice : undefined, + supplier_type: input.supplierType ?? undefined, }; try { diff --git a/negodata/front/src/features/quotations/types.ts b/negodata/front/src/features/quotations/types.ts index a5970d7..477274f 100644 --- a/negodata/front/src/features/quotations/types.ts +++ b/negodata/front/src/features/quotations/types.ts @@ -4,7 +4,7 @@ import type { QuotationSettingData } from '@/api/generated/model/quotationSettin import type { QuotationData } from '@/api/generated/model/quotationData'; import type { SessionData } from '@/api/generated/model/sessionData'; import type { QuotationCardData } from '@/api/generated/model/quotationCardData'; -import { QuotationType, QuotationStatus, SessionStatus, CardType } from '@/api/generated/model'; +import { QuotationType, QuotationStatus, SessionStatus, CardType, SupplierType } from '@/api/generated/model'; import { DELIVERY_TYPE_LABEL } from '@/lib/enumLabels'; import { toMinPrice } from '@/features/products/types'; import type { Product, Partner, NegotiationCard } from '@/types'; @@ -136,14 +136,29 @@ export const QUOTATION_STATUS_OPTIONS = Object.values(QuotationStatus).map((valu export const QUOTATION_TYPE_LABEL: Record = { [QuotationType.RENEGO]: '재협상', [QuotationType.REQUOTE]: '재견적', + [QuotationType.NEW_NEGO]: '신규협상', + [QuotationType.NEW_QUOTE]: '신규견적', }; export const quotationTypeLabel = (t?: number | null): string => t != null ? QUOTATION_TYPE_LABEL[t as QuotationType] ?? String(t) : ''; -export const QUOTATION_TYPE_OPTIONS = [QuotationType.REQUOTE, QuotationType.RENEGO].map((value) => ({ +export const QUOTATION_TYPE_OPTIONS = [ + QuotationType.NEW_QUOTE, + QuotationType.NEW_NEGO, + QuotationType.REQUOTE, + QuotationType.RENEGO, +].map((value) => ({ value, label: QUOTATION_TYPE_LABEL[value], })); +// 협력사 유형 선택지(견적생성 모달). 재견적은 1:1이라 견적에 협력사 유형을 박는다. 없음(ETC=0) 포함. +export const supplierTypeOptions: { value: number; label: string }[] = [ + { value: SupplierType.DISTRIBUTION, label: '유통' }, + { value: SupplierType.MANUFACTURE, label: '제조' }, + { value: SupplierType.SOLE_AGENCY, label: '총판' }, + { value: SupplierType.ETC, label: '없음' }, +]; + // ── 라운드 체인(같은 견적번호) ─────────────────────────────────────────── // 한 라운드(견적)의 결과를 한 단어로. 낙찰=종료, 동가/마감=후속 라운드 가능, 진행중=아직 안 닫힘. export type ChainRoundState = 'awarded' | 'equal' | 'closed' | 'active'; diff --git a/negodata/front/src/types.ts b/negodata/front/src/types.ts index 6693cd6..53089d5 100644 --- a/negodata/front/src/types.ts +++ b/negodata/front/src/types.ts @@ -23,6 +23,7 @@ export type Partner = SupplierData & { export interface NegotiationCard { id: string; isWildcard: boolean; + usageType: number; // usage_type(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 code: string; title: string; scriptPreview: string; diff --git a/postgres-init/01-schema.sql b/postgres-init/01-schema.sql index bb7129f..b25ebd3 100644 --- a/postgres-init/01-schema.sql +++ b/postgres-init/01-schema.sql @@ -156,6 +156,9 @@ CREATE TABLE IF NOT EXISTS partner.items ( vat_yn BOOLEAN NULL, -- 부가세 포함 여부 delivery_fee_yn BOOLEAN NULL, -- 배송비 포함 여부 internet_lowest_price_yn BOOLEAN NOT NULL DEFAULT FALSE, -- 최저가 솔루션의 원자성을 보존하기 위한 보조 컬럼 + internet_lowest_price BIGINT NULL, -- 인터넷 최저가 + purchase_price BIGINT NULL, -- 매입가 + selling_price BIGINT NULL, -- 판매가 category_type INTEGER NOT NULL DEFAULT 1, -- 자동으로 늘어나는 숫자 ( 카테고리 찾을때 유용한 컬럼) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) @@ -197,6 +200,7 @@ CREATE TABLE IF NOT EXISTS card.nego_cards ( number VARCHAR(10) NULL, -- 식별번호 script VARCHAR(255) NULL, -- 협상 스크립트 edit_script JSONB NULL, -- 편집된 스크립트(JSON) + usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 @@ -209,6 +213,7 @@ CREATE TABLE IF NOT EXISTS card.wild_cards ( number VARCHAR(10) NULL, -- 식별번호 script VARCHAR(255) NULL, -- 협상 스크립트 edit_script JSONB NULL, -- 편집된 스크립트(JSON) + usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 condition VARCHAR(255) NULL, -- 커스터마이징 협상 카드이기 때문에 상세 조건을 기재해야 함 available BOOLEAN NOT NULL DEFAULT FALSE, -- 와일드 카드는 수동으로 코드에 추가해야 하기 때문에 컬럼 추가 memo VARCHAR(255) NULL, -- 사용 조건 이외에 자유롭게 적을 수 있는 메모 @@ -243,6 +248,7 @@ CREATE TABLE IF NOT EXISTS quotation.quotation_settings ( user_id uuid NOT NULL, -- 견적 설정을 생성한 유저 아이디(company.users.user_id) target_margin_rate NUMERIC(8,6) NOT NULL, -- 목표 마진율 (정수부 2자리 + 소수 6자리, -99.999999~99.999999) anchoring_value NUMERIC(8,6) NOT NULL DEFAULT 0.01, -- 앵커링 값 (정수부 2자리 + 소수 6자리) + internet_average_fee NUMERIC(8,6) NOT NULL DEFAULT 0.078, -- 인터넷 평균 수수료율 card_count INTEGER NOT NULL DEFAULT 3, -- 한개의 협상 안에서 협상카드 사용 횟수 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) @@ -265,6 +271,8 @@ CREATE TABLE IF NOT EXISTS quotation.quotations ( manager_email VARCHAR(255) NULL, -- 담당자 이메일 manager_contact_number VARCHAR(20) NULL, -- 담당자 연락처 memo VARCHAR(100) NULL, -- 메모 + md_price BIGINT NULL, -- MD 제시가(원). 목표가 산정 최우선값 (견적생성 모달 입력) + supplier_type SMALLINT NULL, -- 협력사 유형(SupplierType). 재견적 1:1 → 견적에 기록 (견적생성 모달 입력) iteration INTEGER NOT NULL DEFAULT 0, -- 반복 횟수 preferred_sp_yn BOOLEAN NULL, -- 선호 공급사 지정 여부 preferred_sp_id uuid NULL, -- 선호 공급사(partner.suppliers.supplier_id) @@ -288,6 +296,7 @@ CREATE TABLE IF NOT EXISTS negotiation.sessions ( qt_round INTEGER NOT NULL, -- 견적 라운드(스냅샷) qt_type SMALLINT NOT NULL, -- 견적 유형(스냅샷): 1=renego, 2=requote target_price BIGINT NOT NULL, -- 목표가(원) + target_anchoring_price BIGINT NULL, -- 앵커링가(원) status SMALLINT NOT NULL, -- 진행 상태 (코드, 앱 enum 매핑) bid_price BIGINT NULL, -- 입찰가(원) bid_at TIMESTAMPTZ NULL, -- 입찰 시각 diff --git a/postgres-init/04-alter-negodata-pricing.sql b/postgres-init/04-alter-negodata-pricing.sql new file mode 100644 index 0000000..c3a6c19 --- /dev/null +++ b/postgres-init/04-alter-negodata-pricing.sql @@ -0,0 +1,27 @@ +-- negosium 견적 개편: 가격(매입/판매)·수수료율·앵커링가 + 견적/카드/협력사 분류 컬럼 +-- 적용 대상: 이미 돌아가는 DB (신규/리셋 DB는 postgres-init/01-schema.sql 에 이미 포함). + +-- 상품: 인터넷최저가 실값 + 매입가 + 판매가 +ALTER TABLE partner.items + ADD COLUMN IF NOT EXISTS internet_lowest_price BIGINT, + ADD COLUMN IF NOT EXISTS purchase_price BIGINT, + ADD COLUMN IF NOT EXISTS selling_price BIGINT; + +-- 견적 설정: 인터넷 평균 수수료율 +ALTER TABLE quotation.quotation_settings + ADD COLUMN IF NOT EXISTS internet_average_fee NUMERIC(8,6) NOT NULL DEFAULT 0.078; + +-- 세션: 앵커링가 +ALTER TABLE negotiation.sessions + ADD COLUMN IF NOT EXISTS target_anchoring_price BIGINT; + +-- 견적: MD 제시가 + 협력사(공급채널) 유형 +ALTER TABLE quotation.quotations + ADD COLUMN IF NOT EXISTS md_price BIGINT, + ADD COLUMN IF NOT EXISTS supplier_type SMALLINT; + +-- 카드: 사용 범위 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용 +ALTER TABLE card.nego_cards + ADD COLUMN IF NOT EXISTS usage_type SMALLINT NOT NULL DEFAULT 1; +ALTER TABLE card.wild_cards + ADD COLUMN IF NOT EXISTS usage_type SMALLINT NOT NULL DEFAULT 1;