Tag
AI
9 posts
Hermes로 만든 AI 웨딩 플래너 김실장: 생각보다 너무 좋다
2026.08.12
요약: 김실장은 결혼 준비를 대신 결정하는 AI가 아니다. 두 사람이 놓치기 쉬운 일정·예산·업체·결정 이유를 같은 상태로 보게 만드는, Hermes 기반의 개인용 운영 시스템이다. 결혼 준비를 시작하면 정보는 넘친다. 검색하면 후기와 견적이 쏟아지고, 웨딩 앱에는 체크리스트가 있고, 상담을 다녀오면 카카오톡과 사진첩에도 기록이 쌓인다. 문제는 정보량이 아니라 상태가 흩어진다는 것이었다. 어떤 업체를 검토 중인지, 계약 전에 무엇을 확인해야 하는지, 비용을 넣으면 전체 예산이 어떻게 달라지는지, 일정 전에 준비할 것은 무엇인지. 둘이 이야기를 나눌 때는 분명히 알고 있었는데 며칠만 지나면 다시 처음부터 찾아보게 됐다. 그래서 두 사람이 함께 쓰는 AI 웨딩 플래너 김실장을 만들었다. Hermes와 대화하는 창이 먼저 보이지만, 실제 중심은 채팅이 아니다. 중요한 건 대화가 지나간 뒤에도 남는 기록과 상태다. 생성형 이미지로 표현한 김실장의 사용 장면. 실제 사람·일정·업체 화면은 포함하지 않았다. 처음에는 “드레스샵 비교해줘”, “이번 주에 무엇을 해야 하지?”처럼 질문에 답하는 AI로 시작했다. 그런데 결혼 준비처럼 기간이 길고 이해관계가 많은 일은 답변을 잘 받는 것만으로 해결되지 않았다. 한 가지 선택에도 같이 남겨야 할 것이 많다. - 지금은 제안인지, 확인 중인지, 확정인지 - 누가 어떤 이유로 이 선택을 꺼냈는지 - 다음으로 확인할 조건은 무엇인지 - 연결된 예산과 일정은 무엇인지 - 나중에 다시 봐야 할 원문은 어디에 있는지 김실장은 이 정보를 가능한 한 같은 흐름으로 다룬다. Hermes는 대화를 바탕으로 자료와 확인할 조건을 정리하고, 다음 행동을 제안하는 인터페이스다. 하지만 결정 자체를 채팅 기록 속에 묻어두지는 않는다. 결혼 준비를 대화 문제가 아니라 공유해야 하는 상태의 문제로 보기 시작한 뒤부터 구조가 훨씬 단단해졌다. 김실장의 기준 문서는 로컬 Markdown이다. 체크리스트, 예산, 업체 비교, 의사결정 기록, 상담 메모처럼 길게 맥락을 남겨야 하는 정보는 파일로 관리한다. 특정 서비스의 화면이나 한 번의 대화에만 의존하지 않기 위해서다. 이 방식이 좋은 이유는 단순하다. - 사람이 읽기 쉽다. - 변경 이유를 문장으로 남길 수 있다. - 필요한 도구를 바꿔도 원본 기록은 남는다. - Hermes도 같은 문서를 읽고, 이미 정리된 맥락을 바탕으로 다음 작업을 이어갈 수 있다. 특히 의사결정 기록은 “무엇을 골랐는가”보다 “왜 그렇게 골랐는가”를 남기는 데 유용하다. 시간이 지나면 최종 선택은 기억나도 비교했던 후보와 망설였던 조건은 사라진다. 그래서 제안, 확인, 확정 같은 상태를 구분하고 결정 이유와 후속 확인 항목을 같이 적는다. Markdown이 원본 기록이라면, Notion은 현재 진행 중인 일을 연결해서 보는 운영 레이어다. 김실장에서는 업체·할 일·예산·상담 메모를 관계형 데이터로 관리한다. 업체를 볼 때 연결된 확인 항목·비용·상담 기록·다음 행동을 함께 볼 수 있도록 운영한다. 아직 특정 업체로 분류하지 않은 항목은 별도 분류로 관리한다. 여기서 중요한 건 같은 내용을 모두 한곳에 몰아넣지 않는 것이다. 비교 근거와 긴 메모는 Markdown에 남기고, 현재 운영에 필요한 관계와 상태는 Notion에서 본다. 입력이 겹치지 않도록 역할을 나누는 원칙으로 운영한다. 로컬 원본 기록, 관계형 운영 데이터, 일정, 계산 자동화가 역할별로 나뉘는 모습을 개념적으로 표현했다. 이 그림은 자동 동기화 흐름을 뜻하지 않는다. 전체 구조는 다음처럼 역할을 나눠 둔 상태에 가깝다. 둘의 대화·상담 자료·조사 결과 ├─ Hermes: 자료·확인 항목 정리와 다음 행동 제안 ├─ 로컬 Markdown: 긴 기록과 결정 근거 ├─ Notion 관계형 운영 DB: 관계와 상태 관리 └─ Google Calendar: 시간 제약이 있는 일정 관리 규칙 기반 자동화: Notion 예비비 계산·갱신 레이어 사이의 반영이 모두 자동으로 이뤄진다는 뜻은 아니다. 현재 확인된 규칙 기반 자동화는 Notion 예비비 계산이며, 그 밖의 레이어 간 반영을 모두 자동화로 단정하지 않는다. 가능한 한 각 레이어의 역할을 나누는 게 핵심이다. Markdown은 기억과 근거를 보존하고, Notion은 관계를 보여주고, Calendar는 시간 제약이 있는 일을 실행 가능한 일정으로 만든다. “상담 전 확인할 조건 정리하기”는 할 일이다. 반면 “특정 시각의 상담”이나 “가봉 일정”은 시간이 묶인 실행 항목이다. 둘을 같은 목록에 쌓아두면 중요한 시간 약속이 체크리스트에 파묻힌다. 그래서 시간 제약이 있는 항목은 전용 Google Calendar에 분리해 관리한다. 등록한 일정은 재조회로 반영 여부를 확인하는 방식으로 운영해 왔다. 아직 정확한 소요 시간이 확인되지 않은 일정은 필요에 따라 임시 블록으로 잡고, 상담이나 안내문을 확인한 뒤 수정한다. 이렇게 하면 일정도 “등록했으니 아마 됐겠지”가 아니라, 확인 가능한 운영 상태가 된다. 결혼 준비에서 예산은 자주 바뀐다. 어떤 항목은 계약하면서 금액이 달라지고, 어떤 비용은 아직 미확정이고, 비교안은 남겨두되 합계에는 중복으로 넣으면 안 된다. 김실장에는 updatenotioncontingency.py라는 작은 자동화 스크립트가 있다. 이 스크립트는 Notion의 계획 예산을 읽어 정해 둔 계산 규칙에 따라 예비비를 다시 계산한다. 흐름은 복잡하지 않다. 1. Notion의 계획 예산을 읽는다. 2. 예식장 분야에 연결된 항목과 예비비 자체는 합계에서 뺀다. 3. 목표 예산과의 차이로 남은 예비비를 계산한다. 4. 예산이 목표를 넘으면 예비비를 0으로 두고 초과 상태를 기록한다. 5. 값이 달라졌거나 초과 상태일 때 예비비 항목의 금액과 비고를 갱신한다. 이 스크립트는 별도 Hermes 스케줄 작업으로 현재 30분마다 실행된다. 여기서 자동화가 하는 일은 결정을 내리는 것이 아니라, 이미 입력한 계획이 지금 어떤 결과를 만드는지 투명하게 보여주는 것이다. 예를 들어 새로운 견적을 반영했을 때 “이걸 진행해도 되는가”를 AI가 답으로 확정하는 게 아니다. 대신 예산 상태가 어떻게 변했는지, 무엇을 다시 봐야 하는지 빠르게 드러낸다. 자동화는 계산과 확인을 맡고, 계약·결제·취향처럼 중요한 결정은 두 사람이 검토하는 경계를 표현한 이미지다. 운영하며 Hermes에 맡기는 일은 정리와 추적이다. - 상담 내용을 바탕으로 확인할 조건 정리하기 - 업체·할 일·예산·일정의 연결 관계 보기 - 미확정 상태를 구분해 보기 - 반복 계산을 같은 규칙으로 처리하기 - 이전 결정의 근거를 다시 찾기 반대로 자동으로 확정하지 않는 일도 명확히 둔다. - 계약과 결제 - 취소·환불·변경 조건의 최종 확인 - 예산 초과를 받아들일지 여부 - 둘의 취향이 갈리는 선택 - 개인정보가 들어가는 외부 공유 특히 계약 조건이나 실제 금액은 대화 요약이 아니라 안내문·견적서·계약서처럼 원문으로 다시 확인한다. AI가 정리한 내용은 다음 질문을 잘 만들기 위한 보조 자료이며, 계약 판단의 근거로 단독 사용하지 않는다. 이 경계를 두니까 오히려 편하게 쓸 수 있었다. AI에게 일을 많이 시키는 것보다, 잘못하면 곤란한 일을 자동으로 넘기지 않는 편이 훨씬 중요했다. 김실장을 혼자 썼다면 아마 괜찮은 개인 정리 도구 정도였을 것이다. 두 사람이 함께 쓰면서 더 좋아졌다. 한 사람이 모든 정보를 머릿속에 들고 있는 구조가 줄었다. 누가 마지막으로 어디까지 알아봤는지, 왜 그 선택을 보류했는지, 다음 상담 전에 무엇을 물어봐야 하는지 같은 정보가 특정 사람의 기억에만 남지 않는다. 그리고 두 사람이 대화할 때도 조금 달라졌다. “이거 할까?”에서 끝나는 대신, 현재 예산·남은 확인 항목·일정상 마감·이전 결정의 맥락을 같이 보고 이야기하게 된다. AI가 결혼 준비를 대신해줘서 좋은 건 아니다. 우리가 같은 상태를 보고, 빠뜨리지 않고 같이 준비할 수 있게 해줘서 좋다. 김실장은 웨딩 앱을 대체하려고 만든 서비스가 아니다. 두 사람이 진행하는 큰 프로젝트를 대화와 캡처에 흩어두지 않고, 기록·관계·일정·계산으로 나눠 운영해보려는 실험에 가깝다. 결혼 준비가 끝난 뒤에도 이 구조는 남을 것 같다. 집을 구하거나, 여행을 계획하거나, 큰 지출을 관리할 때도 결국 비슷한 문제가 생긴다. 정보는 흩어지고, 결정은 쌓이고, 일정과 예산은 연결되고, 누군가는 전체 맥락을 기억해야 한다. 김실장이 지금 해주는 가장 좋은 일은 답을 대신 내리는 게 아니다. 우리가 결정하기 전에 필요한 맥락을 다시 꺼내주고, 결정한 뒤에는 이유와 다음 행동을 잃어버리지 않게 해주는 일이다.
내 우분투 서버에 Hermes로 내부 대시보드 만들고 운영하기
2026.07.11
요약: 중요한 건 Docker로 띄웠다는 사실이 아니라, 내 우분투 서버 위에서 Hermes Agent를 실행기로 삼아 내부 운영 대시보드를 직접 만들고 계속 고쳐가며 쓰고 있다는 점이다. 내 우분투 서버에는 이것저것 많이 올라가 있다. Hermes Agent, n8n, Docker 서비스, Cloudflare Tunnel, Telegram 알림, 내부 프로젝트들. 처음엔 각각 따로 확인했다. docker ps systemctl status something journalctl -u something curl localhost:3000 이런 식으로. 물론 가능은 하다. 근데 자주 쓰다 보면 금방 귀찮아진다. 서비스가 죽었는지, 포트가 열려 있는지, n8n은 정상인지, Hermes gateway는 살아 있는지 매번 터미널에서 확인하는 게 은근히 번거롭다. 그래서 생각했다. 내가 쓰는 서버 상태를 한 화면에서 보면 되지 않을까? 처음부터 거창한 운영툴을 만들 생각은 아니었다. Datadog이나 Grafana 같은 걸 붙이고 싶은 것도 아니었다. 그냥 내가 매일 보는 정보만, 내 방식대로 정리된 작은 내부 대시보드가 필요했다. 이 대시보드는 Docker로 배포돼 있다. 하지만 이 글에서 중요한 건 Docker가 아니다. Docker는 그냥 실행 방식이다. 컨테이너로 띄우면 재시작하기 쉽고, 환경이 고정되고, 서버에서 관리하기 편하니까 쓴다. 그 이상도 이하도 아니다. 진짜 중요한 건 이 대시보드를 내가 Hermes와 대화하면서 만들고 운영한다는 점이다. 예전 같으면 대시보드를 고치려면 이렇게 해야 했다. 1. 서버 접속 2. 프로젝트 폴더 이동 3. 코드 수정 4. 빌드 5. Docker 재빌드 6. 브라우저 확인 7. 문제 있으면 다시 수정 지금은 흐름이 다르다. 나: Hermes 페이지에서 토큰 사용량 카드 빼줘. Hermes: 코드 수정 → 빌드 → Docker 재시작 → 브라우저 확인 나: 프로젝트별 Vercel 링크가 공통 대시보드로 가는데 고쳐줘. Hermes: Vercel API 확인 → 동적 URL 생성 → 빌드 → 실제 링크 검증 나: Penpot 완전히 삭제해. 대시보드에서도 빼고. Hermes: Docker 스택 삭제 → 볼륨 확인 → 대시보드 코드 제거 → 404 검증 대시보드는 사람이 직접 보는 화면이지만, 운영 방식은 거의 AI 에이전트 중심으로 바뀌었다. 내 우분투 서버의 주요 상태, Hermes, n8n, Vercel 프로젝트를 한 화면에서 보는 내부 대시보드. 처음에는 욕심이 많았다. 서버 상태, 토큰 사용량, 프로젝트 카드, n8n 상태, Hermes 상태, 배포 링크, 설정값까지 다 보여주고 싶었다. 근데 막상 써보니 자주 보는 정보는 몇 개 안 됐다. - 서버가 살아 있는가 - Docker 컨테이너가 정상인가 - n8n이 떠 있는가 - Hermes gateway가 살아 있는가 - 주요 프로젝트 링크가 맞는 곳으로 가는가 - 문제가 생겼을 때 어디부터 봐야 하는가 그래서 대시보드도 점점 줄이는 방향으로 갔다. 필요 없는 카드는 빼고, 자주 누르는 링크는 앞으로 가져오고, 설정 화면도 실제로 쓰는 값만 남겼다. 이게 개인 내부 도구의 장점이다. 일반 사용자를 설득할 필요가 없다. 내 사용 패턴에 안 맞으면 바로 지우면 된다. 처음 화면은 조금 산만했다. 페이지마다 정보는 있는데, 어디에 뭐가 있는지 한눈에 안 들어왔다. 그래서 좌측 메뉴를 만들었다. 참고한 건 Linear다. 어두운 배경, 작은 메뉴, 튀지 않는 구분선, 현재 위치만 조용히 강조되는 구조. 내부 도구는 예쁘게 꾸미는 것보다 계속 보기 편한 것이 중요하다. 너무 화려하면 금방 질린다. 반대로 너무 밋밋하면 어디를 눌러야 할지 모르겠다. 그래서 지금 구조는 대충 이렇다. Overview Server Hermes n8n Settings 많지 않다. 이 정도면 충분하다. Hermes Agent 상세 화면. Gateway, Cron, Memory, 세션 상태를 따로 확인할 수 있게 만들었다. 내가 Hermes를 서버에 올려두고 가장 크게 체감한 건 이 부분이다. 단순히 코드를 써주는 게 아니다. 서버 안에서 실제로 실행하고 확인한다. 예를 들면 이런 작업 흐름이다. 1. 코드 수정 2. npm run lint 3. npm run build 4. docker compose build 5. docker compose up -d 6. 브라우저에서 실제 페이지 확인 7. DOM이나 API 응답까지 검증 이 과정이 중요하다. 내부 대시보드는 말만 그럴듯하면 안 된다. 실제 서버에서 떠야 하고, 내가 브라우저로 봤을 때 맞아야 한다. 특히 예전에 Vercel 링크를 고칠 때 이 차이가 컸다. 처음엔 링크가 전부 https://vercel.com/dashboard로 가고 있었다. 보기엔 Vercel 버튼이 있으니 괜찮아 보였지만, 실제로 누르면 각 프로젝트 Overview가 아니라 공통 대시보드로 이동했다. Hermes는 이걸 하드코딩으로 때우지 않고, Vercel API에서 팀 slug와 프로젝트명을 받아와서 동적으로 URL을 만들게 고쳤다. 그리고 브라우저에서 실제 링크 href까지 확인했다. 이런 게 사람이 직접 하면 귀찮은데, 에이전트가 하면 딱 맞는 작업이다. 개인 도구를 만들 때 의외로 중요한 건 추가보다 삭제다. Penpot도 한동안 서버에 올려뒀지만 결국 지웠다. 안 쓰는 서비스는 서버 리소스만 먹고, 대시보드에도 쓸데없는 카드가 남는다. 그래서 이렇게 요청했다. Penpot 완전히 삭제하려고. 내 대시보드에서도 삭제하고. Hermes가 한 일은 단순히 카드 하나 지운 게 아니었다. - Penpot Docker 컨테이너 삭제 - 관련 볼륨 삭제 - /home/duncan/docker/penpot 디렉토리 삭제 - 대시보드의 Penpot 카드 제거 - Settings의 Penpot URL 설정 제거 - /server/penpot 페이지 삭제 - 실제로 404가 나는지 확인 - 메인 페이지 HTML에 penpot 문자열이 남았는지 확인 이런 식으로 서비스 제거도 하나의 운영 작업이 된다. 그냥 화면에서 안 보이게 숨기는 게 아니라, 서버와 코드 양쪽에서 실제로 정리하는 것이다. 이 대시보드는 아직 대단한 제품이 아니다. 남에게 팔 수 있는 SaaS도 아니고, 범용 모니터링 도구도 아니다. 하지만 나한테는 의미가 크다. 서버에 뭔가 문제가 생기면 예전에는 내가 직접 들어가서 봐야 했다. 지금은 먼저 물어본다. 예지야, 메모리 왜 올라갔어? 그러면 Hermes가 free, ps, docker stats, systemctl을 확인해서 원인을 찾아준다. 실제로 VS Code Remote 서버가 맥북에서는 꺼졌는데 우분투에 남아 메모리를 쓰고 있던 것도 이렇게 잡았다. 예지야, 이 프로세스 뭐야? 그러면 dockerd인지, Codex app-server인지, n8n node 프로세스인지 확인하고 설명해준다. 대시보드는 이 구조의 시각적 입구다. 내가 직접 볼 수도 있고, Hermes가 수정할 수도 있고, 필요 없는 건 지울 수도 있다. 내가 만든 건 단순한 서버 상태판이 아니다. 조금 더 정확히 말하면: 내 우분투 서버를 AI 에이전트가 이해하고 조작할 수 있게 만드는 운영 인터페이스 에 가깝다. 브라우저 화면은 사람을 위한 인터페이스고, Hermes는 실행을 위한 인터페이스다. 둘이 같은 서버 위에 있으니 작업 흐름이 짧아진다. 사람이 본다 → 문제를 말한다 → Hermes가 고친다 → 서버에서 검증한다 → 대시보드에 반영된다 이 순환이 생긴 게 제일 크다. 아직 하고 싶은 건 많다. - Docker 컨테이너별 메모리/CPU 추이 - n8n 워크플로우 실패 알림 요약 - Hermes gateway 상태와 최근 에러 로그 - GitHub Actions / Vercel 배포 상태 - Linear 이슈 자동화 상태 - 내 서버에서 돌고 있는 장기 프로세스 목록 - “정리해도 되는 서비스” 추천 하지만 전부 한 번에 붙일 생각은 없다. 내부 도구는 많이 넣는 순간 다시 안 쓰게 된다. 필요가 생길 때 하나씩 붙이고, 안 쓰는 건 바로 빼는 게 맞다. 이 대시보드를 만들면서 느낀 건 하나다. 개인 서버 운영에서 중요한 건 도구를 많이 붙이는 게 아니다. 내가 실제로 운영하는 방식에 맞게, 서버와 AI 에이전트와 화면을 하나의 흐름으로 묶는 것이다. Docker는 그중 실행 방식일 뿐이다. 핵심은 내 우분투 서버 위에 Hermes가 있고, 내가 그 Hermes에게 말하면 서버가 실제로 바뀐다는 점이다. 이제 내 서버는 그냥 접속해서 관리하는 서버가 아니라, 대화하면서 고쳐가는 작업 환경에 가까워지고 있다. 아직 거칠지만, 이 방향은 꽤 마음에 든다.
헤르메스 에이전트(Hermes Agent)와 n8n 조합 개꿀
2026.07.07
요약: n8n은 공개 이벤트를 받고 최소 payload만 넘기는 브릿지로 두고, Hermes Agent가 Linear MCP로 이슈 본문과 댓글을 직접 읽어 작업·검증·완료 처리까지 맡게 했다. n8n으로 [사내 챗봇](/posts/n8ngwa-nosyeoneuro-gucughaneun-rag-giban-sanae-ai-caesbos-gaideu)도 만들어봤고, 캘린더 비서도 만들어봤다. 트리거 받아서 API 몇 개 호출하고 조건 분기하는 자동화는 n8n이 정말 잘한다. 근데 어느 순간 벽에 부딪혔다. 판단이 필요한 작업, 예를 들어 "이 PR 리뷰 코멘트 보고 실제로 코드 고쳐라", "이 Linear 이슈 보고 관련 파일 찾아서 초안 작성해라" 같은 건 n8n 노드 조합으로는 안 된다. AI Agent 노드를 넣어봐도 결국 프롬프트 하나 던지고 텍스트 응답 받는 수준이지, 파일 시스템을 만지거나 여러 단계를 거쳐 뭔가를 실제로 완성시키는 건 완전히 다른 이야기였다. 그래서 최근에 [홈 서버에 올려둔 Hermes Agent](/posts/hom-seobeoe-hermes-agent-olryeobwassda-sabjil-girog)를 n8n 뒤에 실행기로 붙이는 구조를 시험해봤는데, 써보니 이 조합 진짜 개꿀이다. 아이디어 자체는 별거 없다. n8n은 바깥 세상과 접촉하는 레이어, Hermes는 안쪽에서 실제로 일하는 레이어로 나누는 거다. n8n (공개/트리거 레이어) Linear webhook, GitHub webhook, Slack 메시지, 스케줄 트리거를 받는다. 외부 서비스(Notion, Slack, Google 등)와의 인증/연동을 관리한다. 들어온 이벤트를 정제해서 "이 작업을 해줘"라는 형태로 다듬는다. 결과를 다시 Slack이나 Linear 코멘트로 되돌려주는 것도 n8n 몫이다. Hermes Agent (신뢰된 내부 실행기) 실제 판단이 필요한 작업, 즉 코드 수정, 파일 탐색, 여러 단계 추론이 필요한 작업을 맡는다. 세션 간 메모리를 유지하니까 "저번에 하던 그 작업 이어서 해줘" 같은 것도 가능하다. 내부적으로 Claude Code를 오케스트레이션해서 실제 리포지토리를 건드리는 작업(브랜치 생성, 커밋, PR 초안)까지 위임한다. 서버 로컬 파일 시스템, 내부 DB, 사내 API에 직접 접근할 수 있는 유일한 레이어다. 결국 n8n이 못 하는 "진짜 일"은 전부 Hermes로 넘기고, n8n은 그 앞단에서 트래픽을 정리하고 결과를 사람이 보는 채널로 되돌리는 배달원 역할만 하는 셈이다. 여기서 제일 신경 쓴 부분은 Hermes를 절대 인터넷에 직접 노출하지 않는다는 거다. 내 서버 구조는 이렇다. [외부 웹훅/사용자] │ (HTTPS, 공개) ▼ ┌────────┐ │ n8n │ ← 공개 진입점. Cloudflare/Nginx 리버스 프록시 뒤 └───┬────┘ │ (Docker bridge network, 내부 전용) ▼ ┌──────────────┐ │ hermes-agent │ ← 외부 포트 없음. n8n 컨테이너에서만 접근 가능 └──────────────┘ │ ▼ Claude Code / 로컬 파일시스템 / 내부 DB 중요한 건 세 가지다. 1. n8n만 공개 도메인/포트를 가진다. Hermes gateway는 Docker bridge 네트워크 내부 IP로만 열려 있고, 호스트에 포트를 바인딩하지 않는다. 2. 맥북에서 관리용으로 붙을 때는 Tailscale을 쓴다. 이건 나만 접근하는 관리 채널이고, n8n → Hermes 트래픽과는 완전히 다른 경로다. 3. 인증 경계 자체가 다르다. n8n은 webhook URL + secret 정도로 외부 요청을 받지만, Hermes 쪽 엔드포인트는 n8n 컨테이너의 내부 네트워크 접근 자체가 인증이다. 외부에서 Hermes를 직접 때릴 방법이 물리적으로 없다. 이렇게 나누면 "공개된 표면적"과 "실제로 시스템을 조작할 수 있는 권한"이 분리된다. n8n 웹훅이 뚫려도 공격자가 할 수 있는 건 n8n 워크플로우 안에서 노출한 것뿐이고, Hermes가 가진 파일 시스템/Claude Code 실행 권한까지는 못 건드린다. 여기가 이번에 제일 많이 갈아엎은 부분이다. 처음엔 n8n이 이슈 본문까지 통째로 읽어서 정리한 다음 Hermes한테 던지는 구조로 짰는데, 쓰다 보니 이건 방향이 틀렸다는 걸 깨달았다. n8n이 이슈 내용을 파싱하고 가공하기 시작하면, 결국 "판단"의 일부를 n8n이 떠안게 된다. 그러면 Hermes를 붙인 의미가 없어진다. 그래서 최종적으로는 n8n을 얇은 공개 브릿지로만 쓰기로 했다. n8n은 Linear 이벤트를 받아서, "이슈가 예지(Yeji)한테 새로 할당/위임됐는지"만 필터링한다. 본문이나 코멘트 내용은 절대 읽지도, 가공하지도 않는다. 이슈 ID, 팀, 액션 타입 정도만 들어간 최소 payload를 만든다. 여기에 요청 ID와 내부 secret을 붙여서 Hermes webhook(linear-yeji-agent-session)으로 그대로 던진다. 실제 이슈 본문/코멘트/컨텍스트는 Hermes가 Linear MCP로 직접 가져온다. getissue, listcomments로 전체 맥락을 읽고, 어떤 리포지토리 작업인지 매핑한 다음 Claude Code를 오케스트레이션해서 실제로 작업하고, 결과를 검증한 뒤 savecomment로 Linear에 코멘트를 남기고 saveissue로 상태를 Done으로 바꾼다. Linear 이벤트 → n8n: 예지 위임 여부 필터 → n8n: 최소 payload 생성 (issue id, request id, secret) → POST → Hermes webhook (linear-yeji-agent-session) → Hermes: Linear MCP로 getissue / listcomments → Hermes: 리포지토리 매핑 → Claude Code 오케스트레이션 → Hermes: 결과 검증 → savecomment → Hermes: saveissue (status: Done) n8n은 최소한의 이벤트 정보만 정리해서 호출하고 끝이다. 처음엔 중간에 별도 워커 하나를 두고 n8n이 그 워커 상태를 폴링하는 과도기 구조도 써봤는데, 정리하고 보니 n8n → Hermes webhook → Linear MCP로 바로 이어지는 쪽이 훨씬 깔끔했다. 그 워커는 어디까지나 전환기용 런처였고, 이 조합의 핵심은 아니다. GitHub PR에 리뷰 코멘트가 달리면 n8n이 받아서 Hermes에 넘긴다. Hermes가 Claude Code로 실제 코드를 고치고 커밋 푸시까지 하면, n8n이 PR에 "반영 완료" 코멘트를 남긴다. 물론 이건 신뢰도가 높은 저장소/브랜치에서만, 그리고 항상 PR 형태로 남겨서 최종 머지는 사람이 하도록 걸어뒀다. 배포 파이프라인이 실패하면 n8n이 로그를 모아서 Hermes에 "이 로그 보고 원인 추정해줘"라고 요청하고, Hermes가 분석 결과를 정리해서 Slack으로 알림을 보낸다. 이건 판단만 하고 실행은 안 하는 케이스라 권한 경계가 특히 느슨해도 되는 예시다. 이건 최종 구조는 아니고, 중간에 잠깐 곁길로 샜던 실험이다. n8n과 Hermes를 같은 서버에 올려두면, Hermes가 n8n 워크플로우 파일이나 API에 직접 접근해서 워크플로우 자체를 만들어줄 수도 있지 않을까 싶어서 시험해본 적이 있다. 실제로 프롬프트 한 번으로 대충 동작하는 워크플로우가 나오긴 했다. 근데 이건 결국 메인 구조로 채택하지 않았다. Hermes가 n8n 워크플로우 정의까지 손대기 시작하면 경계가 다시 흐려진다 — n8n은 얇은 공개 브릿지로 남아있어야 하는데, 그 브릿지 자체를 내부 실행기가 마음대로 고쳐 쓸 수 있으면 "공개 표면적 = n8n만"이라는 전제가 깨진다. 그래서 최종적으로는 3번(Linear 예지 위임)에서 정리한 것처럼, Hermes는 Linear MCP로 이슈를 읽고 쓰는 것까지만 맡고, n8n 워크플로우 자체는 사람이 관리하는 쪽으로 정리했다. 재미있는 실험이었지만 핵심 얘기는 아니다. 쓰기 권한은 항상 브랜치/PR 단위로 제한한다. Hermes가 main에 직접 푸시하는 경로는 아예 만들지 않았다. n8n → Hermes 요청에도 내부 전용 토큰을 넣는다. 네트워크가 분리돼 있어도 컨테이너 하나가 뚫렸을 때를 대비한 이중 방어다. Hermes가 접근 가능한 리포지토리/디렉토리를 명시적으로 화이트리스트해뒀다. 아무 경로나 건드릴 수 있게 열어두지 않는다. 실행 결과는 항상 사람이 보는 채널(Slack, Linear 코멘트)로 되돌아오게 해서, 뭔가 이상하게 돌아가면 바로 눈에 띄게 만들었다. n8n이 이슈 본문을 읽고 가공하던 초기 구조. 처음엔 n8n이 이슈 제목/본문/코멘트를 다 정리해서 Hermes한테 넘기는 식으로 짰다. 근데 이러면 n8n이 "무슨 내용인지 이해하고 정리하는" 역할까지 떠안게 되고, Hermes는 그냥 실행만 하는 처지가 된다. 결국 n8n은 필터링과 최소 payload 조립까지만 하고, 본문/코멘트는 Hermes가 Linear MCP로 직접 읽어오는 구조로 정리했다. 중간에 잠깐 거쳐간 별도 워커. n8n과 Hermes 사이에 상태를 관리하는 중간 워커를 하나 두고 폴링하는 과도기 구조를 써본 적이 있다. 하지만 최종 방향은 n8n → Hermes webhook → Linear MCP로 바로 잇는 쪽이다. 책임이 줄고, 장애 지점도 줄고, 설명도 쉬워졌다. Docker bridge 네트워크 이름 충돌. n8n과 Hermes를 각각 다른 docker-compose로 올렸더니 기본 네트워크가 분리돼서 서로 못 봤다. 명시적으로 external network를 하나 만들어서 두 컴포즈 파일이 같은 네트워크를 참조하게 고쳐서 해결했다. 권한을 너무 넓게 줬던 것. 처음엔 편의를 위해 Hermes에 파일 시스템 접근을 넓게 줬다가, 이건 아니다 싶어서 필요한 만큼만 남기고 다시 좁혔다. 자동화가 편해질수록 권한 경계는 오히려 더 타이트하게 잡아야 한다는 걸 이번에 체감했다. 처음 이 조합을 시작한다면 이 정도 구조부터 권한다. 1. Docker Compose로 n8n + Hermes를 같은 internal network에 올린다. Hermes는 호스트 포트 바인딩 없이, n8n 컨테이너에서만 접근 가능하게. 2. n8n은 리버스 프록시 뒤에서 공개 webhook만 받는다. HTTPS + secret 검증은 필수. 3. n8n은 이벤트 필터링과 최소 payload 조립까지만 한다. 이슈 본문/코멘트 같은 실제 컨텍스트는 Hermes가 Linear MCP로 직접 가져오게 하고, n8n은 절대 그 내용을 읽거나 실행하지 않는다. 4. Hermes의 쓰기 권한은 브랜치/디렉토리 화이트리스트로 제한하고, 결과는 항상 사람이 확인하는 채널(Linear 코멘트 등)로 되돌린다. 5. 관리자 접근(나)은 Tailscale로만, n8n → Hermes 트래픽과 완전히 분리된 경로를 유지한다. 이 다섯 가지만 지켜도 "n8n은 얼굴, Hermes는 손발" 구조를 안전하게 돌릴 수 있다. n8n만으로는 트리거와 연동은 되는데 진짜 판단이나 실행은 안 됐고, Hermes만 쓰자니 외부 서비스 연동과 트리거 관리가 번거로웠다. 둘을 역할별로 나눠 붙이니까 각자 잘하는 것만 하게 됐다. 처음엔 n8n이 이슈 내용을 정리해서 넘겨주는 구조로 짰다가, 그게 결국 n8n이 판단의 일부를 떠안는 거라는 걸 깨닫고 방향을 바꿨다. 지금은 n8n이 필터링과 최소 payload 전달까지만 하고, Hermes가 Linear MCP로 직접 컨텍스트를 읽고 작업하고 코멘트 남기고 이슈를 Done으로 바꾸는 데까지 전부 맡는 구조로 정리됐다. n8n: 공개 트리거, 이벤트 필터링, 최소 payload 전달 Hermes: 신뢰된 내부 실행기, Linear MCP로 컨텍스트 조회/코멘트/상태 변경, Claude Code 오케스트레이션 경계: Docker internal network + 화이트리스트 권한 + Tailscale 관리 채널 아직 실전 검증이 더 필요한 구간이 남아있지만, 방향 자체는 확실히 맞다고 본다. 작업 환경: Ubuntu 미니PC, n8n (Docker), Hermes Agent (Nous Research), Claude Code, Tailscale
폰으로 서버를 제어한다 — Claude Code Remote Control 세팅기
2026.06.22
폰으로 홈 서버에 채팅을 걸면, 서버 위의 Claude가 실행하고 결과를 돌려준다. MacBook 없이 10분 세팅으로 된다. 나는 Intel N100 미니PC에 Ubuntu 24를 올려서 홈 서버(duncan-ubuntu)를 운영하고 있다. n8n 자동화를 비롯해 여러 도커 컨테이너들이 거기서 돌아간다. 문제는 외부에서 서버를 제어하려면 MacBook을 꺼내야 했다는 거다. Tailscale이 깔려 있어서 어디서든 SSH는 가능하다. 그런데 SSH로 터미널을 여는 건 폰에서 하기엔 번거롭다. 코드도 입력해야 하고, 작은 화면에 터미널 UX는 그렇게 좋지 않다. 내가 원했던 건 이거였다: 이동 중에 폰으로 서버 상태를 물어보고 싶다 "n8n 컨테이너 재시작해줘"처럼 자연어로 작업을 시키고 싶다 결과를 텍스트로 받아보고 싶다 그러니까 터미널 에뮬레이터가 아니라, 서버 위의 AI 에이전트를 폰에서 채팅으로 제어하는 것. Claude Code Remote Control이 그 역할을 한다. Claude Code는 CLI 기반 AI 코딩 에이전트다. 터미널에서 실행하면 파일을 읽고, bash를 실행하고, 코드를 수정하는 걸 Claude가 알아서 한다. Remote Control은 여기에 붙는 기능이다. 서버에서 실행 중인 Claude Code 세션을 외부 기기(폰, 다른 맥)에서 채팅 UI로 연결할 수 있다. Claude 앱을 열면 "Code" 탭에 연결된 세션이 나타나고, 거기서 채팅하면 실제로 서버 위에서 명령이 실행된다. 구조를 단순하게 그리면 이렇다: [폰 Claude 앱] ←→ [Anthropic 릴레이] ←→ [duncan-ubuntu의 claude 프로세스] 서버를 인터넷에 직접 노출하지 않고, Anthropic 릴레이를 통해 연결된다. Tailscale 없이도 된다. Claude Pro 또는 Max 플랜 (API 키 방식 불가, claude.ai OAuth 로그인 필수) 서버: Ubuntu 등 Linux 계열 (macOS도 됨) 폰: Claude 앱 설치 (iOS / Android) 서버에 SSH로 접속한 뒤: curl -fsSL https://claude.ai/install.sh | bash 설치 확인: claude --version claude auth login claude.ai 계정으로 OAuth 로그인. API 키 방식으로 연결된 환경이라면 먼저 해제해야 한다: unset ANTHROPICAPIKEY tmux 없이도 Remote Control은 된다. 그러나 SSH 연결이 끊기면 프로세스도 같이 죽는다. 상시 연결을 원한다면 tmux가 필수다. sudo apt install tmux -y tmux new -s claude tmux 세션 안에서: claude Claude Code가 실행되면, 프롬프트에서: /remote-control duncan-ubuntu 세션 이름은 원하는 이름으로 지으면 된다. 실행하면 연결 URL과 QR 코드가 출력된다. Claude 앱 → Code 탭 → 좌측 사이드바에 방금 만든 세션이 보인다. 탭하면 연결 완료. 혹은 서버에서 출력된 QR을 폰 카메라로 스캔해도 된다. 연결되면 채팅창에 타이핑한 내용이 서버의 Claude Code로 전달된다. Claude는 필요하면 bash를 실행하고, 파일을 읽고, 결과를 돌려준다. 실제로 쓰는 것들: 서버 상태 확인 docker ps 해줘 컨테이너 목록, 상태, 업타임을 정리해서 보여준다. 컨테이너 재시작 n8n 재시작해줘 docker restart n8n-n8n-1을 실행하고 결과를 알려준다. 로그 확인 n8n 최근 에러 로그 보여줘 docker logs 결과에서 에러만 필터링해서 가져온다. 디스크/메모리 상태 디스크 남은 용량 확인해줘 간단한 파일 작업 /etc/hosts에 뭐가 들어있어? 요약하면, 터미널에서 SSH로 하던 작업 대부분을 자연어 채팅으로 할 수 있다. 푸시 알림은 안 된다. Routines(클라우드 스케줄 에이전트)는 Remote Control 세션과 분리된 환경에서 실행되기 때문에, 작업 완료 알림이 폰으로 오지 않는다. 정기 알림이 필요하면 n8n cron → Slack으로 별도 구성하는 게 낫다. 서버 재시작 후 세션이 죽는다. tmux 세션이 살아있어도 서버가 재부팅되면 다시 접속해서 claude → /remote-control을 실행해야 한다. systemd 서비스로 자동 시작을 구성할 수 있지만 아직 직접 해보진 않았다. OAuth 로그인 필수. Hermes처럼 API 키로 Anthropic API를 쓰는 에이전트 시스템과는 별개다. Remote Control은 claude.ai 계정 기반이라 구독 플랜이 필요하다. 복잡한 셋업 없이 10분 안에 끝난다. Tailscale로 이미 서버에 붙을 수 있는 환경이라면 추가 네트워크 설정은 전혀 필요 없다. 이동 중에 서버에 뭔가 물어보거나, 간단한 작업을 시켜야 할 때 MacBook을 꺼낼 필요가 없어졌다는 게 체감상 제일 크다. 코딩 에이전트라는 포지셔닝 때문에 개발 용도로만 생각하기 쉬운데, 서버 운영·관리 용도로도 충분히 유용하다. 설치 환경: Ubuntu 24.04, Claude Code v2.1.183+, Claude Pro/Max 플랜
홈 서버에 Hermes Agent 올려봤다 — 삽질 기록
2026.06.09
요약: Ubuntu 미니PC에 AI 에이전트를 상시 실행 서비스로 올리는 게 이렇게 간단할 줄 몰랐다. 그리고 이렇게 삽질할 줄도 몰랐다. AI 에이전트 툴을 한 번 제대로 써보고 싶다는 생각은 오래 전부터 있었다. 조건은 하나였다. 에이전트는 서버에서 돌고, 나는 맥북에서 앱으로 붙는 구조. SSH 열고 터미널 띄우는 게 아니라, 앱 켜면 바로 연결되는 리모트 환경. 코딩할 때 잠깐 켜는 도구가 아니라, 서버에서 상시 돌면서 슬랙으로 명령 받고 결과 뱉는 구조도 함께 원했다. 후보는 여러 개였는데 최종적으로 둘을 비교했다. OpenClaw — GitHub 스타 수가 어마어마하고 생태계가 넓다. 심지어 Hermes에는 hermes claw migrate라는 마이그레이션 명령이 있을 정도로 인지도 있는 도구다. 근데 코드베이스가 거대하고, 내가 필요한 건 그 1%도 안 될 것 같았다. Hermes Agent — Nous Research 작. 세션 간 메모리 지속, 자가 스킬 생성, 멀티 메시징 플랫폼 지원. MIT 라이선스, 데이터는 내 머신에만. 결국 Hermes를 선택한 이유는 세 가지였다. 첫째, 맥북 전용 데스크탑 앱이 있고 리모트 서버에 붙는 구조를 공식 지원한다. Tailscale IP만 입력하면 앱에서 바로 연결된다. 둘째, Slack 연동이 네이티브로 된다. 셋째, LLM 프로바이더를 고정하지 않는다 — OpenRouter, Nous Portal, Anthropic, 자체 엔드포인트까지 hermes model 한 줄로 전환 가능하다. 집에서 운영 중인 Ubuntu 미니PC(duncan-ubuntu, Intel N100, RAM 16GB)에 올렸다. 이미 Ghost CMS, n8n이 각자 전용 유저로 격리돼서 돌고 있는 구조라, Hermes도 똑같은 패턴으로 붙이기로 했다. 접근은 Tailscale로 한다. 포트 직접 열지 않아도 된다. sudo useradd -m -s /bin/bash hermes sudo su - hermes curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash 끝이다. 진짜로. 설치 스크립트가 Python 3.11 가상환경, Node.js, 의존성 전부 알아서 세팅해준다. 2분도 안 걸렸다. 설치 후 LLM 프로바이더와 Slack 연동 설정은 두 가지 방법 중 하나로 한다. hermes setup nano ~/.hermes/.env 나는 .env를 직접 편집했다. 프로바이더 API 키와 Slack 봇 토큰을 여기에 넣으면 된다. Slack 연동 설정하고 테스트 메시지 보냈더니 바로 에러가 떴다. slacksdk.errors.SlackApiError: missingscope 원인은 Slack Bot Token 스코프 설정 누락이었다. 처음에 기본 권한만 주고 넘어갔는데, 채널/그룹 목록을 읽으려면 추가 스코프가 필요했다. Slack App 설정 → OAuth & Permissions 에서 아래 스코프를 추가하고 Bot Token 재발급 후 ~/.hermes/.env 업데이트. channels:read groups:read mpim:read im:read 이거 하나 때문에 30분 썼다. 에러 메시지는 명확했는데 어디서 추가하는지 처음엔 헷갈렸다. 처음엔 서비스가 하나인 줄 알았다. 근데 구조를 보니 둘이었다. | 서비스 | 역할 | 없으면? | | --- | --- | --- | | hermes-gateway | Slack/Telegram 등 연동, 실제 에이전트 루프 | 메시징 명령 안 먹힘 | | hermes-dashboard | 맥북 데스크탑 앱 연결용 웹소켓 백엔드 | 앱 접속 불가, Slack은 정상 | Slack만 쓸 거면 gateway만 올려도 된다. 나는 맥북 데스크탑 앱도 쓰고 싶어서 둘 다 systemd로 등록했다. [Unit] Description=Hermes Agent Gateway After=network-online.target Wants=network-online.target [Service] User=hermes Group=hermes ExecStart=/home/hermes/.hermes/hermes-agent/venv/bin/python -m hermescli.main gateway run WorkingDirectory=/home/hermes/.hermes EnvironmentFile=/home/hermes/.hermes/.env Restart=always RestartSec=5 [Install] WantedBy=multi-user.target [Unit] Description=Hermes Agent Dashboard After=network.target hermes-gateway.service [Service] User=hermes EnvironmentFile=/home/hermes/.hermes/.env ExecStart=/home/hermes/.local/bin/hermes dashboard --host 100.x.x.x Restart=always RestartSec=5 [Install] WantedBy=multi-user.target --host에는 Tailscale IP를 넣는다. systemctl enable 해두면 서버 재부팅 후 자동 실행된다. 한 가지 더 — gateway 서비스의 ExecStart는 반드시 venv 절대경로로 써야 한다. hermes gateway처럼 CLI 명령만 쓰면 PATH 해석이 꼬이는 경우가 있었다. 데스크탑 앱에서 Gateway 설정에 Tailscale IP와 대시보드 포트(기본 9119)를 입력하면 끝. VPN(Tailscale) 없이 외부 포트 열 필요 없다는 게 제일 마음에 드는 부분이다. 보안 신경 쓸 게 크게 줄었다. 서버에 이미 Betterstack Heartbeat 크론이 돌고 있어서, Hermes gateway용 항목을 하나 추가했다. /5 systemctl is-active --quiet hermes-gateway && curl -s https://uptime.betterstack.com/api/v1/heartbeat/[TOKEN] /dev/null systemctl is-active --quiet 조건을 걸어서, 서비스가 죽어 있으면 핑을 안 보내게 했다. Betterstack이 핑 끊기면 알람 날려주는 구조. 솔직히 말하면 아직 업무에 깊게 붙이진 못했다. 설치하고 Slack 연동되는 걸 확인하는 것까지가 이번 라운드였다. 다만 구조를 보면서 느낀 건, 이게 단순한 챗봇 래퍼가 아니라는 점이다. 세션이 끊겨도 프로젝트 맥락을 기억하고, 문제 해결 패턴을 스킬로 저장한다. 이미 내 서버에서 돌면서 스스로 스킬을 쌓아가고 있다. 지금 n8n으로 돌리는 자동화 파이프라인들을 점진적으로 이쪽으로 옮기거나 연계하는 방향을 고민 중이다. 아직 검증이 더 필요하지만, 일단 인프라 세팅 허들은 생각보다 낮았다는 건 확실히 말할 수 있다. 설치: curl 한 줄, 체감 난이도 낮음. 인스톨러가 venv·Node까지 알아서 처리 인증: LLM 프로바이더 API 키 방식 (.env 또는 hermes setup 마법사) 삽질 구간: Slack 스코프 설정, gateway/dashboard 역할 구분, systemd ExecStart 경로 운영 구조: systemd 서비스 + Tailscale + Betterstack 조합 현재 상태: 세팅 완료, 실사용 검증 중 다음엔 실제로 업무 자동화에 붙여본 결과를 써볼 예정이다. 설치 환경: Ubuntu 24.04, Hermes Agent (Nous Research), Slack Socket Mode
Text-to-SQL: "매출 지난달이랑 비교해줘" 한 마디로 BigQuery의 데이터 분석 및 리포트 작성
2026.06.04
TL;DR — SQL을 몰라도 된다. 자연어로 분석을 요청하면 AI가 SQL을 자동 생성하고, BigQuery에 직접 쿼리를 날려 결과를 돌려주는 Text-to-SQL 분석 시스템을 구축했다. 핵심은 Claude Projects + BigQuery MCP + 커스텀 Analytics Skill의 조합이다. 데이터가 쌓이면 질문이 생긴다. "이번 달 채널별 매출 비중이 어떻게 돼?" "지난 3개월 평균이랑 비교하면 어때?" "해외 환자 유입이 전년 대비 얼마나 늘었어?" 이 질문들의 공통점은 답이 데이터베이스 안에 있다는 것이다. 그런데 문제가 있었다. 데이터를 꺼내려면 SQL을 알아야 한다. 우리팀은 분석 요청이 생길 때마다 업무 리소스가 발생했다. 요청 → 전달 → 쿼리 작성 → 결과 전달의 사이클이 반복됐고, 특히 복잡한 분석을 요청하는 경우 쿼리 작성 시간이 길어졌다. 우리팀은 더 게을러지기 위해 이 Claude Project를 만들었다. Text-to-SQL은 자연어(Natural Language) 질의를 SQL로 자동 변환하는 기술이다. NL2SQL이라고도 부른다. 개념 자체는 오래됐지만, LLM(대형 언어 모델)의 등장으로 실용적인 수준에 도달했다. 특히 테이블 스키마와 비즈니스 컨텍스트를 함께 제공하면, 모델이 도메인에 맞는 정확한 SQL을 생성할 수 있게 됐다. 이 방식을 선택한 이유는 단순하다. 별도 UI 개발 없이 구현 가능 기존 BigQuery 인프라를 그대로 활용 비개발자도 즉시 사용 가능 질문의 형태와 깊이에 제한이 없음 세 가지 컴포넌트가 핵심이다. MCP(Model Context Protocol)는 AI 모델이 외부 서비스와 직접 통신할 수 있게 해주는 프로토콜이다. BigQuery MCP를 연결하면 Claude가 SQL을 생성하는 것에 그치지 않고, BigQuery에 직접 쿼리를 실행하고 결과를 받아올 수 있다. 기존 Text-to-SQL의 한계는 "SQL을 생성해주면 사람이 직접 실행해야 한다"는 것이었다. MCP는 이 단계를 없앤다. 자연어 질의부터 분석 결과까지 중간에 사람이 개입하지 않아도 된다. Claude의 Skill은 특정 도메인에 대한 지식과 규칙을 패키징한 컨텍스트 파일이다. tu-analytics Skill에는 다음이 담겨 있다. 스키마 정보 (schema.md) 테이블 구조, 컬럼 설명, 데이터 타입. Claude가 올바른 SQL을 생성하려면 테이블이 어떻게 생겼는지 알아야 한다. 지표 정의 (tables.md / 분석 지침) 단순한 컬럼 설명을 넘어, 비즈니스 지표의 정확한 계산식을 정의한다. 예를 들어 "객단가"는 단순 평균이 아니라 특정 필터 조건을 만족하는 행만 포함해야 한다. 이런 규칙이 없으면 SQL은 실행되지만 결과가 틀린다. 쿼리 패턴 (querypatterns.md) 자주 사용하는 분석 패턴을 템플릿화했다. 영업일 기준 동기 비교, 국내/해외 분리 집계, 누적 영업일 필터링 등 반복되는 로직을 표준화해두면 매번 처음부터 만들지 않아도 된다. 특수 케이스 (specialcases.md) 데이터의 예외 상황과 주의사항. 예를 들어 특정 컬럼에 NULL이 들어오는 경우 어떻게 처리해야 하는지, 중복 제거는 어떤 기준으로 하는지 등이 명시돼 있다. 이 Skill이 없으면 Claude는 일반적인 SQL은 만들 수 있지만, 이 데이터베이스의 맥락에 맞는 정확한 SQL은 만들기 어렵다. Claude Projects는 특정 프로젝트에 대한 지식과 설정을 지속적으로 유지할 수 있는 환경이다. tu-analytics Skill을 프로젝트에 연결해두면, 매번 컨텍스트를 다시 설명하지 않아도 된다. 대화가 길어지거나 세션이 바뀌어도 테이블 구조, 지표 정의, 분석 원칙이 유지된다. 질의 예시: "5월 국내/해외 매출 전월 동기 대비 비교해줘" 내부 처리 과정: 1. Claude가 Skill을 참조해 관련 테이블과 컬럼을 파악 2. "전월 동기"의 의미를 영업일 기준으로 해석 (달력 날짜 기준이 아님) 3. 국내/해외 분리 집계 쿼리 생성 (nationalitytype 기준) 4. BigQuery MCP로 쿼리 실행 5. 결과 반환 → 증감 계산 + 시각화 + 인사이트 해석 핵심은 3번과 4번 사이에 사람이 없다는 것이다. 결과 리포트 예시 "매출이 얼마야?"라는 질문은 단순해 보이지만, 어떤 컬럼을 쓰느냐에 따라 숫자가 달라진다. 고객납부액인지, 매출귀속액인지, 세금 포함/제외인지. 이 정의가 Skill에 명확하게 박혀 있어야 일관된 결과가 나온다. 수식 하나하나를 명문화하는 작업이 시스템 구축보다 더 시간이 걸렸다. "전월 대비"를 단순 날짜 수로 비교하면 안 된다. 영업일 수가 다른 달을 날짜 기준으로 비교하면 오해를 부른다. 이 시스템은 영업일 기준 동기 비교를 표준으로 정의하고, 모든 기간 비교에 이 원칙을 적용한다. 당월 / 전월 동기 / 직전 3개월 평균 / 전년 동기 — 네 가지 비교 기준을 항상 함께 제공하는 것도 규칙으로 정해뒀다. 분석 결과가 매번 다른 형태로 나오면 보는 사람이 불편하다. 스코어카드 → 차트 → 표 요약 → 인사이트의 순서를 표준화하고, 증감 표기 방식(▲/▼, 색상 코드)도 규칙으로 정의했다. 완벽하지 않다. LLM 기반 SQL 생성은 복잡한 조건이 중첩될수록 오류 가능성이 높아진다. 이 때문에 중요한 분석은 결과를 검증하는 습관이 필요하다. 컨텍스트 품질이 전부다. Skill에 정의된 스키마와 지표가 부정확하면 SQL도 틀린다. "쓰레기가 들어가면 쓰레기가 나온다(GIGO)"는 여기서도 그대로 적용된다. 초기 Skill 작성과 지속적인 업데이트가 시스템 품질을 결정한다. MCP는 현재 사용자 단위 인증이다. BigQuery MCP는 개인 인증 기반으로 동작한다. 조직 전체가 동일한 연결을 공유하는 구조가 아니므로, 팀 단위 배포 시에는 이 점을 고려해야 한다. SQL을 모르는 팀원도 분석 질문을 직접 던질 수 있게 됐다. 분석 요청 → 결과 확인의 사이클이 대폭 줄었고, "물어보기 애매해서 참던" 질문들이 올라오기 시작했다. 데이터 민주화라는 말이 있다. 데이터를 다루는 기술이 없어도 데이터에서 인사이트를 얻을 수 있게 되는 것. Text-to-SQL이 그 방향으로 한 걸음 가까워지게 해준 기술이라고 생각한다. 사용 기술: Claude Projects, BigQuery MCP, Model Context Protocol (MCP), Google BigQuery 관련 키워드: Text-to-SQL, NL2SQL, Conversational Analytics, AI Analytics, Data Democratization
회사 캘린더 비서 만들기 (feat. n8n, Gemini API)
2026.05.12
잔디에 말 걸면 알아서 회의실 예약해주는 AI 비서, 직접 만들어봤습니다. 우리 회사는 회의실 예약을 Google Calendar로 관리한다. 문제는 예약할 때마다 캘린더 앱을 열고, 시간대를 확인하고, 이벤트를 직접 만들어야 한다는 것. 팀원들이 회의실 예약하러 캘린더 들어가는 걸 귀찮아하는 걸 보고 생각했다. "잔디에서 그냥 말로 하면 안 되나?" 그래서 만들었다. 잔디 채팅창에 "내일 오후 2시에 대회의실 1시간 잡아줘" 라고 보내면, AI가 알아서 캘린더에 예약을 넣어주는 시스템. \[이미지: n8n 워크플로우 전체 화면\] 사용한 스택은 단순하다. n8n — 워크플로우 자동화 허브 Google Gemini — 자연어 해석 + 도구 호출 판단 Google Calendar API — 실제 예약 실행 잔디(Jandi) — 사용자 인터페이스 (채팅) 흐름은 이렇다. 잔디 메시지 → n8n Webhook → AI Agent (Gemini) → Google Calendar → 잔디 응답 AI Agent가 중심이고, 상황에 따라 4가지 Calendar 도구 중 하나를 선택해서 실행한다. | 도구 | 기능 | | --- | --- | | createconferenceroomevent | 새 회의실 예약 생성 | | updateconferenceroomevent | 기존 예약 수정 | | deleteconferenceroomevent | 예약 삭제 | | searchconferenceroom | 예약 현황 조회 | 잔디는 특정 채널에 메시지가 오면 외부 URL로 POST 요청을 보내는 Incoming/Outgoing Webhook을 지원한다. 잔디 팀 설정에서 Outgoing Webhook을 추가한다. 채널: 봇이 응답할 채널 선택 트리거 단어: 설정하지 않으면 모든 메시지에 반응 (봇 전용 채널 추천) URL: n8n Webhook URL 입력 \[이미지: 잔디 Outgoing Webhook 설정 화면\] 잔디가 보내는 POST body 구조는 다음과 같다. json { "token": "...", "teamName": "tugether", "roomName": "회의실봇", "writerName": "홍길동", "text": "내일 오후 2시에 대회의실 1시간 잡아줘", "writer": "user@company.com" } n8n에서 잔디로 응답을 보낼 때는 잔디의 Incoming Webhook URL을 사용한다. 잔디 채널 설정 → Incoming Webhook 추가 → URL 복사. 나중에 n8n에서 HTTP Request 노드로 이 URL에 POST하면 된다. json { "body": "✅ 예약 완료! 내일 오후 2~3시 대회의실을 예약했습니다." } n8n에서 새 워크플로우를 만들고 Webhook 노드를 추가한다. HTTP Method: POST Path: 원하는 경로 (예: calendar-bot) Response Mode: Respond to Webhook 노드로 분리 팁: Response Mode를 "Last Node"로 하면 AI 처리가 끝날 때까지 HTTP 연결을 물고 있어야 한다. Gemini 응답이 느릴 수 있으니 "Respond to Webhook" 노드를 별도로 두고 먼저 잔디에 응답하는 구조를 추천한다. Webhook URL을 복사해서 잔디 Outgoing Webhook에 붙여넣으면 1단계 연결 완료. n8n에서 Google Calendar 노드를 쓰려면 OAuth2 인증이 필요하다. 1. [Google Cloud Console](https://console.cloud.google.com/?ref=haas.kr) → API & Services → Credentials 2. OAuth 2.0 클라이언트 ID 생성 (웹 애플리케이션) 3. 승인된 리디렉션 URI에 n8n OAuth 콜백 URL 추가 형식: https://[n8n도메인]/rest/oauth2-credential/callback 4. 클라이언트 ID / Secret 복사 n8n → Credentials → Google Calendar OAuth2 API → 위 값 입력 후 연결. AI Agent의 Tool 포트에 Google Calendar 노드 4개를 연결한다. 각 노드 설정: create\conference\room\event Operation: Create Calendar: 회의실 캘린더 선택 나머지 필드: AI가 채워줌 (Expression으로 설정) update\conference\room\event Operation: Update Event ID: AI가 검색해서 넘겨줌 delete\conference\room\event Operation: Delete search\conference\room Operation: Get All 시간 범위: AI가 판단해서 설정 각 노드의 이름을 도구 이름과 동일하게 지정해야 AI Agent가 올바르게 인식한다. 워크플로우의 핵심. AI Agent 노드를 추가하고 설정한다. AI Agent의 Chat Model 포트에 Google Gemini Chat Model 노드를 연결한다. Model: gemini-2.0-flash (속도/비용 균형) API Key: Google AI Studio에서 발급한 Gemini API 키 AI Agent의 System Prompt가 전체 품질을 결정한다. 아래는 실제 사용 중인 프롬프트 구조다. 너는 00(test.ai)의 전문 회의실 예약 비서야. 사용자의 입력({{ $json.body.data }})과 작성자 정보({{ $json.body.writer.email }})를 바탕으로 [예약/수정/삭제] 업무를 수행해. 1. 의도 분류: 사용자의 요청이 '신규 예약', '시간/제목 수정', '예약 취소(삭제)' 중 무엇인지 판단해. 2. 시간 계산: 현재 시간({{ $now }})을 기준으로 시작/종료 시간을 ISO8601 형식으로 변환해. (타임존: Asia/Seoul) - 별도 언급 없으면 종료 시간은 시작 시간 1시간 뒤로 설정. '2시간 동안' 등 기간 언급 시 그에 맞게 계산. 3. 제목 생성: 제목 언급이 없으면 "{{ $json.body.writer.name }}님의 회의"로 자동 생성. 4. 회의실 ID 매핑: (언급 없으면 '10층 대회의실' 기본값) - 10층 대회의실/큰방: '~~8757c93@group.calendar.google.com' - 10층 소회의실/작은방: '~~b2d83@group.calendar.google.com' - 8층 대회의실/미팅룸: '~~549@group.calendar.google.com' 5. 참석자: 본문의 <@이메일 멘션을 추출해 'getmemberemails'로 이메일을 확보하고 작성자({{ $json.body.writer.email }})를 포함해. - 모든 액션 전 'searchroomschedules' 도구를 호출해 해당 날짜의 전체 일정을 가져와. - [예약/수정 시]: 요청한 시간대에 겹치는 일정이 있는지 확인해. (수정의 경우 본인 기존 일정은 제외) - [수정/삭제 시]: 기존 일정 중 사용자가 말한 시간/제목과 일치하는 'Event ID'를 찾아내. 1. 신규 예약: 중복이 없을 때만 'createroomevent' 호출. 2. 일정 수정: 'Event ID'가 확인되고 변경 시간대에 중복이 없을 때 'updateroomevent' 호출. 3. 일정 삭제: 'Event ID'가 확인되면 'deleteroomevent' 호출. 모든 응답은 친절한 한국어로 하며 아래 형식을 지켜줘. ✅ 성공 시: [예약 성공 / 수정 완료 / 삭제 완료] 되었습니다. - 회의실: [회의실 이름] - 일시: [시작시간] ~ [종료시간] - 제목: [회의 제목] - 참석자: [이름/이메일 리스트] ❌ 중복/실패 시: 요청하신 [회의실 이름]은 이미 아래 일정이 잡혀 있어 처리가 불가능합니다. [해당 날짜 전체 일정 리스트] - [시작~종료] : [제목] (이후 "다른 시간으로 도와드릴까요?"라고 마무리) AI Agent의 User Message에는 잔디에서 받은 텍스트를 넘긴다. {{ $json.body.text }} 잔디 Outgoing Webhook이 text 필드에 메시지를 담아서 보내기 때문에 이렇게 받는다. AI Agent 처리가 끝나면 Respond to Webhook 노드로 잔디에 응답을 돌려준다. Response Body: AI Agent의 출력값 {{ $json.output }} 동시에 잔디 Incoming Webhook으로도 별도 POST를 보내면 채널에 메시지가 표시된다. 잔디에서 예약 요청 메시지 보내는 화면 잔디에서 AI 응답이 돌아오는 화면 실제로 이런 요청들이 동작한다. "내일 오후 3시에 소회의실 2시간 잡아줘" → 중복 확인 후 예약 생성 "이번 주 금요일 대회의실 예약 취소해줘" → 검색 후 삭제 "다음 주 월요일 오전에 어느 회의실 비어 있어?" → 가용 시간 조회 "아까 예약한 거 4시로 바꿔줘" → 이벤트 찾아서 업데이트 잔디에서 Webhook이 안 불릴 때 잔디 Outgoing Webhook은 Public URL이 필요하다. 로컬 n8n은 Cloudflare Tunnel이나 ngrok으로 외부 노출이 필요하다. Gemini가 도구를 안 쓸 때 시스템 프롬프트에 "반드시 도구를 사용하라"는 지시를 명시적으로 추가한다. Gemini는 도구 사용 여부를 자체 판단하므로 프롬프트로 유도해야 한다. 날짜 계산이 틀릴 때 시스템 프롬프트에 {{ $now }}를 포함해서 현재 시각을 명시적으로 알려줘야 한다. 이게 없으면 Gemini가 날짜를 잘못 계산하는 경우가 생긴다. 이중 예약 문제 create 전에 반드시 search로 확인하도록 프롬프트에 강제하는 게 핵심이다. Freebusy API를 별도로 연동하면 더 정확하게 처리할 수 있다. n8n + Gemini 조합으로 생각보다 빠르게 실용적인 AI 비서를 만들 수 있었다. 코드 한 줄 없이 노드 연결만으로 자연어 처리부터 Calendar API 호출까지 완성된다는 게 n8n의 강점이다. 비슷한 구조로 HR 문의 봇, 공지사항 자동 발송, 리포트 조회 봇 등으로 확장 가능하다. 잔디 대신 Slack이나 카카오워크를 Webhook으로 연결하면 그대로 이식된다. 전체 워크플로우 JSON은 추후 공유할 예정이다.
n8n과 노션으로 구축하는 RAG 기반 사내 AI 챗봇 가이드
2026.03.18
자사에서는 업계 특성상 임직원의 입/퇴사가 잦은 편이고, 이로 인한 자세한 온보딩 여력이 되지 않는다. 그로인한 다양한 임직원의 문의는 인사팀의 업무 리소스로 이어지며, 이를 해결하기 위해 사내 AI 봇을 기획하고 만들게 되었다. [Make](https://www.make.com/en?ref=haas.kr) 와 [Zapier](https://zapier.com/?ref=haas.kr) 같이 다양한 애플리케이션 간의 자동화를 no code로 도와주는 서비스 이다. 하지만, 위 툴들과 다르게 [n8n](https://n8n.io/?ref=haas.kr)이 요즘 각광을 받는 이유는 한 가지다. 바로 '셀프호스팅'을 제공한다. 즉, 내가 DevOps 지식만 있다면, 거의 무료로 자동화 시스템을 구축 할 수 있다는 말이다. 처음에 기획 했을때는 RAG가 아닌 단순히 AI Agent 노드로 제작 하였다. 노션에 올라가 있는 데이터가 많지도 않고, 따라서 성능이슈는 없을 것으로 예상했다. AI Agent 독립사용 워크플로우 이미지 1. 노션 페이지의 블록 누락 이슈 이번에 정확히 알게된 사실인데, 노션 데이터를 json으로 받게 되면, 페이지에 그냥 있는 text의 depth와 콜아웃, 토글, 표 등의 블록의 depth가 달라 데이터를 읽어오는 과정에서 이를 모두 고려해야 한다. 그래서 위 이미지를 보면 "데이터베이스 Get - 각각 페이지 ID 추출 - 각 페이지 Get - 각 페이지에서 블록 추출" 하는 로직이 필요하다. 이 과정에서 난 모든 블록을 고려하는 것은 포기하고 표와 텍스트만 가져오기로 했다. 위 문제는 Notion Node를 이용하면 비교적 간단하게 해결이 될 수도 있지만 일일히 설정 해야 하는 것은 변함이 없다. 2. 성능 이슈(속도) 노션에서 데이터를 조회하는 노드가 4개가 있고, 이렇게 하여도 사용자가 채팅을 입력할 때마다 전체 노션 데이터를 실시간으로 조회해야 한다. 데이터가 적을 때는 큰 문제가 없었지만, 페이지가 늘어날수록 응답 지연이 눈에 띄게 증가했다. 매 요청마다 Notion API를 여러 번 호출하는 구조 자체가 근본적인 병목이었다. 위 두 가지 문제를 해결하기 위해 아키텍처를 완전히 재설계했다. 노션 데이터를 매 요청마다 가져오는 대신, 주기적으로 벡터 DB에 임베딩해두고 AI Agent가 이를 Tool로 검색하는 방식으로 전환했다. 전체 플로우 워크플로우는 크게 두 개의 독립적인 플로우로 나뉜다. Schedule Trigger → Get many database pages (DB1) → Get many database pages1 (DB2) → Delete Collection (Qdrant 컬렉션 초기화) → Merge (append) → Filter → Loop Over Items → HTTP Request (Notion 페이지 본문 조회) → Code in JavaScript (텍스트 가공) → Qdrant Vector Store (임베딩 저장) ← Embeddings Google Gemini ← Default Data Loader 주요 포인트: Schedule Trigger로 주기적(예: 매일 새벽)으로 실행된다. 실행 시작 시 Delete Collection으로 기존 벡터 데이터를 전부 삭제 후 재적재한다. 이렇게 하면 노션에서 수정/삭제된 내용이 자동으로 반영된다. 여러 노션 DB를 Merge (append)로 합쳐 단일 파이프라인으로 처리한다. Filter로 불필요한 페이지(미완성, 비공개 등)를 걸러낸다. Loop Over Items로 각 페이지를 순회하며 Notion API(HTTP Request)로 본문을 가져온다. Code in JavaScript로 블록 타입별 텍스트 추출 및 포맷팅을 처리한다. 최종적으로 Qdrant Vector Store에 Google Gemini 임베딩과 함께 저장한다. When chat message received → AI Agent ← Google Gemini Chat Model ← Simple Memory ← Qdrant Vector Store2 (Tool) ← Embeddings Google Gemini1 주요 포인트: 사용자 메시지를 받으면 AI Agent가 동작한다. Qdrant Vector Store를 Tool로 연결하여, Agent가 필요할 때만 벡터 검색을 수행한다. Simple Memory로 대화 히스토리를 유지해 멀티턴 대화가 가능하다. Notion API 호출이 전혀 없으므로 응답 속도가 획기적으로 빨라진다. | 항목 | 개선 전 (AI Agent 단독) | 개선 후 (RAG + Qdrant) | | --- | --- | --- | | 데이터 조회 시점 | 매 요청마다 실시간 | 주기적 사전 적재 | | 응답 속도 | 느림 (Notion API 다중 호출) | 빠름 (벡터 검색) | | 블록 누락 | 있음 | HTTP Request로 직접 조회하여 감소 | | 최신 데이터 반영 | 즉시 | 스케줄 주기에 따라 반영 | | 비용 | Notion API 호출 많음 | 임베딩 비용 발생 (저렴) | Notion Node 대신 HTTP Request를 사용한 이유는 유연성 때문이다. Notion API의 /blocks/{blockid}/children 엔드포인트를 직접 호출하면 블록 타입을 코드로 직접 핸들링할 수 있다. // 블록 타입별 텍스트 추출 예시 const items = $input.all(); const results = []; for (const item of items) { // HTTP Response body 추출 const body = item.json.body ?? item.json; // pageId와 pageTitle은 Loop의 현재 아이템에서 가져오기 // (Loop Over Items가 넘겨준 원본 페이지 정보) const pageId = item.json.pageId ?? item.json.id ?? body?.results?.[0]?.parent?.pageid ?? ""; const pageTitle = item.json.pageTitle ?? item.json.title ?? "Untitled"; const lastEdited = item.json.lasteditedtime ?? ""; // block results 배열 추출 const blocks = body?.results ?? []; // 각 블록에서 텍스트 추출 const textParts = []; for (const block of blocks) { const type = block.type; const blockData = block[type]; if (!blockData) continue; // richtext 배열이 있는 블록 타입들 처리 const richText = blockData.richtext ?? []; const blockText = richText .map(rt = rt.plaintext ?? rt.text?.content ?? "") .join(""); if (blockText.trim()) { // 헤딩 타입은 앞에 마크다운 표시 if (type === "heading1") textParts.push(# ${blockText}); else if (type === "heading2") textParts.push(## ${blockText}); else if (type === "heading3") textParts.push(### ${blockText}); else if (type === "bulletedlistitem") textParts.push(• ${blockText}); else if (type === "numberedlistitem") textParts.push(- ${blockText}); else textParts.push(blockText); } } const bodyText = textParts.join("\n"); results.push({ json: { text: 제목: ${pageTitle}\n\n[본문]\n${bodyText}, metadata: { pageId, pageTitle, chunkIndex: 0, source: "notion", lastEdited, } } }); } return results; 매 실행마다 컬렉션을 삭제 후 재생성하는 방식은 단순하지만 효과적이다. 업데이트/삭제 추적 로직 없이도 항상 노션과 동기화된 상태를 유지할 수 있다. 데이터 양이 많아지면 upsert 방식으로 전환을 고려할 수 있다. n8n + Notion + Qdrant + Google Gemini 조합으로 사내 AI 챗봇을 구축한 결과, 인사팀의 반복 문의 대응 부담을 크게 줄일 수 있었다. 특히 RAG 아키텍처 도입 후 응답 품질과 속도 모두 만족스러운 수준으로 개선되었다. 셀프호스팅 n8n의 가장 큰 장점은 API 키와 데이터가 외부로 나가지 않는다는 점이다. 사내 민감 정보를 다루는 HR 봇에는 이 점이 특히 중요했다.
Claude로 법무법인 마케팅 콘텐츠 자동 검토 파이프라인 만들기
2026.03.11
법무법인 마케팅을 대행하는 지인의 요청으로 콘텐츠 자동 검토 파이프라인을 만들어봤다. 목표는 단순했다. 초안 하나를 넣으면 문맥, 법적 리스크, 마케팅 관점까지 순서대로 검토하고 최종 수정본을 뽑아내는 구조다. 법무법인 마케팅 콘텐츠는 일반 블로그 글처럼 "잘 읽히면 끝"이 아니다. 최소한 세 가지 기준을 동시에 통과해야 한다. | 기준 | 확인할 내용 | | --- | --- | | 가독성 | 잠재 의뢰인이 상황과 해결 방향을 쉽게 이해할 수 있는가 | | 법적 안정성 | 변호사법, 광고 규정에 어긋날 만한 표현은 없는가 | | 마케팅 효과 | CTA, 검색 키워드, Pain Point가 충분히 살아 있는가 | 문제는 이 세 가지를 한 사람이 동시에 잘 보기 어렵다는 점이다. 카피에디터는 법률 리스크를 놓치기 쉽고, 변호사는 마케팅 표현을 지나치게 보수적으로 다듬을 수 있다. 마케터는 전환율을 높이려다가 법적으로 위험한 표현을 쓸 수 있다. 그래서 검토 관점을 하나의 프롬프트에 모두 넣는 대신, 역할별 Agent로 나누기로 했다. 파이프라인은 4단계다. input/original.md - STEP 1: Context Reviewer - STEP 2: Legal Reviewer - STEP 3: Marketing Reviewer - STEP 4: Final Editor - output/step4final/final.md 각 Agent는 이전 단계의 draft.md를 입력으로 받고, 자기 역할에 해당하는 부분만 검토한다. 중요한 원칙은 하나다. 자기 역할이 아닌 영역은 건드리지 않는다. 이 원칙을 넣지 않으면 모든 Agent가 비슷한 방향으로 글을 다시 쓰기 시작한다. 그러면 단계가 나뉘어 있어도 결과는 단일 프롬프트와 크게 다르지 않다. 첫 번째 Agent는 10년 차 카피에디터 역할이다. 법률적으로 맞는지, 마케팅 성과가 좋은지는 판단하지 않는다. 오직 글의 흐름과 읽기 편한지만 본다. 주요 검토 항목은 다음과 같다. 도입, 사례, 법적 쟁점, 해결 방향, CTA의 흐름이 자연스러운가 한 문장이 지나치게 길지 않은가 문단 간 전환이 끊기지 않는가 같은 내용을 반복하고 있지 않은가 원본의 핵심 사례나 사실관계를 훼손하지 않았는가 출력은 두 파일로 나눴다. output/step1context/ ├── feedback.md └── draft.md feedback.md에는 문제점과 수정 방향을 남기고, draft.md에는 실제 수정된 본문을 저장한다. 두 번째 Agent는 법률 광고 리스크를 보는 역할이다. STEP 1에서 정리된 흐름은 최대한 유지하고, 위험한 표현만 잡는다. | 유형 | 위험한 표현 | 대체 방향 | | --- | --- | --- | | 결과 보장 | "반드시 승소", "100% 해결" | "유리한 결과를 이끌어낸 경험" | | 최상급 표현 | "국내 최고", "업계 1위" | "풍부한 경험", "다수의 유사 사건 처리" | | 비용 오해 | "무조건 전액 환급" | "사안에 따라 비용 상담 가능" | 여기서 중요한 건 모든 문장을 무조건 약하게 만들지 않는 것이다. 법적으로 문제가 될 수 있는 표현은 줄이되, 허용되는 범위 안에서는 설득력을 최대한 유지하도록 지시했다. 세 번째 Agent는 디지털 마케터 역할이다. STEP 2에서 설정된 법적 한계선을 넘지 않는 조건으로 전환율을 높이는 방향을 찾는다. 주요 검토 항목은 다음과 같다. 잠재 의뢰인의 Pain Point가 충분히 드러나는가 CTA가 구체적이고 부담스럽지 않은가 SEO 키워드가 자연스럽게 들어가 있는가 블로그, SNS, 웹사이트 중 어느 채널에 적합한 톤인가 제목과 소제목이 검색 의도에 맞는가 법리 검토 이후에는 표현이 다소 밋밋해질 수 있다. 이 단계에서는 다시 마케팅 관점에서 힘을 실어준다. 단, 결과 보장처럼 법적으로 민감한 표현은 다시 살리지 않는다. 마지막 Agent는 앞선 세 단계의 결과를 조율한다. 역할은 단순한 문장 교정이 아니다. 문맥, 법리, 마케팅 의견이 충돌할 때 최종 판단을 내리는 편집자다. 우선순위는 이렇게 정했다. | 충돌 상황 | 판단 기준 | | --- | --- | | 마케팅 효과 vs 법적 제약 | 법적 제약을 우선하고, 허용 범위 안에서 마케팅 효과를 최대화 | | 문맥 흐름 vs 마케팅 강조 | 독자 경험을 우선 | | SEO 키워드 vs 자연스러운 문장 | 자연스러운 문장을 우선 | 최종 결과물에는 본문뿐 아니라 운영에 필요한 정보도 같이 넣었다. 최종 제목 제목 후보 2~3개 단계별 주요 변경사항 추천 게재 채널 SEO 키워드 후속 콘텐츠 아이디어 Claude Code에서 바로 돌릴 수 있도록 파일 구조를 단순하게 만들었다. project/ ├── CLAUDE.md ├── agents/ │ ├── agent1context.md │ ├── agent2legal.md │ ├── agent3marketing.md │ └── agent4final.md ├── input/ │ └── original.md └── output/ ├── step1context/ │ ├── feedback.md │ └── draft.md ├── step2legal/ │ ├── feedback.md │ └── draft.md ├── step3marketing/ │ ├── feedback.md │ └── draft.md └── step4final/ └── final.md 핵심은 CLAUDE.md다. 여기에 전체 실행 순서, 각 Agent의 역할, 입출력 경로, 금지 행동을 적어둔다. Claude Code는 프로젝트 디렉토리의 CLAUDE.md를 먼저 읽기 때문에, 실행할 때마다 같은 기준으로 파이프라인을 돌릴 수 있다. 실행 요청은 이렇게 단순하게 만들었다. input/original.md 파일을 읽고 CLAUDE.md의 파이프라인대로 STEP 1부터 STEP 4까지 순차적으로 실행해줘. 각 단계마다 결과 파일을 저장하고 완료 여부를 알려줘. 이 파이프라인은 병렬 실행하면 안 된다. STEP 2는 STEP 1의 draft.md를 기준으로 검토해야 하고, STEP 3은 STEP 2에서 정리한 법적 한계선을 지켜야 한다. STEP 4는 앞선 세 단계의 결과를 모두 읽어야 한다. 겉으로는 네 명의 전문가가 독립적으로 검토하는 구조처럼 보이지만, 실제로는 순차 편집 파이프라인에 가깝다. 그래서 CLAUDE.md에 다음 원칙을 명시했다. 각 단계는 반드시 순차 실행한다. 이전 단계의 draft.md가 생성되기 전에는 다음 단계를 시작하지 않는다. 병렬 실행하지 않는다. 처음에는 하나의 프롬프트로 문맥, 법리, 마케팅 검토를 모두 시켰다. 결과는 애매했다. 법리 검토는 얕고, 마케팅 표현은 약하고, 문맥 수정은 거의 없었다. 역할을 나누니 결과가 훨씬 안정적이었다. | 항목 | 단일 프롬프트 | 4단계 파이프라인 | | --- | --- | --- | | 문맥 정리 | 부분 수정에 그침 | 글 전체 흐름 기준으로 정리 | | 법리 검토 | 위험 표현 일부만 탐지 | 표현 유형별로 체계적 검토 | | 마케팅 보강 | 일반적인 조언 위주 | CTA, SEO, Pain Point를 분리해 개선 | | 충돌 조율 | 기준이 불명확 | Final Editor가 우선순위에 따라 판단 | | 재현성 | 실행할 때마다 편차 큼 | 같은 구조로 반복 실행 가능 | 멀티에이전트 구조의 핵심은 Agent 수를 늘리는 게 아니었다. 중요한 건 역할과 금지 행동을 명확히 나누는 것이다. 특히 법무법인 마케팅처럼 표현의 강도와 리스크가 계속 충돌하는 콘텐츠는 한 번에 다 고치려 하면 결과가 흐려진다. 문맥, 법리, 마케팅, 최종 조율을 분리하니 각 단계의 판단 근거가 더 선명해졌다. Claude Code와 로컬 파일 구조만으로도 이런 검토 파이프라인은 충분히 만들 수 있다. 외부 SaaS를 붙이지 않아도 되고, 단계별 결과가 파일로 남기 때문에 나중에 어떤 판단으로 문장이 바뀌었는지도 추적하기 쉽다.