Skip to content

♻️ refactor: Feed Crawler, Server 환경 변수 이름 및 위치 통일, 로깅 범위 결정 Docker Compose (5) #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5ec9733
Merge branch 'refactor/docker-front' into refactor/docker-compose
Jo-Minseok Jan 29, 2025
5fa6233
Merge branch 'refactor/docker-server' into refactor/docker-compose
Jo-Minseok Jan 29, 2025
0eb7762
Merge branch 'refactor/docker-feed-crawler' into refactor/docker-compose
Jo-Minseok Jan 29, 2025
9776063
Merge branch 'refactor/docker-front' into refactor/docker-compose
Jo-Minseok Jan 29, 2025
2aca5cf
📦 chore: docker compose init SQL 작성
Jo-Minseok Jan 29, 2025
193e26c
Merge remote-tracking branch 'origin/refactor/docker-front' into refa…
Jo-Minseok Feb 2, 2025
c32b220
Merge branch 'chore/feed-crawler-CICD' into refactor/docker-compose
Jo-Minseok Feb 2, 2025
8ca059f
Merge remote-tracking branch 'origin/refactor/docker-front' into refa…
Jo-Minseok Feb 2, 2025
fb59254
Merge remote-tracking branch 'origin/refactor/docker-feed-crawler' in…
Jo-Minseok Feb 2, 2025
1956973
Merge remote-tracking branch 'origin/refactor/docker-server' into ref…
Jo-Minseok Feb 2, 2025
586affb
Merge branch 'refactor/docker-feed-crawler' into refactor/docker-compose
Jo-Minseok Feb 2, 2025
2ccf437
Merge branch 'refactor/docker-server' into refactor/docker-compose
Jo-Minseok Feb 2, 2025
1a79b13
Merge branch 'chore/feed-crawler-CICD' into refactor/docker-compose
Jo-Minseok Feb 2, 2025
c321772
Merge branch 'refactor/docker-feed-crawler' into refactor/docker-compose
Jo-Minseok Feb 2, 2025
fcc4e23
📦 chore: 로컬 도커 환경변수 파일 업로드
Jo-Minseok Feb 3, 2025
21a378f
Merge branch 'refactor/docker-front' into refactor/docker-compose
Jo-Minseok Feb 3, 2025
135cca8
Merge remote-tracking branch 'origin/refactor/docker-server' into ref…
Jo-Minseok Feb 3, 2025
48a3632
Merge remote-tracking branch 'origin/refactor/docker-feed-crawler' in…
Jo-Minseok Feb 3, 2025
5ebe30a
🧼 clean: 테스트에서는 로깅을 하지 않기 때문에 SQLITE 제거
Jo-Minseok Feb 5, 2025
2c1b1ed
♻️ refactor: 로컬, 개발 환경에서는 SQL문 출력하도록 변경
Jo-Minseok Feb 5, 2025
d3932c5
♻️ refactor: 로컬, 개발 환경에서는 API 응답 속도 출력하도록 변경
Jo-Minseok Feb 5, 2025
f11325a
♻️ refactor: 테스트 = 로그 출력 X, 로컬 = 파일 + 콘솔, 운영 = 파일, 개발 = 콘솔 방식으로 변경
Jo-Minseok Feb 5, 2025
33da336
♻️ refactor: 환경 변수 이름, 환경 변수 위치, 로깅 방식 Server Feed Crawler 통일
Jo-Minseok Feb 5, 2025
3d15958
♻️ refactor: rss parser 클래스 의존성 주입 방식으로 변경
Jo-Minseok Feb 5, 2025
2655140
🐛 fix: 로그 위치 이상한 부분 수정
Jo-Minseok Feb 5, 2025
fdb7f98
🐛 fix: dEV 환경 누락 수정
Jo-Minseok Feb 5, 2025
3d3646a
✅ test: feed crawler 데이터 Mocking 방식으로 처리
Jo-Minseok Feb 5, 2025
0d08ad4
Merge branch 'main' into refactor/log-env
Jo-Minseok Feb 6, 2025
c5ca89b
Merge branch 'main' into refactor/log-env
Jo-Minseok Feb 19, 2025
a1c8472
🐛 fix: 병합 문법 수정
Jo-Minseok Feb 19, 2025
340571c
Merge branch 'main' into refactor/log-env
Jo-Minseok Feb 19, 2025
4c45456
Merge branch 'main' into refactor/log-env
Jo-Minseok Feb 19, 2025
0ed7e7f
📦 chore: gitignore .env 추가
Jo-Minseok Feb 20, 2025
f4dadb5
Merge branch 'main' into refactor/log-env
Jo-Minseok Feb 20, 2025
8b30030
Merge branch 'main' into refactor/log-env
Jo-Minseok Feb 21, 2025
202e470
Merge branch 'main' into refactor/log-env
Jo-Minseok Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
!Dockerfile.local

