본문으로 건너뛰기
멍냥잇 — AI & LIFE
MUNGNYANG IT / 기록

나라장터 공고 알림 만들기: AI 적합도 평가·카카오톡·회사 메일 브리핑

AI 활용 제작 기록 · 실제 실행 중인 자동화

나라장터에서 새 공고를 매번 검색하는 대신, 관심 분야의 공고를 모아 회사와의 적합도를 평가하고 카카오톡과 회사 메일로 알려주는 프로그램을 만들었다. 핵심은 AI에게 모든 판단을 맡기는 것이 아니라 공고 수집·중복 제거·명확한 제외 규칙은 코드로 고정하고, 설명이 필요한 적합도 판단에만 AI를 쓰는 것이다.

실행 기록 확인 · 2026년 10월 5일 20시 기준10월 4~5일 저장된 공고 1,037건 중 키워드·금액 필터 통과 117건, AI 평가 기록 113건. DB의 ‘알림 처리’ 표시는 37건이다. 이 수치는 카카오톡 수신 성공 건수를 뜻하지 않는다.

어떤 문제를 해결하려고 만들었나

나라장터 공고에는 소프트웨어 개발과 직접 관련 없는 용역도 많이 섞인다. 공고명을 하나씩 열어보면 시간이 들고, 검색어만 맞춰 알림을 보내면 중요하지 않은 결과가 쌓인다. 그래서 먼저 새 공고를 빠짐없이 가져오고, 제목과 추정가격으로 후보를 줄인 다음, 남은 공고만 회사 프로필과 비교하도록 설계했다.

현재 설정은 용역 공고를 조회하고, ‘시스템·구축·AI·데이터’ 등의 포함어와 청소·감리 같은 제외어를 적용한다. 초기 기록 당시에는 추정가격이 있는 공고를 2천만 원 이상으로 걸렀고, 현재 운영 기준은 1억 5천만 원 이상으로 조정했다. 금액이 공개되지 않았다는 이유만으로 공고를 버리지는 않는다. 이런 규칙은 비용을 줄이지만 제목에 중요한 내용이 드러나지 않은 공고를 놓칠 수 있으므로, 필터를 완전한 판정기로 보지는 않는다.

수집부터 알림까지의 흐름

공공데이터포털의 조달청 나라장터 입찰공고정보서비스를 사용한다. API는 업무구분별 조회 기능이 나뉘어 있어 현재 프로그램은 용역 조회 기능만 호출한다. 한 번에 100건씩 페이지를 넘겨 전체 결과를 수집하고, 지난 실행 시각보다 10분 앞에서 다시 조회한다. 겹치는 구간에서 같은 공고가 다시 나와도 SQLite의 공고번호 확인으로 중복 처리를 피한다.

첫 실행에서는 최근 24시간을 조회한다. 이후에는 마지막 실행 시각을 저장하고 그 시점부터 이어서 확인한다. PC가 꺼져 있었더라도 다음 실행 때 누락 구간을 조회하는 구조지만, PC가 계속 꺼져 있으면 그동안 알림이 실시간으로 오지는 않는다.

아래는 실제 프로그램의 수집·중복 제거 부분을 읽기 쉽게 줄인 코드다. g2b, db, keyword_filter는 이 프로그램의 모듈이며, 인증키 값과 회사 정보는 포함하지 않았다. 이 부분만 복사해서 독립 실행할 수 있는 완성 프로그램은 아니다.

last = db.get_state(conn, "last_run")
start = (datetime.fromisoformat(last) - timedelta(minutes=10)
         if last else now - timedelta(hours=24))

for bid in g2b.fetch_bids(service_key, "servc", start, now):
    if db.is_seen(conn, bid.key):
        continue
    passed = keyword_filter.passes(bid, cfg["filter"])
    db.save_bid(conn, bid, passed)

