From fb443fd72a1067471d4fb13cd9d36298007f16a9 Mon Sep 17 00:00:00 2001 From: hywznn Date: Tue, 18 Aug 2026 12:14:02 +0900 Subject: [PATCH] =?UTF-8?q?fix(document):=20=EC=9E=AC=EA=B3=84=EC=95=BD=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=EC=9D=84=20=ED=91=9C=EC=A4=80=EA=B7=BC?= =?UTF-8?q?=EB=A1=9C=EA=B3=84=EC=95=BD=EC=84=9C=EC=97=90=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow_graph/document_field_map.py | 71 ++++++++++++++++--- .../templates/standard_labor_contract_v6.json | 5 ++ tests/agents/test_document_field_map.py | 29 +++++++- 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/app/agents/workflow_graph/document_field_map.py b/app/agents/workflow_graph/document_field_map.py index 8e68c54..b26b0aa 100644 --- a/app/agents/workflow_graph/document_field_map.py +++ b/app/agents/workflow_graph/document_field_map.py @@ -93,6 +93,35 @@ def _passthrough_known_fields( return {k: v for k, v in slots.items() if k in field_names and v not in (None, "")} +def _contract_period_fields(value: Any) -> dict[str, object]: + normalized = _as_str(value) + if not normalized: + return {} + if re.fullmatch(r"\d{4}-\d{2}-\d{2}\s*~\s*\d{4}-\d{2}-\d{2}", normalized): + start, end = (part.strip() for part in normalized.split("~", 1)) + return {"contract_period": f"{start} ~ {end}"} + return {"contract_months": normalized} + + +def _working_hours_summary(value: Any) -> str | None: + normalized = _as_str(value) + if not normalized: + return None + if re.fullmatch(r"\d+(?:\.\d+)?", normalized): + return f"주 {normalized}시간 (시작·종료 시각 HR 확인 필요)" + return normalized + + +def _format_won(value: Any) -> str | None: + normalized = _as_str(value) + if not normalized: + return None + compact = normalized.replace(",", "") + if compact.isdigit(): + return f"{int(compact):,}" + return normalized + + # 표준근로계약서에 넣을 값 def map_standard_labor_contract(state: RenewalState) -> dict[str, object]: slots = _slots(state) @@ -111,6 +140,10 @@ def map_standard_labor_contract(state: RenewalState) -> dict[str, object]: "business_description", "job_description", "contract_date", + "work_place", + "working_hours_summary", + "monthly_normal_wage", + "accommodation_summary", } values: dict[str, object] = _passthrough_known_fields(slots, fields) _put(values, "employee_name", _first(slots.get("full_name"), slots.get("employee_name"))) @@ -123,13 +156,17 @@ def map_standard_labor_contract(state: RenewalState) -> dict[str, object]: _put( values, "enterprise_address", - _first(slots.get("work_location"), company.get("address"), slots.get("enterprise_address")), + _first(company.get("address"), slots.get("enterprise_address")), ) - _put( - values, - "contract_months", - _first(slots.get("contract_period"), slots.get("contract_months")), + values.update( + _contract_period_fields( + _first(slots.get("contract_period"), slots.get("contract_months")) + ) ) + _put(values, "work_place", slots.get("work_location")) + _put(values, "working_hours_summary", _working_hours_summary(slots.get("working_hours"))) + _put(values, "monthly_normal_wage", _format_won(slots.get("wage"))) + _put(values, "accommodation_summary", slots.get("lodging")) _put(values, "enterprise_name", _first(company.get("name"), slots.get("enterprise_name"))) _put( values, @@ -233,7 +270,11 @@ def map_employment_extension_application(state: RenewalState) -> dict[str, objec _put( values, "representative", - _first(company.get("employer_name"), slots.get("representative"), slots.get("employer_name")), + _first( + company.get("employer_name"), + slots.get("representative"), + slots.get("employer_name"), + ), ) _put(values, "business_type", _first(slots.get("industry"), slots.get("business_type"))) _put( @@ -265,7 +306,11 @@ def map_employment_extension_application(state: RenewalState) -> dict[str, objec _put( values, "applicant_name", - _first(company.get("employer_name"), slots.get("applicant_name"), slots.get("employer_name")), + _first( + company.get("employer_name"), + slots.get("applicant_name"), + slots.get("employer_name"), + ), ) _put(values, "application_date", slots.get("application_date")) return values @@ -314,14 +359,22 @@ def map_identity_guaranty(state: RenewalState) -> dict[str, object]: _put( values, "foreign_korea_address", - _first(slots.get("lodging"), slots.get("work_location"), slots.get("foreign_korea_address")), + _first( + slots.get("lodging"), + slots.get("work_location"), + slots.get("foreign_korea_address"), + ), ) _put(values, "foreign_phone", slots.get("phone")) _put(values, "stay_purpose", _first(slots.get("stay_purpose"), "취업")) _put( values, "guarantor_name", - _first(company.get("employer_name"), slots.get("guarantor_name"), slots.get("employer_name")), + _first( + company.get("employer_name"), + slots.get("guarantor_name"), + slots.get("employer_name"), + ), ) _put(values, "workplace", _first(company.get("name"), slots.get("workplace"))) _put( diff --git a/app/documents/hwp5/templates/standard_labor_contract_v6.json b/app/documents/hwp5/templates/standard_labor_contract_v6.json index d856b47..3c92ded 100644 --- a/app/documents/hwp5/templates/standard_labor_contract_v6.json +++ b/app/documents/hwp5/templates/standard_labor_contract_v6.json @@ -11,10 +11,15 @@ "employee_birthdate": {"paragraph_index": 20, "require_empty": false, "replace_text": "생년월일 Birthdate", "replacement_format": "생년월일 Birthdate {value}"}, "employee_home_address": {"paragraph_index": 21, "require_empty": false, "replace_text": "본국주소 Address(Home Country)", "replacement_format": "본국주소 Address(Home Country) {value}"}, "contract_months": {"paragraph_index": 24, "require_empty": false, "replace_text": " ", "replacement_format": "{value}"}, + "contract_period": {"paragraph_index": 24, "require_empty": false, "replace_text": "- 신규 또는 재입국자: ( ) 개월", "replacement_format": "- 재계약 기간: {value}"}, "use_probation": {"type": "checkbox", "paragraph_index": 26, "require_empty": false}, "industry": {"paragraph_index": 38, "require_empty": false, "replace_text": "- 업종:", "replacement_format": "- 업종: {value}"}, "business_description": {"paragraph_index": 39, "require_empty": false, "replace_text": "- 사업내용:", "replacement_format": "- 사업내용: {value}"}, "job_description": {"paragraph_index": 40, "require_empty": false, "replace_text": "- 직무내용:", "replacement_format": "- 직무내용: {value}"}, + "work_place": {"paragraph_index": 33, "require_empty": false, "replace_text": "2. 근로장소 ", "replacement_format": "2. 근로장소: {value}"}, + "working_hours_summary": {"paragraph_index": 48, "require_empty": false, "replace_text": " 시 분 ~ 시 분", "replacement_format": "근로시간: {value}"}, + "monthly_normal_wage": {"paragraph_index": 73, "require_empty": false, "replace_text": "1) 월 통상임금 ( )원", "replacement_format": "1) 월 통상임금 ({value})원"}, + "accommodation_summary": {"paragraph_index": 99, "require_empty": false, "replace_text": " - 숙박시설 제공 여부: [ ]제공 [ ]미제공", "replacement_format": " - 숙박 조건: {value} (제공 여부·부담액 HR 확인 필요)"}, "payment_direct": {"type": "checkbox", "paragraph_index": 92, "require_empty": false, "checkbox_occurrence": 0}, "payment_bank": {"type": "checkbox", "paragraph_index": 92, "require_empty": false, "checkbox_occurrence": 1}, "contract_date": {"paragraph_index": 123, "require_empty": false, "replace_text": "년 월 일", "replacement_format": "{value}"}, diff --git a/tests/agents/test_document_field_map.py b/tests/agents/test_document_field_map.py index e849582..9c0250c 100644 --- a/tests/agents/test_document_field_map.py +++ b/tests/agents/test_document_field_map.py @@ -11,6 +11,7 @@ ) from app.agents.workflow_graph.nodes.document_generator import EditingServiceDocumentGenerator from app.agents.workflow_graph.state import empty_renewal_state +from app.documents.hwp5 import Hwp5BinaryDocument # 매핑·생성 검증용 샘플 Shared State @@ -31,8 +32,9 @@ def _sample_state(): "job_description": "금속 부품 조립", "work_location": "경기도 안산시", "lodging": "기숙사 A동", - "contract_period": "12", + "contract_period": "2026-10-01~2027-09-30", "wage": "2500000", + "working_hours": "40", "industry": "제조업", "employer_name": "김민수", }, @@ -55,7 +57,22 @@ def test_labor_contract_mapping_uses_slots_and_company() -> None: assert values["enterprise_name"] == "주식회사 한빛정밀" assert values["business_number"] == "123-45-67890" assert values["job_description"] == "금속 부품 조립" + assert values["contract_period"] == "2026-10-01 ~ 2027-09-30" + assert values["enterprise_address"] == "경기도 안산시 단원구 산단로 000" + assert values["work_place"] == "경기도 안산시" + assert values["working_hours_summary"] == "주 40시간 (시작·종료 시각 HR 확인 필요)" + assert values["monthly_normal_wage"] == "2,500,000" + assert values["accommodation_summary"] == "기숙사 A동" + + +def test_labor_contract_mapping_keeps_legacy_month_count() -> None: + state = _sample_state() + state["slots"]["contract_period"] = "12" + + values = map_standard_labor_contract(state) + assert values["contract_months"] == "12" + assert "contract_period" not in values # 통합신청서 매핑이 체류연장 체크와 성·이름·생년월일을 채운다 @@ -108,6 +125,16 @@ def test_editing_generator_maps_and_generates_or_stubs(tmp_path: Path) -> None: assert Path(labor["path"]).exists() assert labor["changed_fields"] + paragraphs = { + paragraph.index: paragraph.text + for paragraph in Hwp5BinaryDocument(labor["path"]).paragraphs() + } + assert "2026-10-01 ~ 2027-09-30" in paragraphs[24] + assert "경기도 안산시" in paragraphs[33] + assert "주 40시간" in paragraphs[48] + assert "2,500,000" in paragraphs[73] + assert "기숙사 A동" in paragraphs[99] + # OCR 신분 값이 slots보다 우선해 서류 매핑에 반영된다 def test_ocr_merges_into_document_slots() -> None: