From 33ce82997aee2c0080ad0a2d8ad51894aff0e216 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Tue, 30 Jun 2026 08:40:30 +0900 Subject: [PATCH] =?UTF-8?q?[refactor]=20negodata:=20=EB=AA=A9=ED=91=9C?= =?UTF-8?q?=EA=B0=80=20=EC=82=B0=EC=A0=95=EB=82=B4=EC=97=AD=EC=9D=84=20?= =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20/target-breakdown=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생성(_calc_target_price)과 표시(get_target_breakdown)가 _candidates 헬퍼를 공유 → 프론트 재계산 제거, 저장된 목표가와 항상 일치 - TargetPriceModal: useGetTargetBreakdown 으로 후보·채택·앵커링가 '표시만' - QuotationType.is_new() 한 곳에서 신규유형 판단(생성·산정 공통) - enum 라벨 단일출처(lib/enumLabels.ts): 협력사유형/카드 사용구분/유저상태 members·CardFormSheet 는 거기서 re-export Co-Authored-By: Claude Opus 4.8 (1M context) --- negodata/backend/common/enums.py | 5 + .../backend/router/v1/quotation/protocol.py | 22 ++ .../backend/router/v1/quotation/quotation.py | 6 + .../backend/services/quotation_service.py | 109 ++++++++-- .../front/src/api/generated/model/index.ts | 9 + .../api/generated/model/resTargetBreakdown.ts | 33 +++ .../model/resTargetBreakdownChosenBasis.ts | 8 + .../model/resTargetBreakdownInternetLowest.ts | 8 + .../model/resTargetBreakdownMdPrice.ts | 8 + .../generated/model/resTargetBreakdownMsg.ts | 8 + .../model/resTargetBreakdownPurchase.ts | 8 + .../model/resTargetBreakdownSelling.ts | 8 + .../resTargetBreakdownTargetAnchoringPrice.ts | 8 + .../api/generated/model/targetCandidate.ts | 12 ++ .../src/api/generated/quotation/quotation.ts | 95 ++++++++- .../cards/components/CardFormSheet.tsx | 14 +- negodata/front/src/features/members/types.ts | 14 +- .../components/QuotationCreateModal.tsx | 11 +- .../QuotationDetailSheet/TargetPriceModal.tsx | 194 ++++++++---------- .../components/QuotationDetailSheet/index.tsx | 18 +- .../front/src/features/quotations/types.ts | 14 +- negodata/front/src/lib/enumLabels.ts | 34 ++- 22 files changed, 471 insertions(+), 175 deletions(-) create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdown.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownChosenBasis.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownInternetLowest.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownMdPrice.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownMsg.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownPurchase.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownSelling.ts create mode 100644 negodata/front/src/api/generated/model/resTargetBreakdownTargetAnchoringPrice.ts create mode 100644 negodata/front/src/api/generated/model/targetCandidate.ts diff --git a/negodata/backend/common/enums.py b/negodata/backend/common/enums.py index f040de9..4d03152 100644 --- a/negodata/backend/common/enums.py +++ b/negodata/backend/common/enums.py @@ -138,6 +138,11 @@ class QuotationType(CodeEnum): NEW_NEGO = 3 # 신규협상(1:1) NEW_QUOTE = 4 # 신규견적(1:N) + @classmethod + def is_new(cls, code) -> bool: + """신규(NEW_*) 견적유형이면 True. 목표가 후보(신규=인터넷최저가만)가 이 분기에 의존하므로 한 곳에서만 판단한다.""" + return code in (cls.NEW_NEGO.value, cls.NEW_QUOTE.value) + class QuotationStatus(CodeEnum): """quotations.status 코드값(SMALLINT). 프론트 견적상태 뱃지와 매핑된다.""" diff --git a/negodata/backend/router/v1/quotation/protocol.py b/negodata/backend/router/v1/quotation/protocol.py index dc150e0..a2cf80f 100644 --- a/negodata/backend/router/v1/quotation/protocol.py +++ b/negodata/backend/router/v1/quotation/protocol.py @@ -180,3 +180,25 @@ class Res_QuotationCards(Res_WebPacketProtocol): class Res_LastSupplierType(Res_WebPacketProtocol): supplier_type: Optional[SupplierType] = None # 협력사 직전 견적의 유형(없으면 None) qt_number: Optional[str] = None # 그 견적의 번호(이전 견적 값임을 표시용) + + +class TargetCandidate(WebPacketProtocol): + basis: str + label: str + value: int + + +class Res_TargetBreakdown(Res_WebPacketProtocol): + is_new: bool = False + is_inherited: bool = False + md_price: Optional[int] = None + internet_lowest: Optional[int] = None + purchase: Optional[int] = None + selling: Optional[int] = None + fee: float = 0.0 + margin: float = 0.0 + anchoring_value: float = 0.0 + candidates: list[TargetCandidate] = [] + chosen_basis: Optional[str] = None + target_price: int = 0 + target_anchoring_price: Optional[int] = None diff --git a/negodata/backend/router/v1/quotation/quotation.py b/negodata/backend/router/v1/quotation/quotation.py index fa97eed..d01ba25 100644 --- a/negodata/backend/router/v1/quotation/quotation.py +++ b/negodata/backend/router/v1/quotation/quotation.py @@ -20,6 +20,7 @@ from .protocol import ( Res_QuotationSessions, Res_QuotationStatus, Res_SessionChat, + Res_TargetBreakdown, ) # 라우터(컨트롤러). 인증(Depends(IsValidAccessToken))으로 UserInfo 를 받는다. @@ -84,6 +85,11 @@ async def get_session_chat(session_id: UUID, service: QuotationService = Depends return RemoveNoneResponse(await service.list_chats(str(session_id))) +@router.get(path="/session/{session_id}/target-breakdown", response_model=Res_TargetBreakdown, summary="세션 목표가 산정내역") +async def get_target_breakdown(session_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): + return RemoveNoneResponse(await service.get_target_breakdown(str(session_id))) + + @router.post(path="/session/{session_id}/notify", response_model=Res_NotifySessions, summary="세션 초청 메일 재발송") async def notify_session(session_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)): return RemoveNoneResponse(await service.notify_session(str(session_id))) diff --git a/negodata/backend/services/quotation_service.py b/negodata/backend/services/quotation_service.py index edff67a..f31b820 100644 --- a/negodata/backend/services/quotation_service.py +++ b/negodata/backend/services/quotation_service.py @@ -30,6 +30,8 @@ from router.v1.quotation.protocol import ( Res_QuotationSessions, Res_QuotationStatus, Res_SessionChat, + Res_TargetBreakdown, + TargetCandidate, ) from services.email import EmailUnavailable, build_invite_email, send_email @@ -56,6 +58,9 @@ class QuotationService: # 인터넷 평균 수수료율(상수). 시장 평균값이라 견적/세팅별로 두지 않고 고정. 목표가=인터넷최저가×(1−값). INTERNET_AVERAGE_FEE = 0.078 + # 목표가 후보 basis 코드 ↔ 표시 라벨(산정내역 응답에서 프론트가 그대로 표기). + _CANDIDATE_LABELS = {"md": "MD 입력가", "internet": "인터넷 최저가", "purchase": "매입가", "selling": "판매가"} + def __init__(self, quotation_crud: IQuotationCRUD = Depends(QuotationCRUD)): self.quotation_crud = quotation_crud @@ -65,6 +70,23 @@ class QuotationService: base = (web_server_config.nego_chat_url or "").rstrip("/") return f"{base}/chat?session_id={session_id}" + @staticmethod + def _candidates(md_price=None, internet_lowest=None, purchase=None, selling=None, fee=0.0, margin=0.0, is_new=False): + """목표가 후보 [(basis, value_float)] 목록(빈 값/0 은 제외). md 있으면 md 단독. + 값은 float(인터넷=가격×(1−수수료), 판매가=가격×(1−마진))이며 채택 시 int() 절삭한다. + _calc_target_price(생성)와 get_target_breakdown(표시)가 공유하는 단일 산정 로직.""" + if md_price: + return [("md", float(int(md_price)))] + out = [] + if internet_lowest: + out.append(("internet", int(internet_lowest) * (1 - (fee or 0.0)))) + if not is_new: # 재(협상·견적)만 매입가·판매가를 후보에 추가. 신규는 인터넷최저가만. + if purchase: + out.append(("purchase", float(int(purchase)))) + if selling: + out.append(("selling", int(selling) * (1 - (margin or 0.0)))) + return out + @staticmethod 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 적응) @@ -76,24 +98,16 @@ class QuotationService: - 매입가 (그대로) - 판매가 × (1 − margin) ← margin=quotation_settings.target_margin_rate ③ 후보 0개 → 견적 생성 불가(ValueError).""" - if md_price: - return int(md_price) - # 율은 비율(0~1 미만)이어야 한다. 1 이상이면 (1−율)≤0 → 목표가가 0/음수가 되므로 설정 오류로 막는다. - if not 0.0 <= (fee or 0.0) < 1.0: - raise ValueError(f"인터넷 수수료율은 0 이상 1 미만이어야 합니다: fee={fee}") - if not is_new and not 0.0 <= (margin or 0.0) < 1.0: - raise ValueError(f"목표 마진율은 0 이상 1 미만이어야 합니다: margin={margin}") - 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: + if not md_price: + # 율은 비율(0~1 미만)이어야 한다. 1 이상이면 (1−율)≤0 → 목표가가 0/음수가 되므로 설정 오류로 막는다. + if not 0.0 <= (fee or 0.0) < 1.0: + raise ValueError(f"인터넷 수수료율은 0 이상 1 미만이어야 합니다: fee={fee}") + if not is_new and not 0.0 <= (margin or 0.0) < 1.0: + raise ValueError(f"목표 마진율은 0 이상 1 미만이어야 합니다: margin={margin}") + cands = QuotationService._candidates(md_price, internet_lowest, purchase, selling, fee, margin, is_new) + if not cands: raise ValueError("타겟 가격 계산 불가: md_price·인터넷최저가" + ("" if is_new else "·매입가·판매가") + " 모두 없음") - return int(min(candidates)) + return int(min(v for _, v in cands)) @staticmethod def _gen_number() -> str: @@ -112,6 +126,65 @@ class QuotationService: return ErrorType.QUOTATION_NOT_FOUND, None return ErrorType.SUCCESS, quotation + async def get_target_breakdown(self, session_id: str) -> Res_TargetBreakdown: + """세션 목표가 산정내역(후보·채택). 저장된 target_price/anchoring 은 그대로 표기하고, + 후보값은 생성과 동일한 _candidates 로직으로 계산해 내려준다(프론트 재계산 제거 → 항상 일치). + 상속분(재생성 라운드)은 현재 후보와 무관하므로 is_inherited=True, 채택 표시는 비운다.""" + res = Res_TargetBreakdown() + err_type, got = await DB_SESSION_MNG.execute_lambda( + sessions.DBType(), + DBWRType.DB_READ.value, + lambda s: self.quotation_crud.get_session_with_supplier(s, uuid.UUID(session_id)), + ) + if err_type != ErrorType.SUCCESS or got is None: + res.result.SetResult(err_type if err_type != ErrorType.SUCCESS else ErrorType.QUOTATION_NOT_FOUND) + return res + sess = got[0] + err_type, quotation = await self._fetch(sess.quotation_id) + if err_type != ErrorType.SUCCESS: + res.result.SetResult(err_type) + return res + + _e, prices = await DB_SESSION_MNG.execute_lambda( + quotations.DBType(), + DBWRType.DB_READ.value, + lambda s: self.quotation_crud.get_item_prices(s, [sess.item_id]), + ) + internet, purchase, selling = (prices or {}).get(sess.item_id) or (None, None, None) + _e, rates = await DB_SESSION_MNG.execute_lambda( + quotations.DBType(), + DBWRType.DB_READ.value, + lambda s: self.quotation_crud.get_setting_rates(s, quotation.qt_setting_id), + ) + rates = rates or {} + fee = self.INTERNET_AVERAGE_FEE + margin = rates.get("margin") or 0.0 + anchoring = rates.get("anchoring") or 0.0 + is_new = QuotationType.is_new(quotation.type) + md = quotation.md_price + + cands = self._candidates(md, internet, purchase, selling, fee, margin, is_new) + chosen_basis, computed = None, None + if cands: + chosen_basis, chosen_val = min(cands, key=lambda c: c[1]) + computed = int(chosen_val) + is_inherited = computed is None or computed != sess.target_price + + res.is_new = is_new + res.is_inherited = is_inherited + res.md_price = int(md) if md else None + res.internet_lowest = int(internet) if internet is not None else None + res.purchase = int(purchase) if purchase is not None else None + res.selling = int(selling) if selling is not None else None + res.fee = fee + res.margin = margin + res.anchoring_value = anchoring + res.candidates = [TargetCandidate(basis=b, label=self._CANDIDATE_LABELS.get(b, b), value=int(v)) for b, v in cands] + res.chosen_basis = None if is_inherited else chosen_basis + res.target_price = sess.target_price + res.target_anchoring_price = sess.target_anchoring_price + return res + async def list_quotations(self, search, status, type_, start_from, start_to, pg: PageParams) -> Res_QuotationList: res = Res_QuotationList(page=pg.page, size=pg.size) @@ -353,7 +426,7 @@ class QuotationService: # 상품 × 공급사 조합마다 세션 1개. md/매입/판매/인터넷 후보가 하나도 없으면 목표가 산정 불가 → 생성 실패. # 신규(NEW_NEGO/NEW_QUOTE)는 인터넷최저가만, 재(RENEGO/REQUOTE)는 매입가·판매가까지 후보(KTC 신규/재 분리). - is_new = type_ in (QuotationType.NEW_NEGO.value, QuotationType.NEW_QUOTE.value) + is_new = QuotationType.is_new(type_) session_objs = [] try: for iid in item_ids: diff --git a/negodata/front/src/api/generated/model/index.ts b/negodata/front/src/api/generated/model/index.ts index d0b2814..75dc364 100644 --- a/negodata/front/src/api/generated/model/index.ts +++ b/negodata/front/src/api/generated/model/index.ts @@ -290,6 +290,14 @@ export * from './resSupplierList'; export * from './resSupplierListMsg'; export * from './resSupplierMsg'; export * from './resSupplierSupplier'; +export * from './resTargetBreakdown'; +export * from './resTargetBreakdownChosenBasis'; +export * from './resTargetBreakdownInternetLowest'; +export * from './resTargetBreakdownMdPrice'; +export * from './resTargetBreakdownMsg'; +export * from './resTargetBreakdownPurchase'; +export * from './resTargetBreakdownSelling'; +export * from './resTargetBreakdownTargetAnchoringPrice'; export * from './sessionData'; export * from './sessionDataBidAt'; export * from './sessionDataBidPrice'; @@ -308,6 +316,7 @@ export * from './supplierDataManagerName'; export * from './supplierDataPriority'; export * from './supplierDataUpdatedAt'; export * from './supplierType'; +export * from './targetCandidate'; export * from './userRole'; export * from './userStatus'; export * from './validationError'; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdown.ts b/negodata/front/src/api/generated/model/resTargetBreakdown.ts new file mode 100644 index 0000000..4697eed --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdown.ts @@ -0,0 +1,33 @@ +/** + * 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 { ResTargetBreakdownMsg } from './resTargetBreakdownMsg'; +import type { ResTargetBreakdownMdPrice } from './resTargetBreakdownMdPrice'; +import type { ResTargetBreakdownInternetLowest } from './resTargetBreakdownInternetLowest'; +import type { ResTargetBreakdownPurchase } from './resTargetBreakdownPurchase'; +import type { ResTargetBreakdownSelling } from './resTargetBreakdownSelling'; +import type { TargetCandidate } from './targetCandidate'; +import type { ResTargetBreakdownChosenBasis } from './resTargetBreakdownChosenBasis'; +import type { ResTargetBreakdownTargetAnchoringPrice } from './resTargetBreakdownTargetAnchoringPrice'; + +export interface ResTargetBreakdown { + result?: ErrorInfo; + msg?: ResTargetBreakdownMsg; + is_new?: boolean; + is_inherited?: boolean; + md_price?: ResTargetBreakdownMdPrice; + internet_lowest?: ResTargetBreakdownInternetLowest; + purchase?: ResTargetBreakdownPurchase; + selling?: ResTargetBreakdownSelling; + fee?: number; + margin?: number; + anchoring_value?: number; + candidates?: TargetCandidate[]; + chosen_basis?: ResTargetBreakdownChosenBasis; + target_price?: number; + target_anchoring_price?: ResTargetBreakdownTargetAnchoringPrice; +} diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownChosenBasis.ts b/negodata/front/src/api/generated/model/resTargetBreakdownChosenBasis.ts new file mode 100644 index 0000000..b6ace49 --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownChosenBasis.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 ResTargetBreakdownChosenBasis = string | null; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownInternetLowest.ts b/negodata/front/src/api/generated/model/resTargetBreakdownInternetLowest.ts new file mode 100644 index 0000000..84cd371 --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownInternetLowest.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 ResTargetBreakdownInternetLowest = number | null; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownMdPrice.ts b/negodata/front/src/api/generated/model/resTargetBreakdownMdPrice.ts new file mode 100644 index 0000000..c5c7008 --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownMdPrice.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 ResTargetBreakdownMdPrice = number | null; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownMsg.ts b/negodata/front/src/api/generated/model/resTargetBreakdownMsg.ts new file mode 100644 index 0000000..0ee0e6f --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownMsg.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 ResTargetBreakdownMsg = string | null; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownPurchase.ts b/negodata/front/src/api/generated/model/resTargetBreakdownPurchase.ts new file mode 100644 index 0000000..4aba8a4 --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownPurchase.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 ResTargetBreakdownPurchase = number | null; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownSelling.ts b/negodata/front/src/api/generated/model/resTargetBreakdownSelling.ts new file mode 100644 index 0000000..a8307f3 --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownSelling.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 ResTargetBreakdownSelling = number | null; diff --git a/negodata/front/src/api/generated/model/resTargetBreakdownTargetAnchoringPrice.ts b/negodata/front/src/api/generated/model/resTargetBreakdownTargetAnchoringPrice.ts new file mode 100644 index 0000000..34ce92e --- /dev/null +++ b/negodata/front/src/api/generated/model/resTargetBreakdownTargetAnchoringPrice.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 ResTargetBreakdownTargetAnchoringPrice = number | null; diff --git a/negodata/front/src/api/generated/model/targetCandidate.ts b/negodata/front/src/api/generated/model/targetCandidate.ts new file mode 100644 index 0000000..e3c30f3 --- /dev/null +++ b/negodata/front/src/api/generated/model/targetCandidate.ts @@ -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 TargetCandidate { + basis: string; + label: string; + value: number; +} diff --git a/negodata/front/src/api/generated/quotation/quotation.ts b/negodata/front/src/api/generated/quotation/quotation.ts index b68178e..a0a91a2 100644 --- a/negodata/front/src/api/generated/quotation/quotation.ts +++ b/negodata/front/src/api/generated/quotation/quotation.ts @@ -38,7 +38,8 @@ import type { ResQuotationResult, ResQuotationSessions, ResQuotationStatus, - ResSessionChat + ResSessionChat, + ResTargetBreakdown } from '.././model'; import { customFetch } from '../../mutator/custom-fetch'; @@ -670,6 +671,98 @@ export function useGetSessionChat,signal?: AbortSignal +) => { + + + return customFetch( + {url: `/v1/quotation/session/${sessionId}/target-breakdown`, method: 'GET', signal + }, + options); + } + + + + +export const getGetTargetBreakdownQueryKey = (sessionId?: string,) => { + return [ + `/v1/quotation/session/${sessionId}/target-breakdown` + ] as const; + } + + +export const getGetTargetBreakdownQueryOptions = >, TError = void | HTTPValidationError>(sessionId: string, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetTargetBreakdownQueryKey(sessionId); + + + + const queryFn: QueryFunction>> = ({ signal }) => getTargetBreakdown(sessionId, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(sessionId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetTargetBreakdownQueryResult = NonNullable>> +export type GetTargetBreakdownQueryError = void | HTTPValidationError + + +export function useGetTargetBreakdown>, TError = void | HTTPValidationError>( + sessionId: string, options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetTargetBreakdown>, TError = void | HTTPValidationError>( + sessionId: string, options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetTargetBreakdown>, TError = void | HTTPValidationError>( + sessionId: string, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary 세션 목표가 산정내역 + */ + +export function useGetTargetBreakdown>, TError = void | HTTPValidationError>( + sessionId: string, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetTargetBreakdownQueryOptions(sessionId,options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + /** * @summary 세션 초청 메일 재발송 */ diff --git a/negodata/front/src/features/cards/components/CardFormSheet.tsx b/negodata/front/src/features/cards/components/CardFormSheet.tsx index e75a0cd..30beb52 100644 --- a/negodata/front/src/features/cards/components/CardFormSheet.tsx +++ b/negodata/front/src/features/cards/components/CardFormSheet.tsx @@ -10,6 +10,7 @@ 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 { CARD_USAGE_TYPE_LABEL, CARD_USAGE_TYPE_OPTIONS } from '@/lib/enumLabels'; import type { CardInput } from '../hooks/useCards'; import { CardScriptEditor, deserialize, serializeToText } from '../editor'; @@ -235,18 +236,13 @@ export function CardFormSheet({ )} diff --git a/negodata/front/src/features/members/types.ts b/negodata/front/src/features/members/types.ts index e668efe..d7cf60a 100644 --- a/negodata/front/src/features/members/types.ts +++ b/negodata/front/src/features/members/types.ts @@ -1,14 +1,8 @@ -import { UserRole } from '@/api/generated/model'; +import { UserRole, UserStatus } from '@/api/generated/model'; -// 회사 유저(계정) 상태 코드 — 백엔드 UserStatus 미러. -// (orval 재생성 전까지 로컬 정의. 재생성 후 @/api/generated/model 의 UserStatus 로 교체 가능) -export const UserStatus = { ACTIVE: 1, INACTIVE: 2 } as const; -export type UserStatus = (typeof UserStatus)[keyof typeof UserStatus]; - -export const USER_STATUS_LABEL: Record = { - [UserStatus.ACTIVE]: '활성', - [UserStatus.INACTIVE]: '비활성', -}; +// 유저 상태 코드/라벨은 생성 enum + 중앙 라벨(lib/enumLabels.ts) 단일 출처를 그대로 재노출한다. +export { UserStatus }; +export { USER_STATUS_LABEL } from '@/lib/enumLabels'; // 백엔드 CompanyUserData 와 1:1. export interface CompanyUserData { diff --git a/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx b/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx index be92979..05359ce 100644 --- a/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx +++ b/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx @@ -10,7 +10,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, supplierTypeOptions } from '../types'; +import { QUOTATION_TYPE_OPTIONS, supplierTypeOptions, isNewQuotationType } from '../types'; +import { supplierTypeLabel } from '@/lib/enumLabels'; import { showToast } from '@/lib/notify'; // datetime-local 디폴트값: 현재 한국시간(Asia/Seoul)의 'YYYY-MM-DDTHH:mm'. @@ -68,7 +69,7 @@ export function QuotationCreateModal({ const navigate = useNavigate(); // 인터넷최저가·매입가·판매가는 상품 속성 — 모달에선 읽기전용으로만 보여주고, 수정은 상품 상세에서 한다. const selectedProduct = products.find((p) => p.id === productId); - const isReType = type === QuotationType.RENEGO || type === QuotationType.REQUOTE; + const isReType = !isNewQuotationType(type); const internetLowest = selectedProduct?.internet_lowest_price ?? null; const purchase = selectedProduct?.purchase_price ?? null; const selling = selectedProduct?.selling_price ?? null; @@ -329,11 +330,7 @@ export function QuotationCreateModal({