Files
hufs-notice-crawler/README.n8n.md
nkey ca460453af
All checks were successful
hufs-notice-crawler-cicd / build_push_deploy (push) Successful in 8m35s
Feat: [main] hufs-notice-crawler CI/CD까지 구현 완료
2026-03-17 17:18:16 +09:00

7.6 KiB

n8n 연동 문서

이 문서는 HUFS 컴퓨터공학부 공지 크롤러n8n과 연결해 Discord Webhook으로 알림을 보내는 방법을 설명합니다.

관련 문서:

왜 n8n을 쓰는가

현재는 Discord Webhook으로 보내더라도, 나중에 Slack이나 Telegram으로 바뀔 수 있습니다.

권장 역할 분리:

  • hufs-notice-crawler
    • 크롤링과 데이터 정규화만 담당
    • 채널별 포맷은 모름
  • n8n
    • 백엔드 JSON을 받아 Discord/Slack/Telegram 형식으로 변환

이 구조의 장점:

  • 알림 채널이 바뀌어도 백엔드 코드 변경 최소화
  • 백엔드 응답을 Discord 전용 형식으로 오염시키지 않음
  • 같은 데이터를 여러 채널로 동시에 보낼 수 있음

즉, 백엔드는 generic JSON provider, n8n은 channel adapter로 두는 것이 맞습니다.

권장 워크플로우

  1. Schedule Trigger
  2. Set
  3. HTTP Request
  4. IF - new_posts_count > 0
  5. IF - test_mode == true
  6. Code
  7. HTTP Request (Discord Webhook)

의미:

  1. 스케줄에 따라 n8n 실행
  2. test_mode를 켜거나 끔
  3. hufs-notice-crawler 호출
  4. 새 글이 있는지 확인
  5. 새 글이 없으면 test mode 여부 확인
  6. 상황에 맞는 Discord 메시지 포맷 생성
  7. Webhook 전송

노드별 설정

1. Schedule Trigger

예시 cron:

0 10,14,18 * * *

의미:

  • 매일 10:00, 14:00, 18:00 실행

2. Set

필드:

  • test_mode
    • Boolean
    • true: 테스트 기간
    • false: 운영 모드

의미:

  • true
    • 새 글이 0개일 때도 "업데이트 없음 + 게시판별 최신 글" 메시지 전송
  • false
    • 새 글이 0개면 아무 메시지도 보내지 않음

3. HTTP Request

역할:

  • 백엔드 API 호출

권장 설정:

  • Method: POST
  • Response Format: JSON

URL:

  • 내부 Docker network 직접 호출
    • http://hufs-notice-crawler:8000/api/v1/crawl
  • nginx reverse proxy 경유 호출 -> nginx에 로그를 모으기 위해 채택
    • https://nkeystudy.site/api/hufs/crawl

백엔드 응답에서 중요한 필드

  • bootstrap_mode
    • 최초 실행 bootstrap 여부
  • bootstrap_inserted_count
    • bootstrap 시 저장된 글 수
  • new_posts_count
    • 실제 신규 글 수
  • new_posts
    • 신규 글 목록
  • latest_posts_by_board
    • 게시판별 최신 글
    • new_posts_count == 0일 때만 포함
    • 별도 추가 요청이 아니라 실제 크롤링 결과 재사용

IF 분기

IF 1: 새 글 여부

조건:

  • Left Value: {{ $json.new_posts_count }}
  • Operation: larger
  • Right Value: 0

분기:

  • True
    • 새 글 알림용 Code 노드로 이동
  • False
    • test_mode 확인용 IF 노드로 이동

IF 2: test mode 여부

조건:

  • Left Value: {{ $('{Set노드의 이름}').item.json.test_mode }}
  • Operation: is true

분기:

  • True
    • "업데이트 없음" 테스트 메시지 전송
  • False
    • 아무 메시지 없이 종료

Code 노드 중요 사항

이 문서의 Code 노드 예시는 Run Once for All Items 기준입니다.

이유:

  • HTTP Request 응답 1개 안에 new_posts 배열이 들어 있음
  • 이를 여러 Discord 메시지 item으로 펼쳐야 함

즉 Code 노드에서:

  • Mode = Run Once for All Items

를 권장합니다.

Run Once for Each Item에서는 return [] 또는 map(...)으로 여러 item을 반환할 때 에러가 날 수 있습니다.

