0%

Backend

공고 등록 자동화: PDF 분석부터 중복 스크리닝까지

전*림··수정됨 2026.07.21

공고 등록 자동화: PDF 분석부터 중복 스크리닝까지

공고 등록을 자동화했습니다: PDF 분석부터 중복 스크리닝까지

공고 하나를 등록하는 데 얼마나 많은 손이 갈까요?

운영 환경에서는 공고 PDF를 직접 열어 내용을 확인하고, 마감일과 분야, 제출 방식, 담당자 정보를 하나씩 입력해야 했습니다. 여기에 신규 수집 데이터의 중복 여부를 판단하고, 우리 서비스 대상이 아닌 공고를 걸러내는 작업까지 더해지면 단순 입력 이상의 시간이 소요됩니다.

이번 공고 등록 자동화 기능 개발 작업은 이 과정을 구조적으로 개선하기 위한 시도였습니다. 사람이 반복적으로 읽고 판단하던 영역을 시스템이 먼저 처리하도록 바꾸는 것이 목표였습니다.


PDF를 올리면, 공고가 업데이트되도록

이번 작업의 핵심은 PDF 기반 자동 분석 기능입니다.

관리자가 공고 상세 화면에서 PDF 파일을 업로드하면, 서버는 해당 파일을 분석하여 공고 정보를 자동으로 업데이트합니다. 처리 흐름은 다음과 같습니다.

image.png
  1. PDF 텍스트 추출

  2. AI 모델을 통한 구조화 분석

  3. 분석 결과를 Eval 도메인에 매핑

  4. 원본 PDF는 S3에 저장

AI 연동은 Spring AI 기반 ChatClient를 활용했습니다. OpenAiConfig에서 ChatClient 빈을 등록하고, PDF에서 추출한 원문 텍스트를 프롬프트에 포함해 모델에 전달하는 방식으로 구현했습니다. 이때 단순 요약을 요청하지 않고, 내부 Eval 도메인에 직접 매핑 가능한 JSON 형태로 반환하도록 프롬프트를 설계한 점이 핵심입니다.

예를 들어, 프롬프트는 다음과 같이 구성했습니다.

다음 공고 PDF 텍스트를 분석하여 지정된 JSON 형식으로 반환하세요.

반드시 JSON만 반환하세요.
설명 문장은 포함하지 마세요.
값이 명확하지 않으면 null로 반환하세요.

{
  "deadlineIso": "yyyy-MM-dd HH:mm",
  "field": "IT/정보통신 | 공학 | 건축/건설 | 환경/에너지 | ... (총 14개 분야 중 하나)",
  "subField": string | null,
  "fieldTags": string, 
  "submitType": "EMAIL | VISIT | MAIL | FAX | ONLINE | ELECTRONIC",
  "managerEmail": string | null,
  "address": string | null,
  "phone": string | null
}

[공고 원문]
{{pdf_text}}

이처럼 필드 구조와 Enum 후보 값을 명시함으로써, 모델이 자유 서술이 아닌 정형 데이터 생성에 집중하도록 유도했습니다. 특히 Enum 값의 범위를 제한한 점이 중요합니다. 이를 통해 이후 도메인 매핑 과정에서 추가 변환 로직을 최소화할 수 있었습니다.

실제 모델 응답은 다음과 같은 형태입니다.

{
  "deadlineIso": "2025-04-10 18:00",
  "field": "공학",
  "subField": "토목·구조",
  "fieldTags": "기술평가, 전문위원, 외부평가",
  "submitType": "EMAIL",
  "managerEmail": "eval@public.or.kr",
  "address": "서울특별시 중구 세종대로 110",
  "phone": "02-2100-1234"
}

AI는 비정형 텍스트를 구조화된 데이터로 변환하는 역할까지만 수행합니다. 이후 검증과 정규화, 그리고 최종 도메인 매핑은 모두 서버에서 처리합니다. AI를 활용하되, 데이터의 정합성과 책임은 애플리케이션이 갖도록 설계한 구조입니다.

AI는 도구이고, 최종 책임은 서버가 집니다

AI가 반환한 결과는 그대로 데이터베이스에 반영되지 않습니다.

모델이 생성한 JSON은 1차적으로 파싱을 거친 뒤, 서버에서 다시 한 번 검증과 정규화 과정을 수행합니다. 마감일(deadlineIso)은 yyyy-MM-dd HH:mm 형식으로 파싱 가능한지 확인하고, 분야(field)는 내부에서 정의한 14개 카테고리 중 하나로 매핑합니다. 제출 방식(submitType) 역시 EMAIL, ONLINE, VISIT 등 사전에 정의된 Enum 값으로 변환합니다. 이메일과 전화번호는 정규식을 통해 형식을 점검하고, 값이 불완전한 경우에는 보정하거나 null로 처리합니다.

