DocPilot 은 HWP·HWPX·DOCX·PPTX·XLSX 문서를 열어 편집하고, 오른쪽 패널의 AI 에이전트(Claude·Codex)에 수정을 요청하는 데스크톱 앱입니다. 문서의 수정할 위치에 코멘트를 달아 두었다가 에이전트에 한꺼번에 요청하는 기능이 있었지만, 코멘트는 글을 선택한 곳에만 달 수 있었습니다. 또한 긴 인용은 뒷부분이 잘린 채 전달되었습니다. 그림·도형·빈 여백에 대한 요청을 표현할 방법과, 긴 글에서 위치 단서를 잃지 않는 전달 방식을 정해야 했습니다.
이 글은 DocPilot 개발을 위해 설계하고 구현한 두 기능, 빈 영역을 드래그해 다는 영역 코멘트와 긴 인용의 중략 전달을 다룹니다. 기능의 구조와 사용 방법, 실제 앱에서 Claude·Codex 로 전송해 확인한 결과를 차례로 정리합니다.
시험 범위. DOCX 시료 1건, 화면 배율 100% 에서 확인했습니다. 시험하지 않은 형식과 배율은 마지막 절의 한계에 적었습니다.
글 코멘트와 영역 코멘트의 구분
코멘트는 두 종류입니다. 어느 쪽이든 패널 목록에 쌓였다가 '한꺼번에 요청' 한 번으로 같은 요청 글에 들어갑니다.
| 구분 | 글 코멘트 | 영역 코멘트 |
|---|---|---|
| 만드는 방법 | 글을 선택하고 '코멘트' 단추, 리본 '코멘트', 또는 Ctrl+Alt+M | 문단 밖 빈 곳(쪽 여백·문단 사이·그림 위)에서 드래그, 글 위에서는 Alt+드래그 |
| 대상 | 선택한 글, 선택이 없으면 커서가 있는 문단 | 드래그한 사각형. 두 쪽에 걸쳐도 됨 |
| 문서 표시 | 노란 강조와 문단 옆 번호 | 쪽마다 점선 사각형, 번호는 첫 쪽 사각형에 |
| 에이전트에 전달하는 정보 | 문단 id, 쪽 표시, 대상 글(긴 글은 중략), 요청 | 쪽 표시, 쪽마다 가로·세로 % 위치, 영역 이미지(PNG), 겹치는 문단 id(최대 12개), 요청 |
| 사용 조건 | 편집할 수 있는 문서가 열려 있을 때 | PDF·읽기 전용 문서와 변경안 비교 화면에서는 쓸 수 없음 |
코멘트 작성과 변경안 반영 절차
- 글 코멘트 달기. 고칠 글을 선택하면 0.15초 뒤 선택 영역 아래에 '코멘트' 단추가 나타납니다. 누르고 요청을 적은 뒤 Enter 로 추가합니다. Shift+Enter 는 줄바꿈, Esc 는 취소입니다.
- 영역 코멘트 달기. 쪽 여백이나 그림 위처럼 글이 없는 곳에서 눌러 끌면 점선 사각형이 그려집니다. 놓으면 그 영역의 화면 이미지가 입력 창에 썸네일로 나오고 아래에 쪽 표시와 위치가 표시됩니다. 글 위에서 영역을 고르려면 Alt 를 누른 채 끕니다.
- 목록 확인. 추가한 코멘트는 에이전트 패널 입력창 위 '수정 요청 N건' 목록에 쌓입니다. 영역 항목은 썸네일과 "영역 · 1~2쪽" 같은 쪽 표시로 구분됩니다. 항목을 누르면 문서의 해당 위치로 이동하고, 문서의 번호를 누르면 목록의 항목이 깜박입니다.
- 한꺼번에 요청. Claude 또는 Codex 탭에서 '한꺼번에 요청'을 누르면 편집 모드로 바뀌고 요청이 전송됩니다. 입력창에 적어 둔 글은 "추가 요청"으로 붙고, 사용자 말풍선에는 '편집'과 '영역 이미지 N장' 표시가 붙습니다.
- 변경안 검토. 응답이 끝나면 원본·수정본 비교 화면이 열립니다. 항목별로 적용·제외를 고른 뒤 '수정 확인'을 눌러야 원본 파일에 반영됩니다.


