diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php
index ad071884..4f811856 100644
--- a/app/Http/Controllers/ESign/EsignApiController.php
+++ b/app/Http/Controllers/ESign/EsignApiController.php
@@ -13,6 +13,7 @@
use App\Models\ESign\EsignSigner;
use App\Models\ESign\EsignSignField;
use App\Models\ESign\EsignAuditLog;
+use App\Models\Tenants\Tenant;
use App\Models\Tenants\TenantSetting;
use App\Services\Barobill\BarobillService;
use App\Services\GoogleCloudStorageService;
@@ -73,6 +74,65 @@ public function searchPartners(Request $request): JsonResponse
return response()->json(['success' => true, 'data' => $data]);
}
+ /**
+ * 고객(테넌트) 검색
+ */
+ public function searchTenants(Request $request): JsonResponse
+ {
+ $q = trim($request->input('q', ''));
+
+ $query = Tenant::whereNull('deleted_at');
+
+ if ($q !== '') {
+ $query->where(function ($w) use ($q) {
+ $w->where('company_name', 'like', "%{$q}%")
+ ->orWhere('business_num', 'like', "%{$q}%")
+ ->orWhere('phone', 'like', "%{$q}%")
+ ->orWhere('ceo_name', 'like', "%{$q}%");
+ });
+ }
+
+ $tenants = $query->orderBy('company_name')->limit(20)->get();
+
+ $data = $tenants->map(fn($t) => [
+ 'id' => $t->id,
+ 'company_name' => $t->company_name,
+ 'business_num' => $t->business_num,
+ 'ceo_name' => $t->ceo_name,
+ 'address' => $t->address,
+ 'phone' => $t->phone,
+ 'email' => $t->email,
+ ]);
+
+ return response()->json(['success' => true, 'data' => $data]);
+ }
+
+ /**
+ * 계약번호 자동 채번 (CONTRACT-YYYY-MMDD-N)
+ */
+ public function generateContractNumber(): JsonResponse
+ {
+ $tenantId = session('selected_tenant_id', 1);
+ $today = now()->format('Ymd');
+ $prefix = "CONTRACT-{$today}-";
+
+ $lastContract = EsignContract::where('tenant_id', $tenantId)
+ ->where('contract_code', 'like', "{$prefix}%")
+ ->orderByRaw("CAST(SUBSTRING(contract_code, ?) AS UNSIGNED) DESC", [strlen($prefix) + 1])
+ ->first();
+
+ $seq = 1;
+ if ($lastContract) {
+ $lastSeq = (int) str_replace($prefix, '', $lastContract->contract_code);
+ $seq = $lastSeq + 1;
+ }
+
+ return response()->json([
+ 'success' => true,
+ 'data' => ['contract_number' => $prefix . $seq],
+ ]);
+ }
+
/**
* 법인도장 조회
*/
diff --git a/resources/views/esign/create.blade.php b/resources/views/esign/create.blade.php
index 51ff4677..912b0c01 100644
--- a/resources/views/esign/create.blade.php
+++ b/resources/views/esign/create.blade.php
@@ -352,6 +352,87 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
);
};
+// ─── TenantSearchModal ───
+const TenantSearchModal = ({ open, onClose, onSelect }) => {
+ const [query, setQuery] = useState('');
+ const [results, setResults] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [activeIdx, setActiveIdx] = useState(-1);
+ const inputRef = useRef(null);
+ const debounceRef = useRef(null);
+
+ const doSearch = useCallback((q) => {
+ setLoading(true);
+ fetch(`/esign/contracts/search-tenants?q=${encodeURIComponent(q)}`, {
+ headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
+ })
+ .then(r => r.json())
+ .then(json => { if (json.success) { setResults(json.data); setActiveIdx(-1); } })
+ .catch(() => {})
+ .finally(() => setLoading(false));
+ }, []);
+
+ useEffect(() => {
+ if (!open) return;
+ setQuery(''); setResults([]); setActiveIdx(-1);
+ doSearch('');
+ setTimeout(() => inputRef.current?.focus(), 100);
+ }, [open, doSearch]);
+
+ const handleInput = (val) => {
+ setQuery(val);
+ clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => doSearch(val), 250);
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIdx(i => Math.min(results.length - 1, i + 1)); }
+ else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIdx(i => Math.max(0, i - 1)); }
+ else if (e.key === 'Enter' && activeIdx >= 0 && results[activeIdx]) { e.preventDefault(); onSelect(results[activeIdx]); onClose(); }
+ else if (e.key === 'Escape') { onClose(); }
+ };
+
+ if (!open) return null;
+
+ return (
+
+
+
e.stopPropagation()}>
+
+
고객 검색
+
+
+
+ handleInput(e.target.value)} onKeyDown={handleKeyDown}
+ placeholder="상호, 사업자등록번호, 대표자명, 전화번호로 검색..."
+ className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" />
+
+
+ {loading &&
검색 중...
}
+ {!loading && results.length === 0 &&
검색 결과가 없습니다.
}
+ {!loading && results.map((t, i) => (
+
+ ))}
+
+
+
+ );
+};
+
// ─── App (Wizard) ───
const App = () => {
const [step, setStep] = useState(1);
@@ -373,6 +454,7 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
const [templateItems, setTemplateItems] = useState([]);
const [metadata, setMetadata] = useState({});
const [partnerModalOpen, setPartnerModalOpen] = useState(false);
+ const [tenantModalOpen, setTenantModalOpen] = useState(false);
const fileRef = useRef(null);
const hasTemplates = templates.length > 0;
@@ -422,6 +504,73 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
} catch (_) { setTemplateItems([]); }
};
+ const isCustomerContract = form.title === '고객 서비스이용 계약서';
+
+ // 계약번호 자동 채번
+ const fetchContractNumber = async () => {
+ try {
+ const res = await fetch('/esign/contracts/generate-contract-number', {
+ headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': csrfToken },
+ });
+ const json = await res.json();
+ if (json.success) return json.data.contract_number;
+ } catch (_) {}
+ return '';
+ };
+
+ // 계약 제목 변경 시 기본값 세팅
+ const applyTitleDefaults = async (title) => {
+ if (title === '고객 서비스이용 계약서') {
+ const contractNum = await fetchContractNumber();
+ setMetadata(prev => ({
+ ...prev,
+ ...(contractNum && !prev.contract_number ? { contract_number: contractNum } : {}),
+ ...(!prev.total_dev_cost ? { total_dev_cost: '20,000,000' } : {}),
+ ...(!prev.monthly_fee ? { monthly_fee: '500,000' } : {}),
+ }));
+ }
+ };
+
+ // 고객(테넌트) 선택 핸들러
+ const handleTenantSelect = (tenant) => {
+ const keyMap = {
+ company_name: tenant.company_name,
+ biz_no: tenant.business_num,
+ address: tenant.address,
+ phone: tenant.phone,
+ };
+ const labelMap = {
+ '상호': tenant.company_name,
+ '사업자등록증': tenant.business_num,
+ '사업자등록번호': tenant.business_num,
+ '주소': tenant.address,
+ '전화': tenant.phone,
+ '전화번호': tenant.phone,
+ };
+
+ setMetadata(prev => {
+ const updated = { ...prev };
+ templateVars.forEach(v => {
+ if (keyMap[v.key] !== undefined && keyMap[v.key]) {
+ updated[v.key] = keyMap[v.key];
+ return;
+ }
+ if (labelMap[v.label] !== undefined && labelMap[v.label]) {
+ updated[v.key] = labelMap[v.label];
+ }
+ });
+ return updated;
+ });
+
+ // 상대방 서명자 정보도 채우기
+ setForm(f => ({
+ ...f,
+ counterpart_name: tenant.company_name || f.counterpart_name,
+ counterpart_email: tenant.email || f.counterpart_email,
+ counterpart_phone: tenant.phone || f.counterpart_phone,
+ }));
+ };
+
const fillRandomCounterpart = async () => {
try {
const res = await fetch(`/esign/contracts/search-partners?q=`, {
@@ -506,6 +655,7 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =
if (!validateStep1()) return;
if (hasTemplates) { setStep(2); } else { handleSubmit(); }
} else if (step === 2) {
+ applyTitleDefaults(form.title);
setStep(3);
}
};
@@ -568,8 +718,12 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i =