이 과정은 단순한 후처리가 아니라, AI 응답을 서비스에서 사용 가능한 상태로 만드는 필수 단계입니다. 모델은 확률 기반으로 동작하기 때문에, 출력 형식이 항상 완벽하다고 가정할 수 없습니다. 따라서 AI는 비정형 텍스트에서 의미를 추출하는 역할까지만 수행하고, 데이터의 정합성과 무결성은 애플리케이션 레이어에서 보장하도록 설계했습니다.

중복 공고와 비대상 데이터, 사람이 먼저 보지 않도록

기존 운영 방식에서는 수집된 공고를 사람이 먼저 확인해야 했습니다.

  • 모집 목적이 아닌 명단 공개, 합격자 발표, 후보자 공지 유형을 걸러내고

  • 기존 공고와 제목이 동일하거나 유사한 데이터를 찾아 중복 여부를 판단하는

작업을 운영자가 직접 수행했습니다. 단순 반복 작업이었지만, 실제로는 가장 많은 시간을 소모하는 구간이었습니다.

이를 개선하기 위해 수집 단계에서 1차 자동 분류 로직을 추가했습니다.

image.png

신규 공고는 먼저 비대상 키워드 포함 여부를 기준으로 분류됩니다. 모집 공고가 아닌 것으로 판단되면 삭제하지 않고 edited 상태만 활성화하여 후속 노출을 제한합니다. 사람의 검토는 필요 시에만 이루어지도록 흐름을 뒤로 미뤘습니다.

비대상에 해당하지 않는 경우에는 기존 공고와 제목을 기준으로 중복 여부를 비교합니다. 중복이 감지되면 원본 공고의 관리 상태를 확인한 뒤, 신규 데이터의 edited 상태를 자동으로 동기화합니다. 이를 통해 이미 관리된 데이터의 상태가 일관되게 유지되도록 했습니다.

여기서 가장 중요하게 본 점은 자동화가 운영자의 판단을 침범하지 않도록 하는 것이었습니다. 이미 수동으로 수정된 데이터는 자동 로직이 다시 덮어쓰지 않도록 책임을 분리했습니다. 자동화는 운영을 대체하는 것이 아니라, 반복적인 판단을 줄여주는 보조 장치로 설계했습니다.

도메인 확장: EvalInfo와 EvalScreeningDto

이번 작업에서는 단순히 API만 추가한 것이 아니라, 도메인 구조도 함께 확장했습니다.

  • EvalInfo: AI 분석 결과를 매핑하기 위한 정보 객체

  • EvalScreeningDto: 스크리닝 처리 결과를 집계하여 반환하기 위한 DTO

  • EvalRepository: 중복 조회 및 상태 동기화를 위한 메서드 추가

또한 ApiResponse에 매개변수 없는 성공 응답 생성 메서드를 추가하여, 단순 성공 케이스에 대해 일관된 응답 포맷을 유지하도록 개선했습니다.

결과적으로, AI 분석 결과 → 도메인 매핑 → 상태 업데이트 → 응답 반환까지의 흐름이 하나의 일관된 레이어 구조 안에서 동작하도록 정리했습니다.

단순 자동화가 아니라, 역할의 재정의

이번 작업은 PDF를 자동으로 읽는다는 기능 구현에 그치지 않습니다.

공고 등록 과정에서 사람이 직접 읽고 입력하던 영역을 시스템이 먼저 처리하도록 구조를 바꾼 작업이었습니다. 운영자는 입력자가 아니라, 검수자이자 의사결정자가 됩니다.

  • 반복 입력은 줄어들고

  • 중복 판단은 시스템이 선처리하며

  • 데이터 정합성은 코드 레벨에서 보장됩니다

기능 단위로 보면 하나의 MR이지만, 운영 관점에서는 공고 관리 방식 자체를 바꾸는 첫 단계라고 생각합니다.

앞으로는 분석 정확도 개선, 비동기 처리 구조 도입, 처리 이력 로깅 고도화 등을 통해 자동화 범위를 점진적으로 확장해 나갈 예정입니다.

자동화는 한 번에 완성되지 않습니다.

다만, 이번 작업을 통해 “왜 사람이 먼저 읽고 있었는가”라는 질문에는 하나의 답을 만들 수 있었다고 생각합니다.

읽기 도구

8분 읽기

이 글이 도움이 되었나요?

다음으로 읽기