에이전트에 전달되는 요청 글
'한꺼번에 요청'을 누르면 영역 이미지를 PNG 파일로 저장한 뒤 항목을 번호 목록 글로 합칩니다. 앱은 이 글 앞에 현재 문서 맥락 블록(문서 경로·형식·보는 위치·커서 문단·모드)을 붙이고, 이미지 파일 경로 목록을 엔진별 방식으로 함께 넘깁니다. 아래는 영역 코멘트 2건을 보냈을 때의 요청 글입니다. 이미지 경로의 앞부분은 줄였습니다.
문서에 단 코멘트 2건을 반영해 고쳐 줘. 항목마다 문단 id 와 대상 글을 기준으로 고치고, 요청하지 않은 곳은 바꾸지 마. 영역 항목은 이미지 경로의 그림을 먼저 확인하고, 쪽 위치와 겹치는 문단 id 를 기준으로 고쳐.
1. 영역 · 1쪽 (쪽 안 가로 4~82%, 세로 42~70%)
이미지: <앱 자료 폴더>\regions\<대화 ID>\rmvs6ckfo1.png
겹치는 문단 id: 4 "그림 1. 분기별 실적"
요청: 이 그래프 아래에 단위(억 원)를 밝히는 문장을 추가
2. 영역 · 1~2쪽 (1쪽 가로 4~59%, 세로 86~100% / 2쪽 가로 4~59%, 세로 0~13%)
이미지: <앱 자료 폴더>\regions\<대화 ID>\rmvs6ckfw2.png
겹치는 문단 id: 9 "5. 세부 추진 항목 설명 문단입니다 (중략 11자) , 예산 집행 계획을 함께 적습니다.", 10 "6. …", 11 "7. …", 12 "8. …"
요청: 쪽이 나뉜 항목 번호가 이어지는지 확인하고 문장 끝을 존댓말로 통일| 항목 | 요청 글에서의 모습 | 만드는 곳 |
|---|---|---|
| 글 코멘트 위치 | 문단 id 41 (3쪽) · 대상 글: "…", 선택이 없으면 대상 문단: "…" | comments.js prompt() |
| 영역 쪽 표시 | 1쪽, 걸치면 1~2쪽, 슬라이드 n, 시트 "이름" | comments.js regionLabel() |
| 영역 위치 | 한 쪽이면 쪽 안 가로 a~b%, 세로 c~d%, 여러 쪽이면 쪽마다 / 로 구분 | doc-editor.js regionInfo() |
| 영역 이미지 | 이미지: <PNG 경로>, 캡처에 실패하면 없음(화면 캡처 실패) | agent_save_image |
| 겹치는 문단 | 사각형과 겹치는 글 있는 문단 최대 12개, 글은 40자 중략 | doc-editor.js regionInfo() |
| 추가 요청 | 패널 입력창에 글이 있을 때만 추가 요청: … | comments.js prompt() |
긴 인용의 중략 전달
인용한 글이 한도를 넘으면 앞부분과 뒷부분을 반씩 남기고 가운데를 (중략 N자) 로 표시합니다. 이전에는 뒷부분을 버리고 … 만 붙였기 때문에 문장 끝의 단서가 전달되지 않았습니다. 전달하는 글자 수는 같습니다.
| 방식 | 612자 글을 한도 300자로 보낼 때 남는 부분 | 빠지는 부분 |
|---|---|---|
| 이전(후략) | 앞 300자 | 뒤 312자(표시 없음) |
| 현재(중략) | 앞 150자와 뒤 150자 | 가운데 312자((중략 312자) 로 표시) |
자르는 지점은 화면과 백엔드에 나뉘어 있습니다. 글을 모으는 단계에서 먼저 앞부분만 남기면 뒤 단계에서 중략할 수 없으므로, 모으는 단계는 상한(2만자)만 두고 실제 줄이기는 글을 쓰는 단계 한 곳에서 합니다. 시스템 지시에는 이 표시의 뜻과, 전체가 필요하면 get_paragraph 로 문단을 읽으라는 문장을 넣었습니다.
| 위치 | 대상 | 한도 |
|---|---|---|
comments.js prompt() | 대상 글 · 대상 문단 · 겹치는 문단 | 300자 · 120자 · 40자, 중략 |
agents/mod.rs preamble() | 맥락 블록의 커서 문단 · 선택한 글 | 200자 · 1500자, 중략 |
doc-editor.js selectionInfo() · contextInfo() | 선택한 글 · 문단 글(모으는 단계) | 상한 2만자, 줄이지 않음 |
/** 긴 글은 앞뒤를 반씩 남기고 가운데를 "(중략 N자)" 로 줄임(글자 단위 = 코드 포인트, 백엔드 clip_mid 와 같은 규칙) */
export const clipMid = (s, max) => {
const cs = Array.from(s ?? '');
if (cs.length <= max) return s ?? '';
const head = Math.ceil(max / 2), tail = max - head;
return `${cs.slice(0, head).join('')} (중략 ${(cs.length - max).toLocaleString('ko-KR')}자) ${cs.slice(cs.length - tail).join('')}`;
};Rust 백엔드에서는 agents/mod.rs 의 clip_mid 가 같은 규칙(천 단위 쉼표 포함)으로 맥락 블록을 줄입니다. 기존 clip 은 오류 메시지와 도구 결과 요약에도 쓰는 공용 함수라 그대로 두었습니다.
영역 이미지 캡처 구조
문서는 shadow DOM 안의 HTML 로, PDF 는 canvas 로 그려지므로 영역 이미지는 화면에 보이는 모습을 그대로 잘라 옵니다. Tauri 2.12.5 에는 화면 캡처 API 가 없어 WebView2 의 CapturePreview 를 직접 호출하는 명령 capture_view 를 추가했습니다. webview2-com 0.39 는 wry 가 이미 사용하는 버전이라 새로운 하위 의존성은 추가되지 않았습니다.
- 드래그를 놓으면 화면이 표시 상자 층과 노란 강조를 숨기고 두 프레임을 기다립니다. 캡처 이미지에 다른 코멘트 표시가 섞이지 않게 하기 위해서입니다.
capture_view가 UI 스레드에서CapturePreview를 호출해 웹뷰 전체를 PNG 로 메모리 스트림에 받고, 완료 알림에서 바이트를 읽어 돌려줍니다(10초 제한).- 화면이 이미지 폭과 창 폭의 비율로 화면 배율을 구해 사각형만 잘라 내고, 긴 변이 1600px 를 넘으면 줄여 PNG 로 보관합니다.
- '한꺼번에 요청' 때
agent_save_image가 PNG 를 대화별 폴더에 저장하고 경로를 돌려줍니다. 이름은 영문·숫자만 허용하고 PNG 서명을 확인합니다.
let core = w.controller().CoreWebView2()?;
let stream = SHCreateMemStream(None).ok_or_else(|| windows::core::Error::from(E_FAIL))?;
let out = stream.clone();
let handler = CapturePreviewCompletedHandler::create(Box::new(move |res| {
let _ = tx.send(res.and_then(|_| read_all(&out)).map_err(|e| e.to_string()));
Ok(())
}));
core.CapturePreview(COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG, &stream, &handler)입력 창은 높이를 재서 화면 아래로 넘치면 사각형 위에 엽니다. 처음 구현에서는 썸네일이 렌더링되기 전에 높이를 계산하여 화면 아래쪽 영역에서 입력 창이 잘리는 문제가 있었습니다. 이후 썸네일 디코딩이 끝난 뒤 입력 창을 여는 방식으로 수정했습니다.
에이전트까지의 전달 경로
요청 글과 이미지 경로는 화면에서 앱 백엔드를 거쳐 엔진으로 갑니다. 엔진은 이미지를 확인한 뒤 문서 MCP 서버의 도구로만 문서를 조회하고 수정합니다. 편집된 내용은 원본 파일 대신 변경안 파일에 누적됩니다. 원본은 사용자가 비교 화면에서 승인한 항목만 바뀝니다.
| 엔진 | 요청 글 | 영역 이미지 | 시스템 지시 |
|---|---|---|---|
| Claude | 응답마다 claude -p --output-format stream-json 을 실행해 표준 입력으로 전달 | 이미지 폴더를 --add-dir 로 추가하고, 에이전트가 요청 글의 경로를 Read 로 엶 | --append-system-prompt-file |
| Codex | 앱 내장 codex app-server 의 turn/start input 텍스트 | 같은 input 배열에 localImage 항목으로 첨부 | developerInstructions |
| Gemini · Copilot | 읽기 전용이라 보낼 수 없음 | 해당 없음 | 해당 없음 |
영역 이미지 파일의 보관과 정리
- 추가할 때는 메모리에만 둡니다. 코멘트를 지우거나 문서를 다시 열면 디스크에 아무것도 남지 않습니다.
- 보낼 때 파일로 저장합니다. 앱 자료 폴더 아래 대화별 하위 폴더에 저장합니다.
- 대화가 이어지는 동안은 남깁니다. 같은 대화의 다음 요청에서 에이전트가 이전 이미지를 다시 열 수 있습니다.
- 새 대화를 시작하면 그 대화 폴더를 지웁니다. 앱을 시작할 때는 7일 넘게 바뀌지 않은 대화 폴더를 지웁니다.
Claude 와 Codex 로 확인한 결과
배포 빌드를 원격 디버깅 포트로 실행하고 Playwright 로 조작했습니다. 시료는 그래프 그림과 20개 문단이 있는 두 쪽짜리 DOCX 입니다. 두 번째 영역 요청은 Codex 시험에서 "이 영역에 보이는 항목 번호를 그대로 적어"로 변경했습니다. 이를 통해 에이전트가 이미지를 실제로 확인했는지 검증했습니다.
| 시험 | 결과 |
|---|---|
| 한 쪽 영역(그림 위 드래그) | 입력 창에 799x374 썸네일, "영역 · 1쪽 · 가로 4~82%, 세로 42~70%" 표시. 겹치는 문단은 캡션 1개 |
| 두 쪽 영역(1쪽 아래~2쪽 위) | "1쪽 가로 4~59%, 세로 86~100% / 2쪽 가로 4~59%, 세로 0~13%", 점선 사각형 2개(번호는 첫 쪽에만), 목록 표시 "영역 · 1~2쪽" |
| 캡처 이미지 내용 | 저장된 PNG 에 문서 화면만 담기고 점선 사각형·번호·입력 창은 들어가지 않음 |
| 중략 함수 | 105자 글을 한도 20자로 줄이면 가가가가가가가가가가 (중략 85자) 가가가가가끝부분단서 |
| Claude(Sonnet, 추론 낮음) | read_paragraphs → Read → set_paragraph_text → save_document. 그림 아래 단위 문장을 넣는 변경안 1건을 만들고, 두 번째 영역은 고칠 것이 없다고 보고. 이미지 2장에 Read 는 1회 |
| Codex(GPT-6.1-Sol, 추론 낮음) | 19초 만에 응답. Read 없이 첨부 이미지를 보고 "보이는 항목 번호는 5, 6, 7, 8번"이라고 답함. 변경안 2건(그래프 단위 문장, 6번 문단 끝 "(상반기)") |