Code 예시 1: 새 글 알림용 Discord Embed

const data = $input.first().json;

if (!data.new_posts || data.new_posts.length === 0) {
  return [];
}

return data.new_posts.map((post) => {
  const publishedAt = post.published_at ?? "날짜 없음";
  const author = post.author ?? "작성자 없음";
  const summary = post.summary ?? "요약 없음";
  const attachments = (post.attachments || [])
    .map((file) => `- [${file.name}](${file.url})`)
    .join("\n");

  const descriptionParts = [
    `게시판: ${post.board_name}`,
    `작성자: ${author}`,
    `작성일: ${publishedAt}`,
    "",
    `요약: ${summary}`,
  ];

  if (attachments) {
    descriptionParts.push("", "첨부파일:", attachments);
  }

  return {
    json: {
      discordPayload: {
        embeds: [
          {
            title: post.title,
            url: post.post_url,
            description: descriptionParts.join("\n").slice(0, 4000),
            color: 3447003,
            footer: {
              text: `article_id: ${post.article_id}`,
            },
            timestamp: post.published_at ?? undefined,
          },
        ],
      },
    },
  };
});

Code 예시 2: 새 글 있을 때 role mention 태그

const roleMention = "<@&123456789012345678>";
const data = $input.first().json;

if (!data.new_posts || data.new_posts.length === 0) {
  return [];
}

return data.new_posts.map((post) => ({
  json: {
    discordPayload: {
      content: `${roleMention} 새 글이 올라왔습니다.`,
      embeds: [
        {
          title: post.title,
          url: post.post_url,
          description: `게시판: ${post.board_name}\n작성일: ${post.published_at ?? "날짜 없음"}`,
          color: 3447003,
        },
      ],
    },
  },
}));

역할 ID만 실제 Discord 서버 값으로 바꾸면 됩니다.

Code 예시 3: 업데이트 없음 테스트 메시지

이 코드는:

  • new_posts_count == 0
  • test_mode == true

일 때만 실행하는 용도입니다.

const data = $input.first().json;
const latestPosts = data.latest_posts_by_board || [];

const lines = ["현재 새로 업데이트된 공지사항은 없습니다."];

if (data.bootstrap_mode) {
  lines.push(
    `초기 bootstrap 저장이 수행되었습니다. 저장된 글 수: ${data.bootstrap_inserted_count}`,
  );
}

if (latestPosts.length > 0) {
  lines.push("", "게시판별 가장 최근 글:");
  for (const post of latestPosts) {
    lines.push(`- [${post.board_name}] ${post.title}`);
    lines.push(`  ${post.post_url}`);
  }
}

return [
  {
    json: {
      discordPayload: {
        content: lines.join("\n"),
      },
    },
  },
];

운영 모드에서는 이런 메시지를 보내지 않는 것을 권장합니다.

Discord Webhook 노드

권장 설정:

  • Method: POST
  • URL: Discord Webhook URL
  • Send Body: Using JSON
  • Body:
{{ $json.discordPayload }}

주의:

  • Code 노드가 여러 item을 반환하면 Discord Webhook 노드는 게시글 수만큼 여러 번 실행됩니다.

test mode 권장 동작

  • 운영 모드 (test_mode = false)

    • 새 글 있을 때만 전송
    • 0개일 때는 전송 안 함
  • 테스트 모드 (test_mode = true)

    • 새 글 있으면 전송
    • 새 글 0개여도 "업데이트 없음" 메시지 전송
    • 게시판별 최신 글 표시
    • bootstrap이면 bootstrap 정보도 함께 표시 가능

bootstrap과 n8n

최초 실행 시:

  • bootstrap_mode = true
  • bootstrap_inserted_count > 0
  • new_posts_count = 0

즉, 기존 글은 DB에 저장되지만 new_posts로 반환되지 않습니다.

그래서:

  • 첫 실행에서 예전 글 알림이 쏟아지지 않음
  • test mode가 켜져 있으면 "업데이트 없음" 메시지로 상태만 확인 가능

나중에 Slack으로 바뀌면

바뀌는 것:

  • Webhook URL
  • Code 노드의 메시지 포맷
  • 마지막 전송 노드

안 바뀌는 것:

  • hufs-notice-crawler 응답 구조
  • 크롤링 로직
  • 신규 글 판별 로직

즉 채널 변경 비용을 n8n 수정으로 제한할 수 있습니다.