!.env.local
!Dockerfile.local
Expand Down
174 changes: 174 additions & 0 deletions docker-compose/init.sql

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions feed-crawler/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/node_modules
.env
.env.*
!.env.local
/dist
feed-crawler.log
/test/coverage
/logs
/test/coverage
.env
10 changes: 10 additions & 0 deletions feed-crawler/env/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DB_HOST=mysql-db
DB_NAME=denamu
DB_USER=denamu-db-user
DB_PASS=denamu-db-pw
DB_TABLE=rss_accept
TIME_INTERVAL=30
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_USERNAME=denamu-redis-user
REDIS_PASSWORD=denamu-redis-pw
13 changes: 4 additions & 9 deletions feed-crawler/src/common/constant.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import * as dotenv from 'dotenv';

dotenv.config({
path: process.env.NODE_ENV === 'production' ? 'feed-crawler/.env' : '.env',
});
export const CONNECTION_LIMIT = 50;
export const redisConstant = {
FEED_RECENT_ALL_KEY: 'feed:recent:*',
FEED_AI_QUEUE: `feed:ai:queue`,
};

export const ONE_MINUTE = 60 * 1000;
export const INTERVAL =
process.env.NODE_ENV === 'test'
? parseInt(process.env.TEST_TIME_INTERVAL)
: parseInt(process.env.TIME_INTERVAL);
export const TIME_INTERVAL =
process.env.NODE_ENV !== 'test'
? parseInt(process.env.TIME_INTERVAL)
: Number.MAX_SAFE_INTEGER;

export const FEED_AI_SUMMARY_IN_PROGRESS_MESSAGE = `아직 AI가 요약을 진행중인 게시글 이에요! 💭`;

Expand Down
10 changes: 10 additions & 0 deletions feed-crawler/src/common/env-load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as dotenv from "dotenv";

dotenv.config({
path:
{
PROD: `${process.cwd()}/env/.env.prod`,
LOCAL: `${process.cwd()}/env/.env.local`,
DEV: `${process.cwd()}/env/.env.local`,
}[process.env.NODE_ENV] || "",
});
16 changes: 7 additions & 9 deletions feed-crawler/src/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@ const logFormat = printf(({ level, message, timestamp }) => {
});

