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 ? (
+
) : (