o2o-negosium-original/negodata/front/src/lib/confirm.tsx
Mina Choi 7186446068 [feat] negodata/front: 협상대화 카드내용 렌더 + 견적상세 헤더·테이블 UI 정리
- 협상대화: 사용 카드 번호/멘트(Slate 서식본)/와일드 조건·메모 표시, SlateRenderer
  밑줄·헥스색·{변수} 치환 보정, 말풍선 우측정렬·발신자 아바타 제거
- 견적상세 헤더: DB 컬럼명 라벨 제거, 견적정보/(진행상태+세팅)/상품(이미지) 2열 재배치,
  협상카드 탭에 사용 카드 수 표시
- DataTable 행 패딩 축소, 협력사명 아바타 제거, 카드관리 본인 카드 안내문
- orval 재생성(QuotationCardData 신규 필드)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:21:58 +09:00

72 lines
2.9 KiB
TypeScript

import { create } from 'zustand';
import { AlertDialog as AD } from '@base-ui/react/alert-dialog';
import { Button } from '@/components/ui/button';
// 전역 확인 다이얼로그. 네이티브 window.confirm 대신 shadcn 풍 AlertDialog(base-ui)로 띄운다.
// showToast 처럼 명령형으로 어디서든(컴포넌트/훅) 호출하고 Promise<boolean> 을 받는다.
// if (await confirm({ title: '삭제하시겠습니까?', destructive: true })) { ... }
export type ConfirmOptions = {
title: string;
description?: string;
confirmText?: string; // 기본 '확인'
cancelText?: string; // 기본 '취소'
destructive?: boolean; // true 면 확인 버튼 빨강
};
type ConfirmStore = {
open: boolean;
options: ConfirmOptions | null;
resolve: ((ok: boolean) => void) | null;
request: (options: ConfirmOptions) => Promise<boolean>;
settle: (ok: boolean) => void;
};
const useConfirmStore = create<ConfirmStore>((set, get) => ({
open: false,
options: null,
resolve: null,
request: (options) =>
new Promise<boolean>((resolve) => set({ open: true, options, resolve })),
settle: (ok) => {
get().resolve?.(ok);
set({ open: false, resolve: null });
},
}));
export function confirm(options: ConfirmOptions): Promise<boolean> {
return useConfirmStore.getState().request(options);
}
// 앱 루트(provider)에 1회 마운트 — sonner <Toaster/> 와 같은 역할.
export function ConfirmHost() {
const { open, options, settle } = useConfirmStore();
return (
<AD.Root open={open} onOpenChange={(next) => { if (!next) settle(false); }}>
<AD.Portal>
<AD.Backdrop className="fixed inset-0 z-50 bg-black/30 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
<AD.Popup className="fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl bg-popover p-5 text-sm text-popover-foreground ring-1 ring-foreground/10 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95">
<AD.Title className="font-heading text-base font-semibold">{options?.title}</AD.Title>
{options?.description && (
<AD.Description className="text-sm text-muted-foreground whitespace-pre-line">
{options.description}
</AD.Description>
)}
<div className="mt-2 flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => settle(false)}>
{options?.cancelText ?? '취소'}
</Button>
<Button
variant={options?.destructive ? 'destructive' : 'default'}
size="sm"
onClick={() => settle(true)}
>
{options?.confirmText ?? '확인'}
</Button>
</div>
</AD.Popup>
</AD.Portal>
</AD.Root>
);
}