Merge branch 'jskim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user