여기서 중요한 설계 선택은 last_run만 믿고 정확히 그다음 초부터 조회하지 않는 것이다. 10분을 겹쳐 다시 가져오되 이미 본 공고는 DB에서 걸러, 조회 경계의 누락과 중복 발송을 함께 줄였다. 이 구현은 공고번호만 중복 키로 쓰므로 정정 차수를 새 이벤트로 추적하려면 키를 확장해야 한다.

AI가 맡는 일과 맡지 않는 일

AI 평가 단계에는 공고명, 발주·수요기관, 추정가격, 계약 방식, 마감일과 별도로 작성한 회사 프로필을 전달한다. 결과는 자유로운 감상문이 아니라 score, verdict, reason, risks 네 항목의 JSON으로 받는다. 코드에서 다시 구조를 검증한다. Gemini의 구조화 출력 방식은 공식 문서에 설명돼 있다.

현재 알림 기준은 60점 이상이며, 한 번 실행에 최대 10건을 보낸다. 점수와 한 문장 이유는 검토 순서를 정하기 위한 보조 정보다. 예를 들어 공고명에 ‘AI’가 들어 있어도 실제 사업 범위가 교육 행사라면 제안 적합도가 낮을 수 있다. 반대로 제목만으로 수행 범위를 확정하기 어려우면 공고 원문과 제안요청서를 사람이 확인해야 한다. AI 점수를 입찰 가능 여부나 수주 가능성의 확정 판정으로 쓰지 않는다.

평가 결과를 실제 알림에 연결하는 판단은 아래처럼 단순하다. evaluate()가 구조에 맞는 결과를 주지 못하면 임의의 점수를 생성하지 않는다. 운영 코드에는 실패 건을 다음 실행에 재시도하는 저장 경로와 한 번에 보낼 수 있는 메시지 수 제한이 추가돼 있다.

result = evaluate(bid, company_profile, cfg["evaluate"])
if result is not None and result.score >= cfg["evaluate"]["notify_threshold"]:
    text = format_alert(bid, result.score, result.verdict, result.reason)
    errors = kakao.send(text, bid.detail_url)
    if errors:
        print("발송 결과 재확인 필요")

카카오톡에서는 공고 전문을 붙여 넣지 않고 200자 안에서 점수·공고명·기관·금액·마감·짧은 이유를 보여 준다. 아래는 실제 발송 기록이 아닌 형식 설명용 가상 예시다.

[나라장터 78점·검토 필요]
민원상담 AI 챗봇 구축 사업
가상 공공기관 | 3.5억원 | 마감 10/20 10:00
업무 분야는 맞지만 상주 인력 조건 확인 필요
[공고 보기 버튼]
현재 설정에서 제안서 자동 작성은 꺼져 있다. 설정값 auto_draft_threshold: 101은 0~100점 평가로 도달할 수 없는 값이다. 소스에는 첨부파일을 분석해 제안서 초안을 만드는 경로도 있지만, 자동 알림 과정과 분리해 선택한 공고에 수동으로 실행한다. 초안을 실제 입찰서로 바로 제출할 수 있다는 뜻은 아니다.

카카오톡과 회사 메일을 나눠 받은 이유

운영하면서 카카오톡 알림에 더해 회사 메일로도 공고를 받도록 확장했다. 카카오는 새 후보를 빠르게 확인하는 데 쓰고, 회사 메일은 공고 여러 건을 한눈에 비교하는 데 쓴다. 같은 공고 평가 결과를 두 채널에 사용하므로, 메일 본문을 만들 때 AI에게 적합도를 다시 평가하도록 요청하지 않는다.

구분 카카오톡 회사 메일
발송 단위 실행 중 기준을 통과한 공고별 알림 아직 메일로 보내지 않은 대상 공고를 하루 한 통으로 묶음
담는 정보 점수·공고명·짧은 이유·원문 버튼 점수 순으로 정렬한 공고 표, 기관·금액·마감·이유·원문 링크
확인 방식 이동 중 새 공고를 빠르게 확인 회사에서 여러 공고를 비교하고 원문 검토

