Merge branch 'jskim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node

This commit is contained in:
kjs
2026-04-06 17:23:42 +09:00
4 changed files with 2538 additions and 474 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ function SortableMappingRow({ id, children }: { id: string; children: React.Reac
export default function CustomerManagementPage() {
const { user } = useAuth();
const { confirm, ConfirmDialogComponent } = useConfirmDialog();
const { confirm, ConfirmDialogComponent, isConfirmOpenRef } = useConfirmDialog();
const ts = useTableSettings("c16-customer", CUSTOMER_TABLE, CUSTOMER_GRID_COLUMNS);
const dndSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
@@ -253,9 +253,6 @@ export default function CustomerManagementPage() {
contact_person: mainContact?.contact_name || "",
contact_phone: mainContact?.contact_phone || "",
email: mainContact?.contact_email || "",
internal_manager: r.internal_manager
? (employeeOptions.find((e: any) => e.user_id === r.internal_manager)?.user_name || r.internal_manager)
: "",
};
});
// 거래처코드 숫자 기준 내림차순 정렬
@@ -455,9 +452,12 @@ export default function CustomerManagementPage() {
const handleModalContactSave = async () => {
if (!modalContactForm.contact_name) { toast.error("담당자명은 필수입니다."); return; }
if (modalContactEditId) {
// 수정 — 로컬 리스트에서 교체
// 수정 — 로컬 리스트에서 교체 + 메인 설정 시 다른 메인 해제
const isSettingMain = modalContactForm.is_main === "Y" || modalContactForm.is_main === true;
setModalContacts((prev) => prev.map((c) =>
c._localId === modalContactEditId ? { ...c, ...modalContactForm } : c
(c._localId || c.id) === modalContactEditId
? { ...c, ...modalContactForm }
: isSettingMain ? { ...c, is_main: "N" } : c
));
} else {
// 추가 — 로컬 리스트에 카드 추가
@@ -515,8 +515,11 @@ export default function CustomerManagementPage() {
const handleModalDeliverySave = async () => {
if (!modalDeliveryForm.destination_name) { toast.error("납품처명은 필수입니다."); return; }
if (modalDeliveryEditId) {
const isSettingMain = modalDeliveryForm.is_default === "Y" || modalDeliveryForm.is_default === true;
setModalDeliveries((prev) => prev.map((d) =>
(d._localId || d.id) === modalDeliveryEditId ? { ...d, ...modalDeliveryForm } : d
(d._localId || d.id) === modalDeliveryEditId
? { ...d, ...modalDeliveryForm }
: isSettingMain ? { ...d, is_default: "N" } : d
));
} else {
setModalDeliveries((prev) => [...prev, {
@@ -1389,7 +1392,7 @@ export default function CustomerManagementPage() {
<div className="flex items-center gap-1.5">
<label className="flex items-center gap-1.5 cursor-pointer mr-1">
<input type="checkbox" checked={showInactive} onChange={(e) => setShowInactive(e.target.checked)} className="rounded" />
<span className="text-[11px] text-muted-foreground"> </span>
<span className="text-[11px] text-muted-foreground"> </span>
</label>
<Button size="sm" onClick={openCustomerRegister}>
<Plus className="w-3.5 h-3.5 mr-1" />
@@ -1409,7 +1412,7 @@ export default function CustomerManagementPage() {
{/* 거래처 테이블 */}
<EDataTable
columns={customerColumns}
data={ts.groupData(showInactive ? customers : customers.filter((c) => c.status !== "비활성"))}
data={ts.groupData(showInactive ? customers : customers.filter((c) => c.status !== "거래정지"))}
rowKey={(row) => row.id}
loading={customerLoading}
emptyMessage="등록된 거래처가 없어요"
@@ -1715,6 +1718,7 @@ export default function CustomerManagementPage() {
{/* ── 모달: 거래처 등록/수정 (3탭) ── */}
<Dialog open={customerModalOpen} onOpenChange={(open) => {
if (!open && isConfirmOpenRef.current) return;
setCustomerModalOpen(open);
if (!open) {
setModalContactFormOpen(false);
@@ -1822,23 +1826,6 @@ export default function CustomerManagementPage() {
className="h-9"
/>
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Select
value={customerForm.internal_manager || "__none__"}
onValueChange={(v) => setCustomerForm((p) => ({ ...p, internal_manager: v === "__none__" ? "" : v }))}
>
<SelectTrigger className="h-9"><SelectValue placeholder="사내담당자 선택" /></SelectTrigger>
<SelectContent>
<SelectItem value="__none__"> </SelectItem>
{employeeOptions.map((emp) => (
<SelectItem key={emp.user_id} value={emp.user_id}>
{emp.user_name}{emp.position_name ? ` (${emp.position_name})` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Input
@@ -1974,7 +1961,7 @@ export default function CustomerManagementPage() {
const bMain = b.is_main === "Y" || b.is_main === true ? 0 : 1;
return aMain - bMain;
}).map((c) => (
<TableRow key={c.id} className="h-[41px]">
<TableRow key={c._localId || c.id} className="h-[41px]">
<TableCell className="text-sm font-medium">{c.contact_name}</TableCell>
<TableCell className="text-[13px]">{c.contact_phone}</TableCell>
<TableCell className="text-[13px] text-muted-foreground">{c.contact_email}</TableCell>
@@ -1987,12 +1974,24 @@ export default function CustomerManagementPage() {
? "bg-primary text-primary-foreground border-primary shadow-sm shadow-primary/30"
: "bg-transparent text-muted-foreground border-muted-foreground/20 hover:border-primary/50 hover:text-primary"
)}
onClick={() => {
setModalContacts((prev) => prev.map((item) =>
(item._localId || item.id) === (c._localId || c.id)
? { ...item, is_main: (item.is_main === "Y" || item.is_main === true) ? "N" : "Y" }
: item
));
onClick={async () => {
const isCurrentMain = c.is_main === "Y" || c.is_main === true;
if (isCurrentMain) {
setModalContacts((prev) => prev.map((item) =>
(item._localId || item.id) === (c._localId || c.id) ? { ...item, is_main: "N" } : item
));
} else {
const existingMain = modalContacts.find((x) => (x.is_main === "Y" || x.is_main === true) && (x._localId || x.id) !== (c._localId || c.id));
if (existingMain) {
const ok = await confirm(`현재 메인 담당자는 "${existingMain.contact_name}"입니다. 변경하시겠습니까?`);
if (!ok) return;
}
setModalContacts((prev) => prev.map((item) =>
(item._localId || item.id) === (c._localId || c.id)
? { ...item, is_main: "Y" }
: { ...item, is_main: "N" }
));
}
}}
>
{(c.is_main === "Y" || c.is_main === true) ? "★ 메인" : "메인"}
@@ -2084,7 +2083,20 @@ export default function CustomerManagementPage() {
<input
type="checkbox"
checked={modalContactForm.is_main === "Y" || modalContactForm.is_main === true}
onChange={(e) => setModalContactForm((p) => ({ ...p, is_main: e.target.checked ? "Y" : "N" }))}
onChange={async (e) => {
const checked = e.target.checked;
if (checked) {
const existingMain = modalContacts.find((x) => (x.is_main === "Y" || x.is_main === true) && (x._localId || x.id) !== modalContactEditId);
if (existingMain) {
const ok = await confirm(`현재 메인 담당자는 "${existingMain.contact_name}"입니다. 변경하시겠습니까?`);
if (!ok) return;
setModalContacts((prev) => prev.map((item) => ({ ...item, is_main: "N" })));
}
setModalContactForm((p) => ({ ...p, is_main: "Y" }));
} else {
setModalContactForm((p) => ({ ...p, is_main: "N" }));
}
}}
className="rounded"
/>
<span className="text-sm"> </span>
@@ -2172,12 +2184,24 @@ export default function CustomerManagementPage() {
? "bg-primary text-primary-foreground border-primary shadow-sm shadow-primary/30"
: "bg-transparent text-muted-foreground border-muted-foreground/20 hover:border-primary/50 hover:text-primary"
)}
onClick={() => {
setModalDeliveries((prev) => prev.map((item) =>
(item._localId || item.id) === (d._localId || d.id)
? { ...item, is_default: (item.is_default === "Y" || item.is_default === true) ? "N" : "Y" }
: item
));
onClick={async () => {
const isCurrentMain = d.is_default === "Y" || d.is_default === true;
if (isCurrentMain) {
setModalDeliveries((prev) => prev.map((item) =>
(item._localId || item.id) === (d._localId || d.id) ? { ...item, is_default: "N" } : item
));
} else {
const existingMain = modalDeliveries.find((x) => (x.is_default === "Y" || x.is_default === true) && (x._localId || x.id) !== (d._localId || d.id));
if (existingMain) {
const ok = await confirm(`현재 메인 납품처는 "${existingMain.destination_name}"입니다. 변경하시겠습니까?`);
if (!ok) return;
}
setModalDeliveries((prev) => prev.map((item) =>
(item._localId || item.id) === (d._localId || d.id)
? { ...item, is_default: "Y" }
: { ...item, is_default: "N" }
));
}
}}
>
{(d.is_default === "Y" || d.is_default === true) ? "★ 메인" : "메인"}
@@ -2286,7 +2310,20 @@ export default function CustomerManagementPage() {
<input
type="checkbox"
checked={modalDeliveryForm.is_default === "Y" || modalDeliveryForm.is_default === true}
onChange={(e) => setModalDeliveryForm((p) => ({ ...p, is_default: e.target.checked ? "Y" : "N" }))}
onChange={async (e) => {
const checked = e.target.checked;
if (checked) {
const existingMain = modalDeliveries.find((x) => (x.is_default === "Y" || x.is_default === true) && (x._localId || x.id) !== modalDeliveryEditId);
if (existingMain) {
const ok = await confirm(`현재 메인 납품처는 "${existingMain.destination_name}"입니다. 변경하시겠습니까?`);
if (!ok) return;
setModalDeliveries((prev) => prev.map((item) => ({ ...item, is_default: "N" })));
}
setModalDeliveryForm((p) => ({ ...p, is_default: "Y" }));
} else {
setModalDeliveryForm((p) => ({ ...p, is_default: "N" }));
}
}}
className="rounded"
/>
<span className="text-sm"> </span>

View File

@@ -60,11 +60,13 @@ export function useConfirmDialog() {
const [title, setTitle] = useState("");
const [options, setOptions] = useState<ConfirmOptions>({});
const resolveRef = useRef<((value: boolean) => void) | null>(null);
const isOpenRef = useRef(false);
const confirm = useCallback((msg: string, opts?: ConfirmOptions): Promise<boolean> => {
setTitle(msg);
setOptions(opts || {});
setOpen(true);
isOpenRef.current = true;
return new Promise<boolean>((resolve) => {
resolveRef.current = resolve;
});
@@ -73,11 +75,13 @@ export function useConfirmDialog() {
const handleConfirm = () => {
setOpen(false);
resolveRef.current?.(true);
setTimeout(() => { isOpenRef.current = false; }, 100);
};
const handleCancel = () => {
setOpen(false);
resolveRef.current?.(false);
setTimeout(() => { isOpenRef.current = false; }, 100);
};
const variant = options.variant || "default";
@@ -86,7 +90,12 @@ export function useConfirmDialog() {
const ConfirmDialogComponent = (
<AlertDialog open={open} onOpenChange={(v) => { if (!v) handleCancel(); }}>
<AlertDialogContent className="max-w-[420px]">
<AlertDialogContent
className="max-w-[420px]"
onCloseAutoFocus={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<AlertDialogHeader>
<div className="flex items-start gap-3">
<div className={cn("mt-0.5 shrink-0", config.iconClass)}>
@@ -103,10 +112,10 @@ export function useConfirmDialog() {
</div>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancel}>
<AlertDialogCancel onClick={(e) => { e.stopPropagation(); handleCancel(); }}>
{options.cancelText || "취소"}
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm} className={cn(config.buttonClass)}>
<AlertDialogAction onClick={(e) => { e.stopPropagation(); handleConfirm(); }} className={cn(config.buttonClass)}>
{options.confirmText || "확인"}
</AlertDialogAction>
</AlertDialogFooter>
@@ -114,5 +123,5 @@ export function useConfirmDialog() {
</AlertDialog>
);
return { confirm, ConfirmDialogComponent };
return { confirm, ConfirmDialogComponent, isConfirmOpenRef: isOpenRef };
}

View File

@@ -121,7 +121,7 @@ const AlertDialogContent = React.forwardRef<
return (
<DialogPrimitive.Portal container={container ?? undefined}>
<div
className="absolute inset-0 z-1050 flex items-center justify-center overflow-hidden p-4"
className="absolute inset-0 z-[10100] flex items-center justify-center overflow-hidden p-4"
style={(hiddenProp || !isTabActive) ? { display: "none" } : undefined}
>
<div className="absolute inset-0 bg-black/80" />
@@ -147,11 +147,11 @@ const AlertDialogContent = React.forwardRef<
<div
style={hiddenProp ? { display: "none" } : undefined}
>
<AlertDialogPrimitive.Overlay className="fixed inset-0 z-1050 bg-black/80" />
<AlertDialogPrimitive.Overlay className="fixed inset-0 z-[10100] bg-black/80" />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"bg-background fixed top-[50%] left-[50%] z-1100 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg",
"bg-background fixed top-[50%] left-[50%] z-[10101] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg",
className,
)}
style={adjustedStyle}