Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions app/agents/workflow_graph/document_field_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")))
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions app/documents/hwp5/templates/standard_labor_contract_v6.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"},
Expand Down
29 changes: 28 additions & 1 deletion tests/agents/test_document_field_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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": "김민수",
},
Expand All @@ -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


# 통합신청서 매핑이 체류연장 체크와 성·이름·생년월일을 채운다
Expand Down Expand Up @@ -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:
Expand Down