[Control Tower] Spring AI(Gemini)로 근태 증빙서류 자동 완성(OCR) 구현하기 🚀 (Zero-Click UX 적용기)

2026. 2. 12. 11:10·Projects/Team Projects

🎯 도입 배경: 직원의 귀찮음을 AI로 해결할 수 없을까?

항공사 HR SaaS 서비스인 'Control Tower'를 개발하면서 근태 정정 신청 기능을 구현하고 있었습니다. 직원이 지각이나 결근을 했을 때, 대중교통 지연 확인서나 병원 진단서 같은 '증빙 서류'를 제출해야 한다.

기존 시스템은 직원이 1) 캘린더에서 날짜를 고르고 2) 신청 폼에 들어가서 3) 출퇴근 시간과 사유를 수기로 적은 뒤 4) 서류를 첨부하는 번거로운 과정을 거쳐야 했다.

"어차피 서류에 날짜, 시간, 사유가 다 적혀있는데 AI가 알아서 읽고 폼을 채워주면 안 될까?" 이 작은 물음에서 출발하여, Spring AI(Gemini)를 활용한 멀티모달 OCR 기능을 도입하고 프론트엔드 UX까지 전면 개편한 과정을 공유한다.


🛠️ Step 1. 백엔드: Spring AI(Gemini)로 구조화된 데이터 추출하기

가장 먼저 할 일은 이미지를 받아 텍스트를 추출하는 API를 만드는 것이었습니다. 단순한 텍스트 덩어리가 아니라 프론트엔드 폼에 딱딱 맞게 꽂아주기 위해, Gemini에게 JSON 형태로 구조화된 응답을 요구하도록 프롬프트를 설계했다.

1) VisionService 구현 및 프롬프트 엔지니어링

Java
public ProtestDto.OcrResponse extractText(MultipartFile file) {
    String promptText = "이 이미지에서 다음 정보를 추출해서 순수 JSON 형식으로만 반환해. " +
            "마크다운이나 다른 설명은 절대 추가하지 마.\n" +
            "- targetDate: 문서에 적힌 날짜 (YYYY-MM-DD 형식, 없으면 null)\n" +
            "- updateTime: 문서에 적힌 시간 (HH:mm 형식, 없으면 null)\n" +
            "- reason: 문서의 핵심 내용 및 지연/결근 사유 요약";

    String response = chatClient.prompt()
            .user(userSpec -> userSpec
                    .text(promptText)
                    .media(MimeTypeUtils.parseMimeType(file.getContentType()), resource))
            .call()
            .content();
            
    // JSON 파싱 및 반환 로직...
}

2) 트러블 슈팅: LLM의 마크다운 환각 방어 및 예외 처리

  • Early Return: Gemini API 호출은 비용과 시간이 발생하므로, 컨트롤러 단에서 file.getContentType().startsWith("image/")로 이미지 형식이 아니면 즉시 400 Bad Request를 던지도록 방어 로직을 세웠다.
  • Jackson Parsing: LLM 특성상 간혹 json ... 형태의 마크다운 블록을 붙여서 응답하는 경우가 있어, 이를 정규식으로 걷어내는 후처리를 추가했습니다. 또한, DTO에 @JsonIgnoreProperties(ignoreUnknown = true)를 붙여 AI가 임의의 키값을 추가해도 파싱 에러가 나지 않도록 안정성을 높였다.

💻 Step 2. 프론트엔드: React와 API 연동 및 상태 보호

백엔드에서 넘겨준 JSON(targetDate, updateTime, reason)을 프론트엔드의 폼(Form) 상태에 매핑했다.

UX를 고려한 상태 업데이트 로직

단순히 응답받은 값으로 상태를 덮어씌우면, 사용자가 기존에 작성 중이던 사유가 날아가는 참사가 발생할 수 있습니다. 이를 방지하기 위해 prev 상태를 참조하여 값을 안전하게 덧붙였다.

JavaScript
setFormData(prev => ({
    ...prev,
    // 날짜/시간: AI가 찾은 값이 있으면 쓰고, 없으면 기존 유저 입력값 유지
    targetDate: targetDate || prev.targetDate,
    protestRequestInTime: updateTime || prev.protestRequestInTime,
    
    // 사유: 기존에 쓰던 글이 있다면 그 아래에 줄바꿈으로 AI 추출 내용 추가
    protestReason: prev.protestReason 
        ? `${prev.protestReason}\n${reason}` 
        : reason
}));

여기까지 구현하고 테스트해 보니 사진 한 장에 글자들이 촤르륵 채워지는 마법 같은 기능을 볼 수 있었다. 하지만, 진짜 문제는 여기서부터였다.


💡 Step 3. 완벽한 Zero-Click을 위한 2가지 UX 리팩토링

기능 자체는 완벽하게 동작했지만, 사용자의 전체 플로우(User Journey)를 시뮬레이션해 보니 두 가지 치명적인 UX 결함이 발견되었다.

