개요와 준비물
1단계가 끝나면 아래 구조가 돌아갑니다. 데이터의 원본은 집 PC(B안)에 있고, VPS는 "일꾼"만 담당하므로 나중에 크레딧이 소진되어도 VPS를 갈아치우거나 집으로 옮기기 쉽습니다.
Lightsail VPS · 서울
- n8n (Docker) — 수집 스케줄러
- 수집기 3종: RSS·HN·arXiv
- 실행 이력 자동 프루닝 (7일)
- 공개 포트: SSH(22)뿐
집 PC · Windows (24h)
- PostgreSQL — 리서치 DB 원본
- 수집 데이터 upsert 저장
- 공인망 노출 없음 (Tailscale만)
- WSL·OpenClaw는 4단계에서 합류
디스코드는 이 두 머신 밖에 있는 "사무실"입니다. n8n이 새로 수집한 항목만 #피드 채널로 발송하고, 파이프라인 장애는 #시스템-알림으로 들어옵니다.
준비물 체크리스트
- AWS 계정 + 보유 크레딧이 Lightsail에 적용되는지 확인 (Billing 콘솔 → Credits → 크레딧 상세의 적용 가능 서비스 목록). 적용 불가면 이 가이드의 1단계만 EC2로 대체하면 되고 나머지는 동일합니다.
- 디스코드 계정 (서버는 6단계에서 새로 만듭니다)
- Tailscale 계정 (구글/깃허브/MS 계정으로 무료 가입 —
tailscale.com) - 집 PC 관리자 권한 (PostgreSQL 설치, 방화벽 규칙 추가에 필요)
- 집 PC 절전 모드 해제 상태 확인 (이미 24시간 운용 중이면 통과)
각 단계 제목 아래 "작업 위치"가 표시됩니다. VPS는 Lightsail SSH 터미널, 집 PC는 Windows, 브라우저는 웹 콘솔 작업입니다. 코드 블록의 <꺾쇠> 부분은 본인 값으로 교체하세요.
Lightsail 인스턴스 생성
- AWS 콘솔에서 Lightsail 검색 → Lightsail 콘솔로 이동 → 인스턴스 생성(Create instance).
- 리전: 서울(ap-northeast-2). 집 PC와의 왕복 지연을 최소화합니다.
- 플랫폼: Linux/Unix → OS 전용(OS Only) → Ubuntu 24.04 LTS.
- 플랜: 퍼블릭 IPv4 포함 $7/월 (1GB RAM · 2 vCPU · 40GB SSD). DB를 집에 두는 B안이므로 n8n만 돌리기에 충분합니다. 무거워지면 나중에 스냅샷으로 상위 플랜 이전이 가능합니다.
- 인스턴스 이름을
vps-n8n으로 지정 → 생성. - 생성 완료 후 네트워킹(Networking) 탭 → 고정 IP 생성(Create static IP) → 인스턴스에 연결. (연결된 고정 IP는 무료입니다)
- 같은 네트워킹 탭의 IPv4 방화벽에서 기본으로 열려 있는 HTTP(80) 규칙을 삭제하고 SSH(22)만 남깁니다.
이후 설치할 n8n(5678 포트)은 Lightsail 방화벽에서 절대 열지 않습니다. n8n 접속은 전부 Tailscale 사설망으로만 합니다. 공개 포트가 22 하나뿐이면 공격 표면이 최소화됩니다.
접속은 콘솔의 브라우저 SSH 버튼을 써도 되고, 로컬 터미널을 쓰려면 계정 페이지(Account → SSH Keys)에서 기본 키를 내려받아 접속합니다.
chmod 600 ~/Downloads/LightsailDefaultKey-ap-northeast-2.pem ssh -i ~/Downloads/LightsailDefaultKey-ap-northeast-2.pem ubuntu@<고정IP>
sudo apt-get update && sudo apt-get upgrade -y
Tailscale 사설망 구성
VPS와 집 PC를 하나의 사설망(100.x.x.x 대역)으로 묶습니다. 이후 모든 머신 간 통신(n8n → PostgreSQL)은 이 망을 통해서만 이뤄지고, 집 공유기의 포트포워딩은 일절 필요 없습니다.
2-1VPS에 설치
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# 출력되는 URL을 브라우저로 열어 Tailscale 계정으로 로그인/승인2-2집 PC(Windows)에 설치
tailscale.com/download/windows에서 설치 파일을 받아 설치합니다.- 트레이 아이콘 → 로그인 → VPS와 같은 계정으로 로그인.
- Tailscale은 Windows 서비스로 등록되어 재부팅 후 자동 실행됩니다.
2-3관리 콘솔에서 정리
login.tailscale.com/admin/machines접속 → 두 머신이 보이는지 확인.- 이름을 알아보기 쉽게 변경: VPS →
vps-n8n, 집 PC →home-pc(머신 우측 ⋯ 메뉴 → Edit machine name). - 두 머신 모두 ⋯ 메뉴 → Disable key expiry 설정. 이걸 안 하면 몇 달 뒤 키 만료로 사설망이 조용히 끊깁니다.
- 각 머신의 Tailscale IP(100.x.x.x)를 메모합니다. 특히
home-pc의 IP는 이후 계속 사용합니다.
2-4연결 검증
tailscale status # home-pc가 목록에 보여야 함 tailscale ping home-pc # pong 응답이 오면 성공
관리 콘솔 → DNS 탭에서 MagicDNS가 켜져 있으면(기본값) IP 대신 home-pc라는 이름으로 접속할 수 있습니다. 다만 Docker 컨테이너 내부에서는 이름 해석이 안 될 수 있으므로, n8n 설정에는 100.x.x.x IP를 직접 쓰는 것을 권장합니다. Tailscale IP는 머신이 망에 남아 있는 한 바뀌지 않습니다.
집 PC에 PostgreSQL 설치
합의한 대로 WSL 내부가 아닌 Windows 네이티브로 설치합니다. WSL2에 두면 재부팅 때마다 내부 IP가 바뀌어 포트포워딩이 끊기는 고전적인 함정이 있는데, 네이티브 설치는 이 문제가 없고 Windows 서비스로 자동 시작됩니다.
3-1설치
postgresql.org/download/windows→ EDB 인스톨러 다운로드 (PostgreSQL 17.x 권장).- 설치 진행. 구성 요소는 기본값(pgAdmin 포함) 그대로 두면 됩니다. Stack Builder는 체크 해제해도 무방합니다.
- 설치 중 묻는 postgres 슈퍼유저 비밀번호를 강력하게 정하고 안전한 곳에 기록합니다.
- 포트는 기본 5432 유지.
3-2외부(사설망) 접속 허용 설정
설정 파일 두 개를 수정합니다. 위치는 보통 C:\Program Files\PostgreSQL\17\data\ 입니다. 메모장을 관리자 권한으로 실행해서 여세요.
listen_addresses = '*'
# Tailscale 사설망(100.64.0.0/10)에서 research DB 접속만 허용
host research n8n_app 100.64.0.0/10 scram-sha-256listen_addresses='*'로 열어도, ① Windows 방화벽이 Tailscale 대역 외 접속을 차단하고(3-4), ② pg_hba가 Tailscale 대역 + 특정 DB + 특정 유저 조합만 허용하며, ③ 애초에 공유기 뒤라 공인망에서 5432에 닿을 수 없습니다. 3중 방어입니다.
3-3서비스 재시작
Restart-Service postgresql-x64-17
Get-Service postgresql-x64-17 # Status가 Running이고 StartType이 Automatic인지 확인서비스 이름이 다르면 Get-Service *postgres*로 실제 이름을 찾으세요.
3-4Windows 방화벽 인바운드 규칙
New-NetFirewallRule -DisplayName "PostgreSQL - Tailscale only" ` -Direction Inbound -Protocol TCP -LocalPort 5432 ` -RemoteAddress 100.64.0.0/10 -Action Allow
원격 주소를 100.64.0.0/10(Tailscale 대역)으로 한정했기 때문에, 같은 집 공유기의 다른 기기조차 이 규칙으로는 접속할 수 없습니다.
3-5Windows 재부팅 대비 점검
- 설정 → Windows Update → 고급 옵션 → 사용 시간을 실제 사용 패턴에 맞게 설정해 새벽 자동 재부팅을 줄입니다. (재부팅이 발생해도 수집기는 멱등 설계라 다음 실행에서 구멍을 메꿉니다)
- 재부팅 후 PostgreSQL 서비스와 Tailscale이 자동 복귀하는지 한 번 실제 재부팅으로 확인해 두면 좋습니다.
DB · 스키마 생성
시작 메뉴에서 SQL Shell (psql)을 실행합니다. Server/Database/Port/Username은 전부 엔터(기본값), 비밀번호는 3-1에서 정한 postgres 비밀번호를 입력합니다. pgAdmin의 Query Tool을 써도 동일합니다.
4-1전용 계정과 데이터베이스
CREATE ROLE n8n_app WITH LOGIN PASSWORD '여기에_강력한_비밀번호'; CREATE DATABASE research OWNER n8n_app;
이 비밀번호는 나중에 n8n 설정에 그대로 들어갑니다. 따옴표(')와 쉼표가 없는 긴 무작위 문자열로 만드세요. 슈퍼유저(postgres)가 아닌 전용 계정을 쓰는 이유는, n8n이 탈취되더라도 research DB 밖으로 피해가 번지지 않게 하기 위함입니다.
4-2items 테이블
먼저 \c research n8n_app 명령으로 research DB에 n8n_app 계정으로 재접속한 뒤 실행합니다.
CREATE TABLE IF NOT EXISTS items ( id BIGSERIAL PRIMARY KEY, source TEXT NOT NULL, -- 'rss:simonwillison.net', 'hackernews', 'arxiv' ... source_id TEXT NOT NULL, -- 소스 내 고유 ID (guid, objectID, arXiv id) url TEXT, title TEXT NOT NULL, summary TEXT, author TEXT, published_at TIMESTAMPTZ, collected_at TIMESTAMPTZ NOT NULL DEFAULT now(), score REAL, -- 2단계(LLM 스코어링)에서 채움 status TEXT NOT NULL DEFAULT 'new',-- new → scored → briefed → drafted ... raw JSONB, -- 원본 응답 보존 CONSTRAINT items_source_uniq UNIQUE (source, source_id) ); CREATE INDEX IF NOT EXISTS idx_items_published_at ON items (published_at DESC); CREATE INDEX IF NOT EXISTS idx_items_status ON items (status);
UNIQUE (source, source_id) 제약이 이 시스템의 심장입니다. 수집기가 같은 항목을 몇 번을 다시 만나도 ON CONFLICT DO NOTHING으로 조용히 무시되므로, PC가 몇 시간 꺼져 있었더라도 다음 실행이 알아서 구멍을 메꿉니다(멱등 수집).
4-3VPS에서 접속 검증
sudo apt-get install -y postgresql-client
psql "host=<home-pc의 Tailscale IP> port=5432 dbname=research user=n8n_app" -c "SELECT now();"
# 비밀번호 입력 후 현재 시각이 출력되면 3중 방어를 뚫고(=정상 경로로) 연결된 것실패 메시지별 원인: timeout → Windows 방화벽 규칙(3-4) 또는 Tailscale 연결(2-4) 문제. connection refused → listen_addresses 수정 누락 또는 서비스 재시작 안 함(3-2, 3-3). no pg_hba.conf entry → pg_hba 줄 추가 누락 또는 오타(3-2). 이 검증을 통과하기 전에는 다음 단계로 넘어가지 마세요.
VPS에 n8n 올리기
5-1Docker 설치
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker ubuntu
# 그룹 적용을 위해 SSH 재접속 후:
docker --version5-2docker-compose.yml 작성
mkdir -p ~/n8n && cd ~/n8n && cat > docker-compose.yml << 'EOF'
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "5678:5678"
environment:
- GENERIC_TIMEZONE=Asia/Seoul
- TZ=Asia/Seoul
- N8N_SECURE_COOKIE=false
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=20000
- N8N_DIAGNOSTICS_ENABLED=false
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
EOF
docker compose up -d환경변수 세 가지가 이 구성의 핵심입니다.
| 설정 | 이유 |
|---|---|
EXECUTIONS_DATA_PRUNE=trueEXECUTIONS_DATA_MAX_AGE=168 | 실행 이력을 7일(168시간)만 보관. 앞서 논의한 "VPS 디스크를 실제로 잡아먹는 범인"인 실행 로그를 자동 청소합니다. 이게 없으면 5분 주기 워크플로우 몇 개만으로 수 GB가 쌓입니다. |
N8N_SECURE_COOKIE=false | Tailscale IP로 HTTP 접속하면 n8n이 기본값에선 보안 쿠키 오류로 로그인을 막습니다. 사설망 전용 운용이므로 해제합니다. (공인망에 노출하지 않는 전제) |
GENERIC_TIMEZONE=Asia/Seoul | 스케줄 트리거가 한국 시간 기준으로 동작하게 합니다. 이후 "매일 아침 8시 브리핑" 같은 스케줄의 기준이 됩니다. |
5-3첫 접속과 계정 생성
- VPS의 Tailscale IP를 확인:
tailscale ip -4 - 내 PC 브라우저에서
http://<VPS의 Tailscale IP>:5678접속. (집 PC도 같은 사설망이라 바로 열립니다) - Owner 계정(이메일/비밀번호) 생성.
고정 IP(공인)로 5678에 접속하면 열리지 않아야 정상입니다. 1단계에서 Lightsail 방화벽에 22만 남겼기 때문입니다. 만약 열린다면 방화벽 설정을 다시 확인하세요.
디스코드 워크스페이스 만들기
팀의 "사무실"을 만듭니다. 채널 구조는 이후 단계까지 내다보고 미리 잡아둡니다.
6-1서버와 채널
- 디스코드 좌측 + → 직접 만들기로 개인 서버 생성 (이름 예:
리서치 데스크). - 텍스트 채널 5개 생성:
| 채널 | 용도 | 사용 시점 |
|---|---|---|
#피드 | 수집기가 새 항목을 흘려보내는 곳 | 1단계 (지금) |
#시스템-알림 | 워크플로우 장애·에러 알림 | 1단계 (지금) |
#브리핑 | 매일 아침 상위 항목 요약 브리핑 | 2단계 |
#초안-검수 | 콘텐츠 초안 승인/반려 | 3단계 |
#지시 | OpenClaw와의 대화 채널 | 4단계 |
6-2웹훅 2개 발급
1단계는 봇 없이 웹훅만으로 충분합니다(수신 전용). 봇은 승인·대화가 필요한 3~4단계에서 도입합니다.
- 서버 이름 클릭 → 서버 설정 → 연동(Integrations) → 웹후크(Webhooks) → 새 웹후크.
- 웹훅 ① 이름
수집기, 채널#피드지정 → 웹후크 URL 복사. - 웹훅 ② 이름
시스템, 채널#시스템-알림지정 → URL 복사. - 두 URL을 메모장에 보관합니다. 형식:
https://discord.com/api/webhooks/…
URL만 알면 누구나 그 채널에 글을 쓸 수 있습니다. 공개 저장소나 스크린샷에 노출하지 마세요.
n8n 공통 설정
7-1PostgreSQL 자격증명 등록
- n8n 좌측 하단 ⚙ → Credentials → Add credential → Postgres.
- 아래 값으로 저장 (이름:
research-db):
| 필드 | 값 |
|---|---|
| Host | <home-pc의 Tailscale IP> (예: 100.101.x.x) |
| Database | research |
| User | n8n_app |
| Password | 4-1에서 정한 비밀번호 |
| Port | 5432 |
| SSL | Disable (Tailscale이 이미 WireGuard 암호화를 제공) |
저장 시 n8n이 자동으로 연결 테스트를 합니다. 초록불이 뜨면 통과.
7-2장애 알림 워크플로우
수집기가 죽었는데 모르는 것이 최악의 시나리오입니다. 모든 워크플로우가 공유할 에러 알림부터 만듭니다.
- 새 워크플로우 생성, 이름
공통 - 에러 알림. - Error Trigger 노드 추가.
- HTTP Request 노드 추가, 아래처럼 설정:
| 필드 | 값 |
|---|---|
| Method | POST |
| URL | #시스템-알림 웹훅 URL (웹훅 ②) |
| Body | Send Body 켜기 → Body Content Type: JSON → Specify Body: Using JSON |
={{ JSON.stringify({
content: "🔴 **워크플로우 장애**: " + ($json.workflow?.name ?? "이름 없음")
+ "\n노드: " + ($json.execution?.lastNodeExecuted ?? "-")
+ "\n에러: " + ($json.execution?.error?.message ?? "상세 없음").slice(0, 500)
}) }}- 저장 후 활성화(Active) 토글 ON.
- 이후 만드는 모든 수집기 워크플로우에서 ⋯ 메뉴 → Settings → Error Workflow를
공통 - 에러 알림으로 지정합니다. (각 수집기 만들 때마다 반복— 잊지 마세요)
수집기 ① — RSS + GeekNews
GeekNews도 RSS를 제공하므로 별도 수집기 없이 피드 목록에 넣으면 끝납니다. 노드 6개짜리 워크플로우 하나로 모든 RSS 소스를 처리합니다.
8-1Schedule Trigger
새 워크플로우 생성(이름 수집 - RSS) → 트리거로 Schedule Trigger 선택 → Interval: Minutes, Minutes Between Triggers: 30.
8-2Code 노드 "피드 목록"
const feeds = [
{ url: 'https://news.hada.io/rss/news' }, // GeekNews
{ url: 'https://simonwillison.net/atom/everything/' }, // Simon Willison
{ url: 'https://huggingface.co/blog/feed.xml' }, // Hugging Face 블로그
{ url: 'https://openai.com/news/rss.xml' }, // OpenAI 뉴스
// 원하는 피드를 계속 추가하세요
];
return feeds.map(f => ({ json: f }));위 목록은 예시입니다. 각 URL을 브라우저로 열어 XML이 나오는지 확인한 뒤 등록하세요. 언론사·블로그의 RSS 주소는 종종 바뀝니다. 좋아하는 블로그의 RSS 주소는 보통 사이트주소/rss, /feed, /atom.xml 중 하나이거나 페이지 소스의 type="application/rss+xml" 링크에서 찾을 수 있습니다.
8-3RSS Read 노드
- 노드 추가: RSS Read → URL 필드에 표현식
{{ $json.url }}입력 (필드 좌측 토글을 Expression으로). - 노드 Settings 탭 → On Error: Continue (using error output 아님, 일반 Continue). 피드 하나가 죽어도 전체 수집이 멈추지 않게 합니다.
8-4Code 노드 "정규화"
어떤 피드에서 왔든 동일한 형태로 변환하고, 전체를 하나의 배치(JSON 문자열)로 묶습니다. 소스 이름은 링크의 도메인에서 자동 추출합니다.
const rows = [];
for (const item of $input.all()) {
const j = item.json;
const link = j.link || j.url || '';
let host = 'unknown';
try { host = new URL(link).hostname.replace(/^www\./, ''); } catch (e) {}
const sourceId = j.guid || j.id || link;
if (!sourceId || !j.title) continue;
rows.push({
source: 'rss:' + host,
source_id: String(sourceId).slice(0, 500),
url: link || null,
title: String(j.title).trim().slice(0, 500),
summary: String(j.contentSnippet || j.summary || '').trim().slice(0, 500),
author: j.creator || j.author || null,
published_at: j.isoDate || j.pubDate || null,
raw: JSON.stringify(j).slice(0, 20000),
});
}
return [{ json: { batch: JSON.stringify(rows) } }];8-5Postgres 노드 "저장 (upsert)"
- 노드 추가: Postgres → Credential:
research-db→ Operation: Execute Query. - Query에 아래 SQL 입력:
INSERT INTO items (source, source_id, url, title, summary, author, published_at, raw)
SELECT source, source_id, url, title, summary, author,
NULLIF(published_at, '')::timestamptz,
raw::jsonb
FROM jsonb_to_recordset($1::jsonb)
AS t(source text, source_id text, url text, title text, summary text,
author text, published_at text, raw text)
ON CONFLICT (source, source_id) DO NOTHING
RETURNING id, source, title, url;- Options → Query Parameters 추가 → 값에 표현식
={{ [$json.batch] }}입력. 대괄호가 핵심입니다 — 문자열이 아닌 배열로 넘겨야 n8n이 값 안의 쉼표를 파라미터 구분자로 오인해 쪼개는 알려진 버그를 피합니다.
배치 전체를 파라미터 하나($1)로 넘겨 SQL 인젝션과 쉼표 문제를 원천 차단하고, DB가 jsonb_to_recordset으로 풀어서 넣습니다. RETURNING은 실제로 새로 들어간 행만 돌려주므로, 이 노드의 출력이 곧 "디스코드에 알릴 새 소식 목록"이 됩니다. 이미 있던 항목이면 출력이 0건이고, n8n은 입력이 없는 후속 노드를 실행하지 않으므로 조용히 끝납니다.
8-6Code 노드 "디스코드 임베드"
디스코드 웹훅은 메시지당 임베드 10개 제한이 있어 10개 단위로 쪼갭니다.
const items = $input.all().map(i => i.json);
const embeds = items.map(it => ({
title: String(it.title).slice(0, 240),
url: it.url || undefined,
description: '`' + it.source + '`',
color: 0x0F6E6B,
}));
const out = [];
for (let i = 0; i < embeds.length; i += 10) {
out.push({ json: { body: { embeds: embeds.slice(i, i + 10) } } });
}
return out;8-7HTTP Request 노드 "#피드 발송"
| 필드 | 값 |
|---|---|
| Method / URL | POST / #피드 웹훅 URL (웹훅 ①) |
| Body | Send Body → JSON → Using JSON → ={{ JSON.stringify($json.body) }} |
| Options → Batching | Items per Batch: 1, Batch Interval(ms): 1500 — 디스코드 레이트리밋(429) 회피 |
8-8마무리와 첫 실행
- 워크플로우 Settings → Error Workflow를
공통 - 에러 알림으로 지정. - 우측 상단 Execute Workflow로 수동 실행 → 각 노드에 초록 체크가 붙고
#피드에 카드가 올라오는지 확인. - 한 번 더 수동 실행 → 이번에는 Postgres 노드 출력이 0건이고 디스코드에 아무것도 안 올라와야 정상입니다(멱등성 검증).
- Active 토글 ON — 이제 30분마다 자동으로 돕니다.
수집기 ② — Hacker News
HN은 무료 공개 검색 API(Algolia)를 씁니다. 최근 24시간 내 등록되고 80포인트를 넘긴 글 중 AI 관련 키워드에 걸리는 것만 가져옵니다. 워크플로우 목록에서 수집 - RSS의 ⋯ 메뉴 → Duplicate로 복제한 뒤(이름 수집 - HackerNews), 앞쪽 노드 3개만 교체하면 됩니다. 저장(upsert)·임베드·발송 노드는 그대로 재사용됩니다.
9-1Schedule Trigger 수정
Interval: Hours / 1. HN은 30분보다 1시간이 적당합니다(포인트가 쌓일 시간이 필요).
9-2"피드 목록" 노드를 "검색 URL"로 교체
const since = Math.floor(Date.now() / 1000) - 60 * 60 * 24; // 24시간 전
const terms = ['AI', 'LLM', 'GPT', 'Claude', 'Gemini', 'open source model'];
return terms.map(q => ({ json: {
url: 'https://hn.algolia.com/api/v1/search'
+ '?tags=story'
+ '&query=' + encodeURIComponent(q)
+ '&numericFilters=' + encodeURIComponent('points>80,created_at_i>' + since)
+ '&hitsPerPage=30'
}}));키워드와 포인트 기준(80)은 취향껏 조정하세요. 기준을 낮추면 양이 늘고 소음도 늘어납니다.
9-3RSS Read 노드를 HTTP Request로 교체
- HTTP Request: Method
GET, URL에 표현식{{ $json.url }}. - Settings 탭 → On Error: Continue.
9-4"정규화" 노드 내용 교체
const rows = [];
const seen = new Set(); // 키워드끼리 겹친 글 제거
for (const item of $input.all()) {
for (const h of (item.json.hits || [])) {
if (!h.objectID || seen.has(h.objectID)) continue;
seen.add(h.objectID);
rows.push({
source: 'hackernews',
source_id: String(h.objectID),
url: h.url || ('https://news.ycombinator.com/item?id=' + h.objectID),
title: String(h.title || '(no title)').slice(0, 500),
summary: '▲ ' + (h.points ?? 0) + ' points · 💬 ' + (h.num_comments ?? 0),
author: h.author || null,
published_at: h.created_at || null,
raw: JSON.stringify(h).slice(0, 20000),
});
}
}
return [{ json: { batch: JSON.stringify(rows) } }];9-5임베드 색만 구분 (선택)
재사용 중인 "디스코드 임베드" 노드의 color를 0xE8890C(주황)로 바꾸면 #피드에서 소스가 한눈에 구분됩니다.
9-6마무리
- Settings → Error Workflow 지정 확인(복제 시 승계되지만 한 번 확인).
- 수동 실행 2회로 동작·멱등성 검증 → Active ON.
수집기 ③ — arXiv
arXiv 공식 API로 cs.AI(인공지능)·cs.CL(전산언어학)·cs.LG(머신러닝) 카테고리의 최신 제출 논문을 가져옵니다. arXiv는 하루 한 번 발표되므로 스케줄도 하루 한 번이면 충분합니다.
10-1Schedule Trigger
수집 - RSS를 복제(이름 수집 - arXiv) 후, 트리거를 Interval: Days / 1, Trigger at Hour: 11(오전, KST 기준)로 설정. "피드 목록" Code 노드는 삭제합니다.
10-2HTTP Request 노드
| 필드 | 값 |
|---|---|
| Method | GET |
| URL | http://export.arxiv.org/api/query?search_query=cat:cs.AI+OR+cat:cs.CL+OR+cat:cs.LG&start=0&max_results=60&sortBy=submittedDate&sortOrder=descending |
| Options → Response | Response Format: Text (Atom XML을 문자열로 받음), Put Output in Field: data |
10-3XML 노드
노드 추가: XML → Mode: XML to JSON → Property Name: data. Atom XML이 JSON으로 변환됩니다.
10-4"정규화" 노드 내용 교체
const feed = $input.first().json.feed || {};
let entries = feed.entry || [];
if (!Array.isArray(entries)) entries = [entries]; // 결과 1건이면 객체로 옴
const rows = [];
for (const e of entries) {
const idUrl = typeof e.id === 'string' ? e.id : '';
const arxivId = idUrl.split('/abs/')[1] || idUrl;
if (!arxivId) continue;
const authors = Array.isArray(e.author)
? e.author.map(a => a.name).slice(0, 5).join(', ')
: (e.author && e.author.name) || null;
rows.push({
source: 'arxiv',
source_id: String(arxivId).slice(0, 500),
url: idUrl,
title: String(e.title || '').replace(/\s+/g, ' ').trim().slice(0, 500),
summary: String(e.summary || '').replace(/\s+/g, ' ').trim().slice(0, 500),
author: authors,
published_at: e.published || null,
raw: JSON.stringify(e).slice(0, 20000),
});
}
return [{ json: { batch: JSON.stringify(rows) } }];하루 60건이 부담스러우면 지금은 그대로 두세요. 2단계에서 LLM 스코어링이 붙으면 상위 항목만 브리핑으로 올라오고 #피드는 참고용 로그가 됩니다. 당장 줄이고 싶다면 max_results를 낮추거나 카테고리를 줄이면 됩니다.
10-5마무리
임베드 색 0x8E44AD(보라) 권장 → Error Workflow 확인 → 수동 실행 검증 → Active ON.
부록 — Reddit 소스 추가 (선택)
서브레딧은 URL 뒤에 .rss만 붙이면 피드가 됩니다. 다만 Reddit은 기본 User-Agent를 자주 차단(429)하므로, RSS Read 노드 대신 HTTP Request(+ User-Agent 헤더) → XML 노드 조합이 안정적입니다. 우선은 가장 간단한 방법부터: 수집 - RSS의 피드 목록에 아래를 추가하고 며칠 지켜보세요. 429가 반복되면 그때 HTTP 방식으로 전환하면 됩니다.
{ url: 'https://www.reddit.com/r/LocalLLaMA/top/.rss?t=day' },
{ url: 'https://www.reddit.com/r/MachineLearning/top/.rss?t=day' },검증 체크리스트 · 트러블슈팅
완료 체크리스트
- VPS에서
tailscale ping home-pc가 pong을 반환한다 - VPS에서 psql로 research DB에 접속해
SELECT now();가 실행된다 - 공인 IP의 5678 포트로는 n8n이 열리지 않는다 (Tailscale IP로만 열린다)
- 수집기 3개(RSS·HN·arXiv)가 모두 Active 상태다
- 같은 수집기를 연속 2회 실행하면 두 번째엔 #피드에 아무것도 올라오지 않는다 (멱등성)
- 워크플로우 하나에서 고의로 에러를 내면(예: 웹훅 URL 한 글자 수정) #시스템-알림에 🔴 메시지가 온다 — 확인 후 원복
- 집 PC를 재부팅해도 PostgreSQL·Tailscale이 자동 복귀하고, 다음 수집 주기에 파이프라인이 정상 동작한다
- DB에 데이터가 쌓인다: psql에서
SELECT source, count(*) FROM items GROUP BY 1;
트러블슈팅
| 증상 | 원인과 해결 |
|---|---|
| n8n 로그인 시 secure cookie 오류 | docker-compose에 N8N_SECURE_COOKIE=false 누락. 추가 후 docker compose up -d 재실행. |
| Postgres 노드 timeout | 순서대로 점검: tailscale status(양쪽) → Windows 방화벽 규칙(3-4) → PostgreSQL 서비스 Running 여부. Tailscale 무료 플랜은 장기간 유휴 시 재로그인이 필요할 수 있으니 트레이 아이콘 상태 확인. |
Postgres 노드 invalid input syntax for type timestamptz | 일부 피드의 날짜 형식이 비표준. 정규화 코드에서 해당 소스의 published_at을 null로 두고 collected_at만 사용하도록 수정. |
| 디스코드에 429 (rate limited) | HTTP Request의 Batching Interval을 2500ms로 증가. 첫 실행은 쌓인 항목이 많아 발생할 수 있으며 이후엔 드뭅니다. |
| 특정 RSS 피드만 계속 실패 | RSS Read의 On Error: Continue라 전체엔 지장 없음. 피드 URL을 브라우저로 열어 생존 확인 후 목록에서 제거/교체. |
| 스케줄이 이상한 시간에 돎 | GENERIC_TIMEZONE=Asia/Seoul 확인 + n8n Settings의 워크플로우별 타임존 확인. |
| VPS 디스크 점검 | df -h로 확인. 실행 이력은 7일 프루닝이 걸려 있어 평시엔 여유. 도커 이미지 청소는 docker system prune -f. |
여기까지 완성되면
이제 잠자는 동안에도 GeekNews·해외 블로그·HN·arXiv가 집 PC의 DB에 쌓이고, 새 소식은 디스코드 #피드에 실시간으로 흐릅니다. 시스템이 죽으면 #시스템-알림이 먼저 알려줍니다. 여기까지가 "정보 수집 팀"의 완성입니다.
2단계에서는 이 위에 판단력을 얹습니다 — 저가 LLM이 매 항목을 요약·태깅·스코어링하고(items의 score·status 컬럼이 채워지기 시작합니다), 매일 아침 8시 상위 항목만 추린 모닝 브리핑이 #브리핑 채널로 발송됩니다. 준비되면 2단계 가이드를 요청하세요.