한계와 남은 과제
- Claude 가 모든 이미지를 여는지는 보장되지 않습니다. 시스템 지시와 요청 글로 열도록 안내할 뿐 강제하지 않으며, 시험에서는 이미지 2장에
Read호출이 1번이었습니다. - Codex 는 문서 도구 외에 셸 도구를 1번 호출했습니다. 사용자 환경에 설치된 문서 관련 스킬을 확인하려는 호출이었고, 문서 수정은 MCP 도구로만 이루어졌습니다.
- 화면 배율 100%·DOCX 한 건에서만 확인했습니다. 125%·150% 배율, HWP·PPTX·XLSX, 다크 테마는 시험하지 않았습니다.
- 드래그 중 화면 스크롤은 지원하지 않습니다. 영역은 화면에 보이는 범위 안에서만 고를 수 있습니다.
- 영역 위치는 쪽 크기 대비 비율입니다. 편집으로 쪽 구성이 바뀌면 같은 곳을 가리키지 않을 수 있어, 문서를 다시 열면 영역 항목도 비웁니다.
- 문서 내용이 이미지로도 엔진에 전달됩니다. 텍스트와 마찬가지로 폴더 신뢰 확인을 거치지만, 이미지 내 텍스트와 개인 정보도 함께 전송됩니다.
- 사용자 말풍선에 내부 문단 id 와 이미지 경로가 보입니다. 요청 글을 그대로 기록하기 때문이며, 표시 방식은 별도 개선 대상입니다.
정리
DocPilot 의 코멘트는 이제 글 선택뿐 아니라 화면의 사각형 영역에도 달 수 있고, 영역마다 쪽 위치와 화면 이미지를 에이전트에 함께 전달합니다. 긴 인용은 앞뒤를 남기고 가운데만 줄여 끝부분의 단서가 사라지지 않습니다. 두 기능 모두 기존 전달 경로(요청 글, 엔진, 문서 MCP 도구, 변경안, 비교 화면)의 구조를 바꾸지 않고 정보만 추가했습니다. 또한 Claude 와 Codex 모두 영역 이미지를 받아 변경안을 생성하는 것을 확인했습니다.