"use client"; import React, { useState, useEffect, useMemo, useCallback } from "react"; import { apiClient } from "@/lib/api/client"; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ export interface Customer { id: string; customer_name: string; customer_code?: string; business_number?: string; phone?: string; address?: string; } interface CustomerModalProps { open: boolean; onClose: () => void; onSelect: (customer: Customer) => void; } /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ const AVATAR_COLORS = [ "#22c55e", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899", "#14b8a6", "#f97316", "#6366f1", "#84cc16", "#e11d48", "#0ea5e9", "#a855f7", "#10b981", ]; function getAvatarColor(name: string): string { let hash = 0; for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash); } return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length]; } /** Get the Korean initial consonant (Chosung) for sorting */ export function getChosung(char: string): string { const code = char.charCodeAt(0); if (code < 0xAC00 || code > 0xD7A3) { const upper = char.toUpperCase(); if (/[A-Z]/.test(upper)) return upper; return "#"; } const chosungList = [ "\u3131","\u3132","\u3134","\u3137","\u3138","\u3139","\u3141","\u3142","\u3143","\u3145", "\u3146","\u3147","\u3148","\u3149","\u314A","\u314B","\u314C","\u314D","\u314E", ]; const idx = Math.floor((code - 0xAC00) / 588); return chosungList[idx] ?? "#"; } /** Check if a query (possibly chosung-only) matches a Korean string */ export function matchChosung(text: string, query: string): boolean { if (!query) return true; if (text.toLowerCase().includes(query.toLowerCase())) return true; const chosungChars = "\u3131\u3132\u3134\u3137\u3138\u3139\u3141\u3142\u3143\u3145\u3146\u3147\u3148\u3149\u314A\u314B\u314C\u314D\u314E"; const isChosungQuery = [...query].every((c) => chosungChars.includes(c)); if (!isChosungQuery) return false; const cleaned = text.replace(/^\(주\)|\(유\)|\(합\)/g, "").trim(); const textChosung = [...cleaned].map((c) => getChosung(c)).join(""); return textChosung.includes(query); } /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export function CustomerModal({ open, onClose, onSelect }: CustomerModalProps) { const [customers, setCustomers] = useState([]); const [search, setSearch] = useState(""); const [sortMode, setSortMode] = useState<"korean" | "abc">("korean"); const [loading, setLoading] = useState(false); /* Fetch customers (customer_mng = 고객사) */ const fetchCustomers = useCallback(async () => { setLoading(true); try { const res = await apiClient.get("/data/customer_mng", { params: { pageSize: 500 }, }); const data = res.data?.data ?? res.data?.rows ?? []; const list: Customer[] = (Array.isArray(data) ? data : []).map((r: Record) => ({ id: String(r.id ?? ""), customer_name: String(r.customer_name ?? r.name ?? ""), customer_code: String(r.customer_code ?? r.code ?? ""), business_number: String(r.business_number ?? ""), phone: String(r.contact_phone ?? r.phone ?? ""), address: String(r.address ?? ""), })); setCustomers(list); } catch { setCustomers([]); } finally { setLoading(false); } }, []); useEffect(() => { if (open) { fetchCustomers(); setSearch(""); } }, [open, fetchCustomers]); /* Filtered + grouped */ const grouped = useMemo(() => { const filtered = customers.filter((c) => c.customer_name.toLowerCase().includes(search.toLowerCase()) || (c.customer_code ?? "").toLowerCase().includes(search.toLowerCase()) ); const sorted = [...filtered].sort((a, b) => { if (sortMode === "abc") return a.customer_name.localeCompare(b.customer_name, "en"); return a.customer_name.localeCompare(b.customer_name, "ko"); }); const groups: { letter: string; items: Customer[] }[] = []; const map = new Map(); for (const c of sorted) { const first = c.customer_name.replace(/^\(주\)|\(유\)|\(합\)/g, "").trim().charAt(0); const letter = getChosung(first); if (!map.has(letter)) map.set(letter, []); map.get(letter)!.push(c); } for (const [letter, items] of map) { groups.push({ letter, items }); } return groups; }, [customers, search, sortMode]); if (!open) return null; return (
{/* Overlay */}
{/* Modal */}
{/* Header */}

고객사 선택

{/* Search */}
setSearch(e.target.value)} placeholder="고객사명 또는 코드 검색..." className="w-full pl-10 pr-4 py-2.5 border border-gray-200 rounded-xl text-sm outline-none focus:border-green-400 focus:ring-2 focus:ring-green-100 transition-all" />
{/* Customer list */}
{loading ? (
불러오는 중...
) : grouped.length === 0 ? (
{search ? "검색 결과가 없습니다" : "고객사가 없습니다"}
) : ( grouped.map((group) => (
{group.letter}
{group.items.map((customer) => { const displayName = customer.customer_name.replace(/^\(주\)|\(유\)|\(합\)/g, "").trim(); const initial = displayName.charAt(0); const color = getAvatarColor(customer.customer_name); return ( ); })}
)) )}
); }