한컴인스페이스

Git · 캘린더 · HRWeb 통합 MCP 에이전트 개발

2026.03 ~ 2026.04
설계 및 개발
PythonFastMCPMCPPlaywrightGmail APIGoogle Calendar API

아키텍처

Git 커밋 로그
Git 리포 로컬 클론
Google Calendar
OAuth2 token.json
↓   ↓
MCP 서버
8개 MCP 도구 · FastMCP
AI 클라이언트
Cursor · Claude Desktop
자동화 대상
엑셀 XML · HRWeb · 한컴
Gmail SMTP · Playwright · zipfile + ElementTree

주간보고 작성, HRWeb 공수 입력, 한컴 공수 등 반복 업무를 자동화하기 위해 개인적으로 개발 후 공유한 MCP 기반 에이전트입니다. FastMCP로 8개 도구를 구현하고 Cursor·Claude Desktop에서 바로 호출할 수 있도록 배포했습니다.

핵심 기능

엑셀 서식 100% 보존 - zipfile + XML 직접 조작openpyxl 서식 손실 → ElementTree로 셀 직접 주입

주간보고 템플릿에는 병합 셀, 조건부 서식, 드롭다운 유효성 검사가 들어 있었습니다.openpyxl로 저장하면 이 서식이 전부 사라졌습니다. openpyxl이 xlsx를 읽어 재직렬화할 때 자신이 지원하지 않는 XML 요소를 그냥 버리기 때문입니다.

xlsx는 ZIP 아카이브입니다.zipfile로 압축을 풀고 xml.etree.ElementTree로 내부 xl/worksheets/sheet1.xml을 직접 파싱해 타깃 셀의 값만 교체한 뒤 다시 압축했습니다. 원본 ZIP의 나머지 파일은 전혀 건드리지 않아 서식·수식·드롭다운이 100% 보존됐습니다.

import zipfile, shutil, io
from lxml import etree

