Feat: [main] hufs-notice-crawler CI/CD까지 구현 완료
All checks were successful
hufs-notice-crawler-cicd / build_push_deploy (push) Successful in 8m35s
All checks were successful
hufs-notice-crawler-cicd / build_push_deploy (push) Successful in 8m35s
This commit is contained in:
342
README.n8n.md
Normal file
342
README.n8n.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# n8n 연동 문서
|
||||
|
||||
이 문서는 `HUFS 컴퓨터공학부 공지 크롤러`를 `n8n`과 연결해 Discord Webhook으로 알림을 보내는 방법을 설명합니다.
|
||||
|
||||
관련 문서:
|
||||
|
||||
- 서비스 개요: [`README.md`](/C:/Users/USER/Desktop/notice_crawler/README.md)
|
||||
- 운영/배포: [`README.operation.md`](/C:/Users/USER/Desktop/notice_crawler/README.operation.md)
|
||||
- 테스트: [`README.test.md`](/C:/Users/USER/Desktop/notice_crawler/README.test.md)
|
||||
|
||||
## 왜 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:
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
```javascript
|
||||
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 태그
|
||||
|
||||
```javascript
|
||||
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`
|
||||
|
||||
일 때만 실행하는 용도입니다.
|
||||
|
||||
```javascript
|
||||
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
|
||||
{{ $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` 수정으로 제한할 수 있습니다.
|
||||
Reference in New Issue
Block a user