현재 메일 설정은 오전 9시 이후 첫 실행에 하루 한 번 보내는 방식이다. 정확히 9시에 PC가 꺼져 있으면 그 뒤 프로그램이 실행될 때 보낸다. 기준에 맞는 새 공고가 없으면 “해당 공고 없음” 메일을 보낸다. 메일이 없다는 것이 공고가 없는 상황인지 프로그램이 실행되지 않은 상황인지 구분하는 데 도움이 된다.

메일은 AI가 쓴 별도 뉴스가 아니라 DB에 저장된 공고와 평가 결과를 묶은 브리핑이다. 아직 메일로 보내지 않은 공고 중 필터·점수 기준을 통과하고 마감이 지나지 않은 항목을 조회한다. 실제 프로그램의 제어 흐름을 줄이면 아래와 같다.

if mail_enabled and now.hour >= send_hour and last_brief_date != today:
    alerts = db.unmailed(conn, notify_threshold, now_text)
    message = mailer.build(alerts) if alerts else mailer.build_empty()
    try:
        mailer.send(message, recipients)
    except Exception:
        return  # 완료 기록을 남기지 않아 다음 실행에 재시도
    mark_mailed(alerts)
    save_last_brief_date(today)

위 코드는 흐름을 설명하는 축약 예시이며 단독 실행용 프로그램은 아니다. 실제 SMTP 전송은 SSL 또는 STARTTLS를 사용한다. 서버·계정·비밀번호는 로컬 비밀 설정에, 수신 목록과 발송 시각은 별도 설정에 둔다. 회사 메일 주소와 인증 정보는 글에 공개하지 않는다.

10월 8일 오전 9시 실행 로그에는 공고 10건, 10월 9일 오전 9시 실행 로그에는 공고 7건을 묶어 발송한 기록이 있다. 이것은 메일의 개수가 아니라 각 브리핑에 담긴 공고 수다. 실제 운영에서는 카카오톡과 회사 메일 모두 수신해 활용하고 있다. 다만 SMTP 처리 기록만으로 모든 수신자의 받은편지함 도착을 보증하지는 않는다.

가상 공고 2건으로 메일 생성 함수도 확인했다. 점수가 높은 공고가 먼저 표시되고, 제목의 HTML 특수문자가 처리되며, HTML과 일반 텍스트 본문이 함께 생성됐다. 공고가 없는 경우의 제목도 확인했다. 이 추가 검증에서는 회사로 시험 메일을 보내지 않았다.

남은 운영 과제는 채널별 성공 상태를 더 세밀하게 저장하는 것이다. 메일 발송 중 예외가 발생하면 다음 실행에서 재시도하지만, 일부 수신자만 거절되는 경우의 반환값과 실제 수신 여부를 건별로 대조하는 검증은 별도로 필요하다. 카카오 처리 표시와 메일 처리 표시도 서로 다른 기록으로 관리해야 한다.

같은 구조를 구현하려면

  1. 공고 원천을 정한다. 공공데이터포털에서 나라장터 입찰공고정보서비스 활용 신청을 하고, 필요한 업무구분의 조회 기능과 필드를 확인한다. API 인증키는 환경변수에 보관한다.
  2. 회사 기준을 글로 적는다. 주력 기술, 수행 가능한 규모, 피할 사업 유형을 별도 프로필에 정리한다. 회사 정보가 비어 있으면 AI 평가의 근거도 빈약해진다.
  3. 수집과 중복 제거를 먼저 만든다. 등록 시각 범위를 저장하고 약간 겹쳐 재조회한다. 공고번호를 키로 처리 여부를 기록한다. 정정공고의 차수까지 추적하려면 키 설계를 별도로 보완해야 한다.
  4. 규칙 필터 다음에 AI를 호출한다. 제외어와 최소 금액으로 불필요한 호출을 줄이고, 남은 후보만 JSON 형식으로 평가한다. 응답 형식 오류나 호출 한도 초과 시에는 임의 점수를 만들어 보내지 않는다.
  5. 알림은 짧게 구성한다. 점수, 공고명, 기관, 금액, 마감, 평가 이유를 넣는다. 현재 카카오 텍스트 템플릿은 본문 최대 200자이므로 제목을 줄이고 원문 링크 버튼을 붙인다. 템플릿 제한과 링크 도메인 설정은 카카오 공식 문서에서 확인할 수 있다.
  6. 반복 실행과 기록을 분리한다. 이 프로젝트는 평일 시간대에 반복 실행하는 Windows 작업 스케줄러 등록 스크립트와 월별 로그 파일을 사용한다. 운영 중에는 최근 실행 시각, API 오류, 미평가 공고, 알림 실패를 함께 점검한다.