const transports = [];
transports.push(new winston.transports.Console());
if (process.env.NODE_ENV !== 'test') {
if (process.env.NODE_ENV === 'LOCAL' || process.env.NODE_ENV === 'PROD') {
transports.push(
new winston.transports.File({
filename: `${
process.env.NODE_ENV === 'production'
? 'feed-crawler/logs/feed-crawler.log'
: 'logs/feed-crawler.log'
}`,
}),
new winston.transports.File({ filename: 'logs/feed-crawler.log' }),
);
}

if (process.env.NODE_ENV === 'LOCAL' || process.env.NODE_ENV === 'DEV') {
transports.push(new winston.transports.Console());
}

const logger = winston.createLogger({
level: 'info',
format: combine(
Expand All @@ -28,6 +25,7 @@ const logger = winston.createLogger({
logFormat,
),
transports: transports,
silent: process.env.NODE_ENV === 'test',
});

export default logger;
9 changes: 2 additions & 7 deletions feed-crawler/src/common/mysql-access.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import * as dotenv from 'dotenv';
import * as mysql from 'mysql2/promise';
import { CONNECTION_LIMIT } from './constant';
import { PoolConnection } from 'mysql2/promise';
import { DatabaseConnection } from '../types/database-connection';
import logger from './logger';

dotenv.config({
path: process.env.NODE_ENV === 'production' ? 'feed-crawler/.env' : '.env',
});

export class MySQLConnection implements DatabaseConnection {
private pool: mysql.Pool;
private nameTag: string;
Expand Down Expand Up @@ -38,7 +33,7 @@ export class MySQLConnection implements DatabaseConnection {
logger.error(
`${this.nameTag} 쿼리 ${query} 실행 중 오류 발생
오류 메시지: ${error.message}
스택 트레이스: ${error.stack}`,
스택 트레이스: ${error.stack}`
);
} finally {
if (connection) {
Expand All @@ -48,7 +43,7 @@ export class MySQLConnection implements DatabaseConnection {
logger.error(
`${this.nameTag} connection release 중 오류 발생
오류 메시지: ${error.message}
스택 트레이스: ${error.stack}`,
스택 트레이스: ${error.stack}`
);
}
}
Expand Down
5 changes: 0 additions & 5 deletions feed-crawler/src/common/redis-access.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import * as dotenv from 'dotenv';
import Redis, { ChainableCommander } from 'ioredis';
import Redis_Mock from 'ioredis-mock';
import logger from '../common/logger';
import { injectable } from 'tsyringe';

dotenv.config({
path: process.env.NODE_ENV === 'production' ? 'feed-crawler/.env' : '.env',
});

@injectable()
export class RedisConnection {
private redis: Redis;
Expand Down
56 changes: 56 additions & 0 deletions feed-crawler/src/common/rss-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import logger from "./logger";
import { parse } from "node-html-parser";
import { unescape } from "html-escaper";

export class RssParser {
async getThumbnailUrl(feedUrl: string) {
const response = await fetch(feedUrl, {
headers: {
Accept: "text/html",
},
});
if (!response.ok) {
throw new Error(`${feedUrl}에 GET 요청 실패`);
}

const htmlData = await response.text();
const htmlRootElement = parse(htmlData);
const metaImage = htmlRootElement.querySelector(
'meta[property="og:image"]'
);
let thumbnailUrl = metaImage?.getAttribute("content") ?? "";

if (!thumbnailUrl.length) {
logger.warn(`${feedUrl}에서 썸네일 추출 실패`);
return thumbnailUrl;
}

if (!this.isUrlPath(thumbnailUrl)) {
thumbnailUrl = this.getHttpOriginPath(feedUrl) + thumbnailUrl;
}
return thumbnailUrl;
}

private isUrlPath(thumbnailUrl: string) {
const reg = /^(http|https):\/\//;
return reg.test(thumbnailUrl);
}

private getHttpOriginPath(feedUrl: string) {
return new URL(feedUrl).origin;
}

customUnescape(feedTitle: string): string {
const escapeEntity = {
"·": "·",
" ": " ",
};
Object.keys(escapeEntity).forEach((escapeKey) => {
const value = escapeEntity[escapeKey];
const regex = new RegExp(escapeKey, "g");
feedTitle = feedTitle.replace(regex, value);
});

return unescape(feedTitle);
}
}
20 changes: 13 additions & 7 deletions feed-crawler/src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,39 @@ import { RssRepository } from './repository/rss.repository';
import { FeedRepository } from './repository/feed.repository';
import { RedisConnection } from './common/redis-access';
import { TagMapRepository } from './repository/tag-map.repository';
import { RssParser } from './common/rss-parser';
import { ClaudeService } from './claude.service';
import { RssParser } from './feed-crawler';

container.registerSingleton<DatabaseConnection>(
DEPENDENCY_SYMBOLS.DatabaseConnection,
MySQLConnection
MySQLConnection,
);

container.registerSingleton<RedisConnection>(
DEPENDENCY_SYMBOLS.RedisConnection,
RedisConnection
RedisConnection,
);

container.registerSingleton<RssRepository>(
DEPENDENCY_SYMBOLS.RssRepository,
RssRepository
RssRepository,
);

container.registerSingleton<FeedRepository>(
DEPENDENCY_SYMBOLS.FeedRepository,
FeedRepository
FeedRepository,
);

container.registerSingleton<TagMapRepository>(
DEPENDENCY_SYMBOLS.TagMapRepository,
TagMapRepository
TagMapRepository,
);

container.registerSingleton<ClaudeService>(
DEPENDENCY_SYMBOLS.ClaudeService,
ClaudeService
ClaudeService,
);

container.registerSingleton<RssParser>(DEPENDENCY_SYMBOLS.RssParser, RssParser);

export { container };
63 changes: 2 additions & 61 deletions feed-crawler/src/feed-crawler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import { RssRepository } from './repository/rss.repository';
import logger from './common/logger';
import { RssObj, FeedDetail, RawFeed } from './common/types';
import { XMLParser } from 'fast-xml-parser';
import { parse } from 'node-html-parser';
import { unescape } from 'html-escaper';
import {
ONE_MINUTE,
INTERVAL,
TIME_INTERVAL,
FEED_AI_SUMMARY_IN_PROGRESS_MESSAGE,
} from './common/constant';
import { RssParser } from './common/rss-parser';

export class FeedCrawler {
constructor(
Expand All @@ -25,7 +24,6 @@ export class FeedCrawler {
await this.feedRepository.deleteRecentFeed();

const rssObjects = await this.rssRepository.selectAllRss();

if (!rssObjects || !rssObjects.length) {
logger.info('등록된 RSS가 없습니다.');
return;
Expand All @@ -39,7 +37,6 @@ export class FeedCrawler {
return;
}
logger.info(`총 ${newFeeds.length}개의 새로운 피드가 있습니다.`);

const insertedData: FeedDetail[] = await this.feedRepository.insertFeeds(
newFeeds,
);
Expand All @@ -58,9 +55,7 @@ export class FeedCrawler {
now: number,
): Promise<FeedDetail[]> {
try {
const TIME_INTERVAL = INTERVAL;
const feeds = await this.fetchRss(rssObj.rssUrl);

const filteredFeeds = feeds.filter((item) => {
const pubDate = new Date(item.pubDate).setSeconds(0, 0);
const timeDiff = (now - pubDate) / (ONE_MINUTE * TIME_INTERVAL);
Expand Down Expand Up @@ -146,57 +141,3 @@ export class FeedCrawler {
}));
}
}

export class RssParser {
async getThumbnailUrl(feedUrl: string) {
const response = await fetch(feedUrl, {
headers: {
Accept: 'text/html',
},
});

if (!response.ok) {
throw new Error(`${feedUrl}에 GET 요청 실패`);
}

const htmlData = await response.text();
const htmlRootElement = parse(htmlData);
const metaImage = htmlRootElement.querySelector(
'meta[property="og:image"]',
);
let thumbnailUrl = metaImage?.getAttribute('content') ?? '';

if (!thumbnailUrl.length) {
logger.warn(`${feedUrl}에서 썸네일 추출 실패`);
return thumbnailUrl;
}

if (!this.isUrlPath(thumbnailUrl)) {
thumbnailUrl = this.getHttpOriginPath(feedUrl) + thumbnailUrl;
}
return thumbnailUrl;
}

private isUrlPath(thumbnailUrl: string) {
const reg = /^(http|https):\/\//;
return reg.test(thumbnailUrl);
}

private getHttpOriginPath(feedUrl: string) {
return new URL(feedUrl).origin;
}

customUnescape(feedTitle: string): string {
const escapeEntity = {
'&middot;': '·',
'&nbsp;': ' ',
};
Object.keys(escapeEntity).forEach((escapeKey) => {
const value = escapeEntity[escapeKey];
const regex = new RegExp(escapeKey, 'g');
feedTitle = feedTitle.replace(regex, value);
});

return unescape(feedTitle);
}
}
4 changes: 3 additions & 1 deletion feed-crawler/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'reflect-metadata';
import './common/env-load';
import logger from './common/logger';
import { FeedCrawler, RssParser } from './feed-crawler';
import { FeedCrawler } from './feed-crawler';
import { container } from './container';
import { RssRepository } from './repository/rss.repository';
import { FeedRepository } from './repository/feed.repository';
import { DEPENDENCY_SYMBOLS } from './types/dependency-symbols';
import { DatabaseConnection } from './types/database-connection';
import { RssParser } from './common/rss-parser';
import { ClaudeService } from './claude.service';
import * as schedule from 'node-schedule';
import { RedisConnection } from './common/redis-access';
Expand Down
Loading