def inject_cells(template_path, out_path, cell_map: dict):
    """cell_map: {"D5": "내용", "H5": "내용"} 형태로 전달"""
    shutil.copy(template_path, out_path)

    with zipfile.ZipFile(out_path, "r") as zin:
        names = zin.namelist()
        files = {n: zin.read(n) for n in names}

    sheet_xml = files["xl/worksheets/sheet1.xml"]
    tree = etree.fromstring(sheet_xml)
    ns = {"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}

    for ref, value in cell_map.items():
        cell = tree.find(f'.//x:c[@r="{ref}"]', ns)
        if cell is not None:
            v = cell.find("x:v", ns)
            if v is None:
                v = etree.SubElement(cell, "{...}v")
            # 문자열 셀: 인라인 문자열(t="inlineStr") 방식으로 주입
            cell.set("t", "inlineStr")
            is_elem = etree.SubElement(cell, "{...}is")
            t_elem = etree.SubElement(is_elem, "{...}t")
            t_elem.text = value

    files["xl/worksheets/sheet1.xml"] = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")

    with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zout:
        for name, data in files.items():
            zout.writestr(name, data)
Playwright로 Blazor Server 폼 자동화Blazor 비동기 렌더링 타이밍 · 부분 일치 셀렉터

사내 HRWeb은 Blazor Server 앱입니다. 일반 웹과 달리 폼 값이 SignalR을 통해 서버에서 바인딩되므로 Playwright가 클릭 직후 바로 다음 요소를 조작하면 아직 렌더링이 끝나지 않아 입력이 무시됐습니다.

각 인터랙션 후 time.sleep으로 짧은 대기를 끼워 Blazor 재렌더링이 완료되길 기다렸습니다. 또한 날짜·프로젝트 셀렉터 옵션 텍스트가 매달 바뀌어부분 일치로 선택하도록 했습니다.

async def fill_hrweb(page, date: str, project_keyword: str, hours: float):
    # 월 선택 드롭다운 - 부분 일치
    await page.select_option("select#month", label=re.compile(date[:7]))
    await asyncio.sleep(0.8)  # Blazor 재렌더링 대기

    # 날짜 행 클릭
    row = page.locator(f'tr:has-text("{date}")')
    await row.click()
    await asyncio.sleep(0.5)

    # 프로젝트 셀렉터 - 키워드 부분 일치
    await page.select_option(
        "select#project",
        label=re.compile(project_keyword, re.IGNORECASE)
    )
    await asyncio.sleep(0.5)

    # 공수 입력
    hours_input = page.locator("input#workHours")
    await hours_input.fill(str(hours))
    await page.click("button#save")
    await asyncio.sleep(1.0)  # 저장 완료 대기

한컴 공수 시스템은 Google 계정으로 로그인하는 별도 서비스였습니다. 로그인 후 sessionStorage 토큰을 추출해 이후 API 요청에 재사용하는 방식으로 Playwright 세션 유지 비용을 줄였습니다.

Git · Calendar 병합으로 주간보고 자동 생성커밋 없는 날 패턴 추론 · 폴더 단위 그룹핑

주간보고에는 날짜별로 어떤 프로젝트에서 무슨 작업을 했는지 기록해야 합니다. Git 커밋 로그와 Google Calendar 출장 일정 두 소스를 병합해 초안을 만들었습니다.

소스수집 방법활용
Git 커밋subprocess git log --all --since --until날짜별 작업 내용
Google CalendarOAuth2 Events API (token.json 캐시)출장·회의 일정
패턴 추론주변 커밋 폴더명 + 평일 기본 템플릿커밋 없는 날 보완

리포 경로의 폴더명을 프로젝트 이름으로 사용해 커밋을 그룹핑했습니다. 커밋이 없는 평일은 해당 주 다른 날의 커밋에서 가장 많이 등장한 폴더명을 가져와 “유지보수” 템플릿으로 채웠습니다. 생성된 보고서는 Gmail SMTP로 즉시 발송하거나 엑셀 파일로 저장 후 AI 클라이언트에 반환했습니다.

@mcp.tool()
def generate_report(start_date: str, end_date: str) -> str:
    """Git + Calendar 병합 주간보고 생성"""
    commits = _collect_commits(start_date, end_date)   # subprocess git log
    trips   = _get_calendar_events(start_date, end_date)  # Google Calendar API

    days = _merge_by_date(commits, trips)

    for day in days:
        if not day["commits"] and day["weekday"] < 5:   # 커밋 없는 평일
            day["inferred"] = _infer_from_context(days, day["date"])

    return _render_report(days)   # 엑셀 XML 주입 후 base64 반환
FastMCP 8개 도구 구성 · 팀 배포Cursor · Claude Desktop 바로 호출

FastMCP를 사용하면 Python 함수에 @mcp.tool() 데코레이터만 붙이면 MCP 프로토콜 직렬화·스키마 생성이 자동으로 처리됩니다. 총 8개 도구를 구현했습니다.

도구역할
list_commits기간별 Git 커밋 로그 수집 · 폴더 그룹핑
get_tripsGoogle Calendar 출장·일정 조회
create_calendar_eventCalendar 일정 생성
generate_reportGit + Calendar → 주간보고 엑셀 생성
generate_report_with_contentAI가 내용 작성 → 엑셀 주입
send_report생성된 보고서 Gmail SMTP 발송
preview_hrwebHRWeb 입력 전 미리보기 텍스트 반환
upload_hrwebPlaywright로 HRWeb · 한컴 공수 자동 입력

팀원에게 배포할 때는 mcp.json에 서버 경로만 추가하면 됩니다. Git·캘린더 인증 정보는 각자 token.json.env를 두고 공용 코드를 공유하는 방식으로 배포했습니다.

Jira·Bitbucket 연동 자동 브랜치·PR 문화 정착티켓 상태 변경 시 브랜치 자동 생성 → PR까지 자동화

같은 사내 자동화의 연장선으로, Jira 티켓을 발급받은 뒤 브랜치를 만들고 PR을 여는 흐름도 매번 수작업으로 반복되고 있었습니다. 티켓 상태가 바뀌면 규칙에 따라 브랜치를 자동으로 생성하고, 작업이 끝나면 PR 생성까지 이어지는 자동화를 구축해 팀 내 브랜치·PR 명명 규칙과 작업 흐름을 정착시켰습니다.