🚨 결함 1: 캘린더 강제 진입 문제 (진입점 개선)

  • 문제: 기존에는 캘린더에서 '날짜'를 클릭해야만 신청 페이지로 들어갈 수 있었다. 날짜를 AI가 알아서 뽑아주는데, 굳이 유저가 날짜를 먼저 고르고 들어가는 건 모순이었다.
  • 해결: 캘린더를 거치지 않고 바로 신청 폼으로 들어올 수 있도록 다이렉트 접근을 허용했다. 폼 렌더링 시 location.state에서 넘어온 날짜가 없어도 에러를 내는 대신 빈 <input type="date">를 띄워주고, AI 연동 후 formData.targetDate에 값이 꽂히도록 컴포넌트 유연성을 확보했다.

🚨 결함 2: 두 번 업로드해야 하는 불편함 (Auto-Attach)

  • 문제: 사용자가 AI 텍스트 추출용으로 서류 이미지를 올렸는데, 막상 최종 폼 제출(handleSubmit) 시에는 '증빙 서류가 없습니다'라며 같은 파일을 또 첨부 파일 목록에 넣으라고 요구했다. (OCR API와 Submit API가 분리되어 발생한 문제)
  • 해결: 사용자가 서류를 올려서 OCR에 성공하면, 텍스트 추출뿐만 아니라 제출용 첨부 파일 목록(files 배열)에도 해당 파일을 자동으로 쏙 넣어주는 로직을 추가했다. (물론 중복 방지 로직 포함!)
JavaScript
// OCR 성공 후 파일 자동 첨부 로직
setFiles(prevFiles => {
    const isAlreadyAttached = prevFiles.some(f => f.name === file.name && f.size === file.size);
    if (isAlreadyAttached) return prevFiles;
    return [...prevFiles, file];
});

🎉 마무리 및 회고

이제 직원은 [근태 정정 신청] 메뉴에 들어와서 '지연 확인서' 이미지 하나만 딱 올리면 된다. 그 순간, 날짜가 세팅되고 ➡️ 시간이 맞춰지고 ➡️ 상세 사유가 적히고 ➡️ 증빙 파일 첨부까지 한 번에 완료됩니다. 직원은 확인 후 [신청하기]만 누르면 되는 완벽한 Zero-Click 폼이 완성되었다.

이번 작업을 통해 "백엔드에서 API를 잘 던져주면 끝"이 아니라, 이 기능이 프론트엔드 화면에 어떻게 그려지고 사용자가 어떤 흐름으로 마우스를 움직이는지(UX)까지 끝까지 고민해야 진짜 가치 있는 프로덕트가 나온다는 것을 배울 수 있었다.

앞으로도 사용자 관점에서 1%의 디테일을 더 챙기는 개발자가 되어야겠다!

'Projects > Team Projects' 카테고리의 다른 글

[Control Tower] AWS S3 + CloudFront로 React 앱 정적 배포하기 (보안과 성능 최적화)  (0) 2026.03.07
[Control Tower] Spring AI로 구현한 항공사 사원 가입 자동화 (명함 OCR & 프롬프트 엔지니어링)  (0) 2026.03.07
[Control Tower] 2차 개발 구현 계획(Spring Ai & OCR)  (0) 2026.02.11
[Control Tower] 1차 개발 회고: 협업의 가치와 성장을 기록하다  (0) 2026.02.10
[Control Tower] 개발 효율은 AI로, 팀 성장은 믿음으로 (Cursor, DDD, Git 회고)  (0) 2026.01.23
'Projects/Team Projects' 카테고리의 다른 글
  • [Control Tower] AWS S3 + CloudFront로 React 앱 정적 배포하기 (보안과 성능 최적화)
  • [Control Tower] Spring AI로 구현한 항공사 사원 가입 자동화 (명함 OCR & 프롬프트 엔지니어링)
  • [Control Tower] 2차 개발 구현 계획(Spring Ai & OCR)
  • [Control Tower] 1차 개발 회고: 협업의 가치와 성장을 기록하다
tlsgkstj
tlsgkstj
짱구의 성장 일기
  • tlsgkstj
    코딩하는 짱구
    tlsgkstj
  • 전체
    오늘
    어제
    • 분류 전체보기 (159)
      • About (1)
      • Projects (35)
        • Personal Projects (21)
        • Team Projects (14)
      • Engineering (20)
        • CS & Tools (0)
        • Backend Core (15)
        • Frontend (1)
        • Infra & Cloud (2)
        • AI & Tools (1)
      • Trouble Shooting & Issues (0)
      • Growth & Career (38)
        • Interview Prep (0)
        • Retrospectives (38)
      • Archive (65)
        • TIL (8)
        • Daily Dev Q&A (56)
  • 블로그 메뉴

    • 홈
    • About
    • Projects
    • Tech Stack
    • Dev Log
    • GitHub
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    커리어리셋
    데브페스트
    aws_s3
    프로덕트개발자
    REACT
    DevFestIncheon2025
    devlog
    spring
    jpa
    java
    OrphanRemova
    경기기후바이브코딩
    network
    backend
    Spring비교
    SpringBoot
    클로드코드
    프로젝트회고
    Project_Review
    til
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
tlsgkstj
[Control Tower] Spring AI(Gemini)로 근태 증빙서류 자동 완성(OCR) 구현하기 🚀 (Zero-Click UX 적용기)
상단으로

티스토리툴바