From bb576af346ee91843a918eb9da789976477d7327 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Wed, 22 Jul 2026 15:54:05 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20negodata/front:=20=ED=98=91=EC=83=81?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B6=80=EA=B0=80=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=ED=98=91=EC=83=81=ED=98=84=ED=99=A9=20=EB=AA=A8=EB=8B=AC=C2=B7?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=88=B4=ED=8C=81(base-ui)=C2=B7?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=ED=9A=8C=EC=82=AC=EB=AA=85=20=EB=AA=A8?= =?UTF-8?q?=EB=B0=94=EC=9D=BC=20=EC=A0=84=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../front/src/components/layout/Layout.tsx | 6 +- negodata/front/src/components/ui/tooltip.tsx | 40 ++++++ .../QuotationDetailSheet/ChatTab.tsx | 19 +-- .../SessionsStatusTab.tsx | 123 +++++++++++++----- .../front/src/features/quotations/types.ts | 2 + 5 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 negodata/front/src/components/ui/tooltip.tsx diff --git a/negodata/front/src/components/layout/Layout.tsx b/negodata/front/src/components/layout/Layout.tsx index 8d12d3c..858bcbe 100644 --- a/negodata/front/src/components/layout/Layout.tsx +++ b/negodata/front/src/components/layout/Layout.tsx @@ -502,16 +502,16 @@ function HeaderMeta({ user, today }: { user: SidebarUser; today: string }) { 기준일시: {today} -
-
+ {/* 회사명은 모바일에서만 — 데스크톱은 사이드바 브랜드에 이미 노출 */} +
회사: {user?.company} diff --git a/negodata/front/src/components/ui/tooltip.tsx b/negodata/front/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..69eaa2c --- /dev/null +++ b/negodata/front/src/components/ui/tooltip.tsx @@ -0,0 +1,40 @@ +import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip' +import { cn } from '@/lib/utils' + +// base-ui 기반 툴팁(프로젝트가 radix 아닌 @base-ui/react — tabs/select 와 동일 계열). +function Tooltip(props: TooltipPrimitive.Root.Props) { + return ( + + + + ) +} + +function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) { + return +} + +function TooltipContent({ + className, + sideOffset = 6, + children, + ...props +}: TooltipPrimitive.Popup.Props & { sideOffset?: number }) { + return ( + + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent } diff --git a/negodata/front/src/features/quotations/components/QuotationDetailSheet/ChatTab.tsx b/negodata/front/src/features/quotations/components/QuotationDetailSheet/ChatTab.tsx index 22c21e3..9ae0e92 100644 --- a/negodata/front/src/features/quotations/components/QuotationDetailSheet/ChatTab.tsx +++ b/negodata/front/src/features/quotations/components/QuotationDetailSheet/ChatTab.tsx @@ -80,10 +80,10 @@ export function ChatTab({ // 협상완료 부가정보(sessions.custom) — 공급사가 타결 후 입력. 라벨은 회사 설정(session_fields)에서. const { settings } = useCompanySettings(); const sessionFields = settings.session_fields ?? []; - const custom = (currentSession?.custom ?? {}) as Record; - const extraRows = sessionFields - .map((f) => ({ label: f.label, value: custom[f.key] })) - .filter((r) => r.value !== undefined && r.value !== null && r.value !== ''); + const extraRows = (sd: SessionData | null) => + sessionFields + .map((f) => ({ label: f.label, value: (sd?.custom as Record | undefined)?.[f.key] })) + .filter((r) => r.value !== undefined && r.value !== null && r.value !== ''); return (
{/* Sessions list */} @@ -102,7 +102,7 @@ export function ChatTab({ const name = partners.find((p) => p.id === sd.supplier_id)?.name || sd.supplier_id; const statusLabel = sessionStatusLabel(sd.status); return ( -
- +
); })}
@@ -155,11 +155,11 @@ export function ChatTab({
- {/* 협상완료 부가정보(공급사 입력) — 값이 있을 때만 노출 */} - {extraRows.length > 0 && ( + {/* 채팅에선 선택 세션의 부가정보를 밴드로 전부 노출. 리스트 버튼(모달)과 병행. */} + {extraRows(currentSession ?? null).length > 0 && (
협상완료 부가정보 - {extraRows.map((r) => ( + {extraRows(currentSession ?? null).map((r) => ( {r.label}: {String(r.value)} @@ -199,6 +199,7 @@ export function ChatTab({ )}
+ ); } diff --git a/negodata/front/src/features/quotations/components/QuotationDetailSheet/SessionsStatusTab.tsx b/negodata/front/src/features/quotations/components/QuotationDetailSheet/SessionsStatusTab.tsx index fcb0e42..34caeae 100644 --- a/negodata/front/src/features/quotations/components/QuotationDetailSheet/SessionsStatusTab.tsx +++ b/negodata/front/src/features/quotations/components/QuotationDetailSheet/SessionsStatusTab.tsx @@ -1,14 +1,16 @@ import { useState } from 'react'; import { Link } from 'react-router'; -import { MessageSquare, Copy, Mail, MailCheck, Send, Trophy } from 'lucide-react'; +import { MessageSquare, Copy, Mail, MailCheck, Send, Trophy, ClipboardList, X } from 'lucide-react'; import { showToast } from '@/lib/notify'; import { confirm } from '@/lib/confirm'; import { cn } from '@/lib/utils'; import { Typography, typographyVariants } from '@/components/ui/typography'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; import { SessionStatus } from '@/api/generated/model'; +import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; import { StatusPill, sessionStatusTone } from './StatusPill'; import { mapServerSessionView, sessionStatusLabel } from '../../types'; +import { useCompanySettings } from '@/features/settings/useCompanySettings'; type SessionView = ReturnType; @@ -37,6 +39,13 @@ export function SessionsStatusTab({ /** 고른 협력사를 낙찰 처리. 성공 시 true. */ onAward: (supplierId: string, supplierName: string) => Promise; }) { + const { settings } = useCompanySettings(); + const sessionFields = settings.session_fields ?? []; + const [extraSession, setExtraSession] = useState(null); + const extraRows = (sv: SessionView | null) => + sessionFields + .map((f) => ({ label: f.label, value: (sv?.custom as Record | undefined)?.[f.key] })) + .filter((r) => r.value !== undefined && r.value !== null && r.value !== ''); const [sendingAll, setSendingAll] = useState(false); const [sendingId, setSendingId] = useState(null); const [selectedWinnerId, setSelectedWinnerId] = useState(null); @@ -220,27 +229,48 @@ export function SessionsStatusTab({ > {sess.supplier_name} - + + onOpenChat(sess.session_id)} + className="p-1 hover:bg-primary/10 rounded text-primary hover:text-primary/80 transition-colors cursor-pointer shrink-0" + > + + + } /> + 협상 대화방으로 이동 + + {sess.status === SessionStatus.DONE && ( + + setExtraSession(sess)} + className="p-1 hover:bg-emerald-500/10 rounded text-emerald-600 hover:text-emerald-700 transition-colors cursor-pointer shrink-0" + > + + + } /> + 협상완료 부가정보 + + )} {sess.url ? ( - + + { + void navigator.clipboard?.writeText(sess.url); + showToast('협상 URL을 복사했습니다.', 'success'); + }} + className="p-1 hover:bg-muted rounded text-muted-foreground hover:text-foreground transition-colors cursor-pointer" + > + + + } /> + 협상 URL 복사 + ) : ( - )} @@ -257,20 +287,20 @@ export function SessionsStatusTab({ 미발송 )} - + + handleOne(sess.session_id, sess.supplier_name, !!sess.email_sent_at)} + disabled={!canNotify || sendingId === sess.session_id} + className="p-1 rounded text-primary hover:bg-primary/10 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed" + > + + + } /> + + {!canNotify ? '본인 견적만 발송 가능' : sess.email_sent_at ? '초청 메일 재발송' : '초청 메일 발송'} + + {sess.email_sent_at && ( {sess.email_sent_at} @@ -311,6 +341,35 @@ export function SessionsStatusTab({ + + {extraSession && ( +
setExtraSession(null)}> +
e.stopPropagation()}> +
+
+ + 협상완료 부가정보 +
+ +
+ {extraSession.supplier_name} + {extraRows(extraSession).length > 0 ? ( +
+ {extraRows(extraSession).map((r) => ( +
+ {r.label} + {String(r.value)} +
+ ))} +
+ ) : ( + 입력된 부가정보가 없습니다. + )} +
+
+ )} ); } diff --git a/negodata/front/src/features/quotations/types.ts b/negodata/front/src/features/quotations/types.ts index 31bbe0d..0ccdc01 100644 --- a/negodata/front/src/features/quotations/types.ts +++ b/negodata/front/src/features/quotations/types.ts @@ -250,6 +250,7 @@ export type SessionView = { end_time: string; url: string; // 세션 chat 실행 URL(공급사 협상 프론트) email_sent_at: string | null; // 초청 메일 발송 시각(KST). null=미발송 → 발송배지/재발송 판단 + custom: Record; // 협상완료 부가정보(sessions.custom) }; export type QuotationCardView = { @@ -389,6 +390,7 @@ export function mapServerSessionView(sd: SessionData, partners: Partner[], produ end_time: fmtDateTime(sd.end_time), url: sd.url || '', email_sent_at: sd.email_sent_at ? fmtDateTime(sd.email_sent_at) : null, + custom: (sd.custom ?? {}) as Record, }; }