실행 명령도 단계별로 나눴다. python -m nara run --dry-run은 수집과 필터 결과만 보여 주며 DB 저장·AI 평가·알림은 하지 않는다. python -m nara run은 실제 평가와 알림까지 진행한다. 카카오 연결 시험은 python -m nara kakao-test로 따로 확인한다. 동일한 형태로 구현한다면 실계정 키와 회사 프로필을 예제 코드에 넣거나 공개 저장소에 올리지 않아야 한다.

실행 결과에서 확인한 것

10월 5일 실행 로그에는 오전 11시부터 오후 8시까지 시간대별 조회 기록이 남아 있다. 오후 5시 실행에서는 신규 5건 중 2건이 제목 필터를 통과했지만, AI 평가는 두 건 모두 10점 ‘패스’로 기록했다. 오후 7시에는 신규 8건 중 1건이 통과했고 15점 ‘패스’였다. 제목에 단어가 들어간다는 이유만으로 바로 알림을 보내지 않도록 만든 이유가 이 결과에서 드러난다.

10월 4~5일 SQLite 기록에는 공고 1,037건, 1차 통과 117건, AI 평가 113건이 있다. 평가하지 못한 후보를 다음 실행에서 재시도하는 경로도 코드에 있다. DB의 ‘알림 처리’ 37건은 발송 단계까지 진행한 기록이다. 카카오 서버의 실제 전달과 휴대폰 수신을 건별로 대조한 수치는 아니므로, 이것을 ‘37건 수신 성공’으로 표현하지 않는다. 건수는 수집 범위와 필터 설정에 따라 달라지며 성능 보증값도 아니다.

운영하면서 남은 과제

첫째, 현재 중복 키는 공고번호라서 같은 번호의 정정공고 차수를 별도 새 공고로 알리지 않는다. 둘째, 제목 기반 필터는 의미를 다 읽지 못한다. 제외·포함 규칙이 잘못되면 AI 평가 전에 중요한 공고가 사라질 수 있다. 셋째, 현 구현은 카카오 발송 함수에서 오류가 나더라도 발송 처리 플래그를 기록할 수 있으므로 실제 운영 신뢰도를 높이려면 성공한 수신자만 완료 처리하고 실패 건은 재시도하도록 개선해야 한다.

제안요청서 자동 분석도 파일 형식에 따라 차이가 있다. HWP/HWPX/PDF 등에서 텍스트를 읽는 코드는 있지만, 편집 잠금이나 외부 시스템에만 있는 첨부는 자동으로 읽을 수 없다. 결국 공고 원문, 참가 자격, 평가 기준, 제안서 초안은 사람이 다시 확인해야 한다. 이 자동화의 목적은 입찰 결정을 대신하는 것이 아니라 검토할 공고를 제때 발견하고 읽는 순서를 줄이는 것이다.

확인 자료: 공공데이터포털 나라장터 입찰공고정보서비스 · Gemini API 구조화 출력 · 카카오 메시지 기본 템플릿. 초기 실행 수치는 2026년 10월 5일 기록이며, 메일 추가와 현재 설정은 로컬 소스·SQLite 상태·10월 8~9일 발송 로그를 확인했다.