- 협상대화: 사용 카드 번호/멘트(Slate 서식본)/와일드 조건·메모 표시, SlateRenderer
밑줄·헥스색·{변수} 치환 보정, 말풍선 우측정렬·발신자 아바타 제거
- 견적상세 헤더: DB 컬럼명 라벨 제거, 견적정보/(진행상태+세팅)/상품(이미지) 2열 재배치,
협상카드 탭에 사용 카드 수 표시
- DataTable 행 패딩 축소, 협력사명 아바타 제거, 카드관리 본인 카드 안내문
- orval 재생성(QuotationCardData 신규 필드)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|