diff --git a/app/Http/Controllers/ESign/EsignApiController.php b/app/Http/Controllers/ESign/EsignApiController.php index cc6f5e15..ff859641 100644 --- a/app/Http/Controllers/ESign/EsignApiController.php +++ b/app/Http/Controllers/ESign/EsignApiController.php @@ -75,6 +75,45 @@ public function searchPartners(Request $request): JsonResponse return response()->json(['success' => true, 'data' => $data]); } + /** + * 사원 검색 (근로계약서용) + */ + public function searchEmployees(Request $request): JsonResponse + { + $q = trim($request->input('q', '')); + $tenantId = session('selected_tenant_id', 1); + + $query = \App\Models\HR\Employee::where('tenant_id', $tenantId) + ->where('employee_status', 'active') + ->with(['user', 'department']); + + if ($q !== '') { + $query->where(function ($w) use ($q) { + $w->whereHas('user', fn ($u) => $u->where('name', 'like', "%{$q}%") + ->orWhere('phone', 'like', "%{$q}%") + ->orWhere('email', 'like', "%{$q}%")) + ->orWhereHas('department', fn ($d) => $d->where('name', 'like', "%{$q}%")); + }); + } + + $employees = $query->limit(20)->get(); + + $data = $employees->map(fn ($emp) => [ + 'id' => $emp->id, + 'name' => $emp->user?->name, + 'phone' => $emp->user?->phone, + 'email' => $emp->user?->email, + 'department' => $emp->department?->name, + 'position' => $emp->position_label, + 'job_title' => $emp->job_title_label, + 'address' => $emp->address, + 'hire_date' => $emp->hire_date, + 'birth_year' => $emp->resident_number ? ('19'.substr($emp->resident_number, 0, 2)) : null, + ]); + + return response()->json(['success' => true, 'data' => $data]); + } + /** * 고객(명함 등록 고객) 검색 */ diff --git a/resources/views/esign/create.blade.php b/resources/views/esign/create.blade.php index de0a5858..a181331b 100644 --- a/resources/views/esign/create.blade.php +++ b/resources/views/esign/create.blade.php @@ -355,6 +355,91 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i = ); }; +// ─── EmployeeSearchModal (근로계약서용 사원 검색) ─── +const EmployeeSearchModal = ({ 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-employees?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((emp, i) => ( + + ))} +
+
+
+ ); +}; + // ─── TenantSearchModal ─── const TenantSearchModal = ({ open, onClose, onSelect }) => { const [query, setQuery] = useState(''); @@ -555,6 +640,8 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i = }; const isCustomerContract = form.title === '고객 서비스이용 계약서'; + const isLaborContract = form.title === '근로계약서'; + const [employeeModalOpen, setEmployeeModalOpen] = useState(false); // 계약번호 자동 채번 const fetchContractNumber = async () => { @@ -705,16 +792,96 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i = return; } } - // 3. 매칭 안 되면 라벨 기반 기본값 - if (!updated[v.key]) { - updated[v.key] = `테스트_${v.label}`; - } + } + // 3. 매칭 안 되면 라벨 기반 스마트 기본값 + if (!updated[v.key]) { + updated[v.key] = guessValueByLabel(v.label); } }); return updated; }); }; + // 라벨 텍스트를 분석하여 실제 데이터와 유사한 값을 생성 + const guessValueByLabel = (label) => { + const pick = arr => arr[Math.floor(Math.random() * arr.length)]; + const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; + const l = label.toLowerCase(); + + // 이름/성명 + if (/이름|성명|대표자|계약자|서명자|사원명/.test(l)) + return pick(['김민수','이서연','박지훈','최예린','정우성','한소희','강동원','윤세아']); + + // 주소 + if (/주소|소재지/.test(l)) + return pick(['서울특별시 강남구 테헤란로 123, 4층','경기도 성남시 분당구 판교로 789','서울특별시 마포구 월드컵북로 54길 12','인천광역시 연수구 송도과학로 32']); + + // 전화번호/연락처 + if (/전화|연락처|핸드폰|휴대폰|번호.*전화|mobile|phone/.test(l)) + return `010-${randInt(1000,9999)}-${randInt(1000,9999)}`; + + // 이메일 + if (/이메일|email|메일/.test(l)) + return pick(['test','info','admin','dev']) + '@' + pick(['example.com','test.kr','company.co.kr']); + + // 사업자등록번호 + if (/사업자.*번호|사업자.*등록/.test(l)) + return `${randInt(100,999)}-${randInt(10,99)}-${randInt(10000,99999)}`; + + // 주민등록번호/생년월일 + if (/주민.*번호|resident/.test(l)) + return `${randInt(70,99)}${String(randInt(1,12)).padStart(2,'0')}${String(randInt(1,28)).padStart(2,'0')}-*******`; + if (/생년월일|생년|출생/.test(l)) + return `${randInt(1970,2000)}.${randInt(1,12)}.${randInt(1,28)}`; + + // 년도 + if (/년도|연도/.test(l)) + return `${randInt(2024,2027)}`; + + // 금액/원/급여/수당/보수 + if (/금액|급여|보수|수당|임금|월급|연봉|원$/.test(l)) + return pick(['2,200,000','2,500,000','2,800,000','3,000,000','3,500,000','4,000,000']) + '원'; + + // 기간 + if (/기간|시작.*종료|계약.*일/.test(l)) + return `2026.03.11 ~ 2027.03.10`; + + // 날짜/일자/일시 + if (/날짜|일자|일시|작성일|계약일|시작일|종료일/.test(l)) + return `2026.${String(randInt(1,12)).padStart(2,'0')}.${String(randInt(1,28)).padStart(2,'0')}`; + + // 시간 + if (/시간|시각/.test(l)) + return `${String(randInt(8,18)).padStart(2,'0')}:00`; + + // 회사/상호/업체 + if (/회사|상호|업체|법인|사업장/.test(l)) + return pick(['(주)코드브릿지엑스','(주)블루오션','(주)스마트솔루션','(주)넥스트웨이브','(주)그린테크']); + + // 부서 + if (/부서|팀|소속/.test(l)) + return pick(['생산부','영업부','관리부','개발부','총무부','물류부']); + + // 직책/직위/직급 + if (/직책|직위|직급/.test(l)) + return pick(['사원','주임','대리','과장','차장','부장']); + + // 직종/업무/업종 + if (/직종|업종|업무/.test(l)) + return pick(['생산직','사무직','영업직','기술직','관리직']); + + // 퍼센트/비율 + if (/비율|퍼센트|%|율/.test(l)) + return `${randInt(5,30)}%`; + + // 수량/개수 + if (/수량|개수|인원|명/.test(l)) + return `${randInt(1,50)}`; + + // 기타: 라벨 자체를 활용한 짧은 값 + return label; + }; + const fillRandomCounterpart = async () => { try { const res = await fetch(`/esign/contracts/search-partners?q=`, { @@ -781,6 +948,46 @@ className={`w-full text-left px-3 py-2.5 rounded-lg mb-1 transition-colors ${i = })); }; + // 사원 선택 시 근로계약서 변수 자동 채우기 + const handleEmployeeSelect = (emp) => { + const labelMap = { + '직원.*주소': emp.address, + '출생.*년도': emp.birth_year, + '부서': emp.department, + '직책': emp.position, + '직위': emp.job_title || emp.position, + '연락처': emp.phone, + '전화.*번호': emp.phone, + '이메일': emp.email, + '계약자.*이름': emp.name, + '사원.*이름': emp.name, + '근로자.*이름': emp.name, + '근로자.*성명': emp.name, + '입사.*일': emp.hire_date, + }; + + setMetadata(prev => { + const updated = { ...prev }; + templateVars.forEach(v => { + for (const [pattern, value] of Object.entries(labelMap)) { + if (value && new RegExp(pattern, 'i').test(v.label)) { + updated[v.key] = value; + return; + } + } + }); + return updated; + }); + + // 상대방(직원) 서명자 정보 자동 채우기 + setForm(f => ({ + ...f, + counterpart_name: emp.name || f.counterpart_name, + counterpart_email: emp.email || f.counterpart_email, + counterpart_phone: emp.phone || f.counterpart_phone, + })); + }; + // Step 1 유효성 검사 const validateStep1 = () => { const newErrors = {}; @@ -1253,6 +1460,14 @@ className="inline-flex items-center gap-1 px-2.5 py-1 bg-white border border-gre 고객 불러오기 + ) : isLaborContract ? ( + ) : (