diff --git a/client/.gitignore b/client/.gitignore index cf81e28c..2c0b181b 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +!Dockerfile.local !.env.local !Dockerfile.local diff --git a/docker-compose/init.sql b/docker-compose/init.sql new file mode 100644 index 00000000..d556e64f --- /dev/null +++ b/docker-compose/init.sql @@ -0,0 +1,174 @@ +-- denamu.admin definition + +CREATE TABLE `admin` ( + `id` int NOT NULL AUTO_INCREMENT, + `login_id` varchar(255) NOT NULL, + `password` varchar(60) NOT NULL, + PRIMARY KEY (`id`) +); + +-- denamu.rss definition + +CREATE TABLE `rss` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `user_name` varchar(50) NOT NULL, + `email` varchar(255) NOT NULL, + `rss_url` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +); + +-- denamu.rss_accept definition + +CREATE TABLE `rss_accept` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `user_name` varchar(50) NOT NULL, + `email` varchar(255) NOT NULL, + `rss_url` varchar(255) NOT NULL, + `blog_platform` varchar(255) NOT NULL DEFAULT 'etc', + PRIMARY KEY (`id`), + FULLTEXT KEY (`name`) +); + +-- denamu.rss_reject definition + +CREATE TABLE `rss_reject` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `user_name` varchar(50) NOT NULL, + `email` varchar(255) NOT NULL, + `rss_url` varchar(255) NOT NULL, + `description` varchar(512) NOT NULL, + PRIMARY KEY (`id`) +); + +-- denamu.feed definition + +CREATE TABLE `feed` ( + `id` int NOT NULL AUTO_INCREMENT, + `created_at` datetime NOT NULL, + `title` varchar(255) NOT NULL, + `view_count` int NOT NULL DEFAULT '0', + `path` varchar(512) NOT NULL, + `thumbnail` varchar(255) DEFAULT NULL, + `blog_id` int NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY (`path`), + KEY (`blog_id`), + KEY (`created_at`), + CONSTRAINT `FK_feed_blog_id` FOREIGN KEY (`blog_id`) REFERENCES `rss_accept` (`id`) ON UPDATE CASCADE, + FULLTEXT KEY (`title`) /*!50100 WITH PARSER `ngram` */ +); + +INSERT INTO denamu.rss_accept (name,user_name,email,rss_url,blog_platform) VALUES + ('seok3765.log','조민석','seok3765@naver.com','https://v2.velog.io/rss/@seok3765','velog'), + ('나무보다 숲을','채준혁','cjh4302@gmail.com','https://laurent.tistory.com/rss','tistory'), + ('월성참치','정명기','jmk101711@naver.com','https://tunaspace.tistory.com/rss','tistory'), + ('해야지 뭐','안성윤','asn6878@gmail.com','https://asn6878.tistory.com/rss','tistory'); + +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-12-15 15:20:23','[네이버 커넥트재단 부스트캠프 웹・모바일 9기] 날 것 그대로 작성하는 멤버십 수료 후기 - Web',0,'https://velog.io/@seok3765/네이버-커넥트재단-부스트캠프-웹・모바일-9기-날-것-그대로-작성하는-멤버십-수료-후기-Web','https://velog.velcdn.com/images/seok3765/post/a655dff9-58bc-436b-bdef-9e1195e5cbf6/image.png',1), + ('2024-08-14 14:07:49','[네이버 커넥트재단 부스트캠프 웹・모바일 9기] 날 것 그대로 작성하는 챌린지 수료 후기 - Web',0,'https://velog.io/@seok3765/네이버-커넥트재단-부스트캠프-웹・모바일-9기-날-것-그대로-작성하는-챌린지-수료-후기-Web','https://velog.velcdn.com/images/seok3765/post/2f863481-b594-46f8-9a28-7799afb58aa4/image.jpg',1), + ('2025-01-07 14:18:34','[TIL] 리눅스 입문 with 우분투 1일차 정리 (운영체제, 리눅스 찍먹)',1,'https://velog.io/@seok3765/리눅스-입문-with-우분투-1일차-정리','https://velog.velcdn.com/images/seok3765/post/e44c37ae-ffac-4528-87f6-cae3d6466919/image.png',1), + ('2025-01-07 17:54:16','[TIL] 리눅스 입문 with 우분투 2일차 정리 (우분투 설치)',3,'https://velog.io/@seok3765/리눅스-입문-with-우분투-2일차-정리','https://velog.velcdn.com/images/seok3765/post/e44c37ae-ffac-4528-87f6-cae3d6466919/image.png',1), + ('2025-01-08 15:32:49','[TIL] 리눅스 입문 with 우분투 3일차 정리 (터미널과 셸)',1,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-3일차-정리','https://velog.velcdn.com/images/seok3765/post/e44c37ae-ffac-4528-87f6-cae3d6466919/image.png',1), + ('2025-01-11 14:36:51','[TIL] 리눅스 입문 with 우분투 4일차 정리 (명령어, 파일, 디렉터리)',1,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-4일차-정리','https://velog.velcdn.com/images/seok3765/post/e44c37ae-ffac-4528-87f6-cae3d6466919/image.png',1), + ('2025-01-11 17:09:40','[서평] 코딩 자율학습 리눅스 입문 with 우분투',2,'https://velog.io/@seok3765/서평-코딩-자율학습-리눅스-입문-with-우분투','https://velog.velcdn.com/images/seok3765/post/e760d106-efda-4fae-93a9-11a41993de68/image.jpg',1), + ('2025-01-12 13:45:56','[TIL] 리눅스 입문 with 우분투 5일차 정리 (파일과 디렉터리, 링크)',3,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-5일차-정리','https://velog.velcdn.com/images/seok3765/post/70f0c8b6-95a0-4ed1-b057-5ece06202705/image.png',1), + ('2025-01-13 19:56:10','[TIL] 리눅스 입문 with 우분투 6일차 정리 (사용자, 그룹)',1,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-6일차-정리','https://velog.velcdn.com/images/seok3765/post/09ffddcc-f15e-437c-9b21-46f72e9d0795/image.png',1), + ('2025-01-14 13:58:59','[TIL] 리눅스 입문 with 우분투 7일차 정리 (소유권, 권한)',1,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-7일차-정리','https://velog.velcdn.com/images/seok3765/post/cbdfb185-2e8d-474c-a7f3-84a6400e3532/image.png',1); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2025-01-15 18:03:54','[Docker] 가상머신, 하이퍼바이저, 도커 전체 개념',5,'https://velog.io/@seok3765/Docker-도커-개념','https://velog.velcdn.com/images/seok3765/post/ef1c0705-92fe-4d09-b649-c111eb19e98c/image.png',1), + ('2025-01-18 16:12:01','[TIL] 리눅스 입문 with 우분투 8일차 정리 (컴퓨터 작동 원리, 프로세스 생명 주기)',3,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-8일차-정리','https://velog.velcdn.com/images/seok3765/post/d2314c99-5a5d-4a07-b5d7-c65a99e78b1b/image.png',1), + ('2025-01-18 20:58:34','[TIL] 리눅스 입문 with 우분투 9일차 정리 (파일 디스크립터, 포어그라운드, 백그라운드, IPC)',1,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-9일차-정리','https://velog.velcdn.com/images/seok3765/post/a75a45b0-f609-42bf-bec1-18d4b2a00756/image.png',1), + ('2025-01-19 14:28:55','[TIL] 리눅스 입문 with 우분투 10일차 정리 (시그널)',0,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-10일차-정리','https://velog.velcdn.com/images/seok3765/post/ae79f20a-b64c-4b6d-b246-6e501f9c868a/image.png',1), + ('2025-01-20 08:51:03','[TIL] 리눅스 입문 with 우분투 11일차 정리 (변수, 분기)',3,'https://velog.io/@seok3765/TIL-리눅스-입문-with-우분투-11일차-정리','https://velog.velcdn.com/images/seok3765/post/a9322793-f06a-46e2-b93f-0b7aa8d434fd/image.png',1), + ('2025-01-01 09:57:59','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 8장',2,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-8장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEAc7h%2FbtsLCp0GjT6%2FxmRrt2mHV26Q5EZtnlIuYK%2Fimg.jpg',2), + ('2025-01-01 09:57:41','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 7장',1,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-7장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbra31r%2FbtsLBVyPlia%2FKUrcmpjWoQz72dl4hyhy40%2Fimg.jpg',2), + ('2025-01-01 09:57:27','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 6장',1,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-6장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgHcoJ%2FbtsLCJq32Gz%2FqnoiJfT4R8kPVJZX0HkrE1%2Fimg.jpg',2), + ('2025-01-01 09:57:02','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 5장',1,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-5장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmMIdF%2FbtsLCmiLLlz%2FJh3fcj0EE110gip0VbsKa0%2Fimg.jpg',2), + ('2025-01-01 09:56:42','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 4장',2,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-4장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVk4f3%2FbtsLC6TTxJY%2Fgp2A3Zio9oNgaFwVOJhk50%2Fimg.jpg',2); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2025-01-01 09:56:17','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 3장',2,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-3장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHifmQ%2FbtsLCqSOiNu%2F9jz5XgKtqBGI4GVRmcqo81%2Fimg.jpg',2), + ('2025-01-01 09:55:52','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 2장',1,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-2장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk5adg%2FbtsLCGBmBJQ%2F8wLoOtsuafBu4oyC7X24sk%2Fimg.jpg',2), + ('2025-01-01 09:55:19','[컴퓨터학개론] AI시대의 컴퓨터 개론 - 내용 점검 문제 1장',1,'https://laurent.tistory.com/entry/컴퓨터학개론-AI시대의-컴퓨터-개론-내용-점검-문제-1장','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyP38X%2FbtsLC5tSTmB%2FQLJWCIezMTIMK4TI5DW3ck%2Fimg.jpg',2), + ('2024-12-31 13:29:55','2024년 회고',3,'https://laurent.tistory.com/entry/2024년-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4PqwE%2FbtsLDoUaKl5%2FIzlJCnCQWJgUNihL0l5Sq1%2Fimg.png',3), + ('2024-12-29 14:58:45','[서평] 믿고보는 시리즈 - 소플의 처음 만난 AWS',0,'https://laurent.tistory.com/entry/서평-믿고보는-시리즈-소플의-처음-만난-AWS','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcglUxX%2FbtsLAycE6Oq%2FA76t3kFoRKww5Z3Y7tSMQK%2Fimg.png',2), + ('2024-12-24 14:59:11','[서평] 기초부터 배우는 최신 스토리지 입문',0,'https://laurent.tistory.com/entry/서평-기초부터-배우는-최신-스토리지-입문','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmlbHE%2FbtsLxGg2crn%2FItBJRu6dK8d1ugjCH5pBoK%2Fimg.jpg',2), + ('2024-12-23 12:31:09','[React] 좋아요 기능 버그 해결 및 서버 데이터 활용',0,'https://laurent.tistory.com/entry/React-좋아요-기능-버그-해결-및-서버-데이터-활용','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn9XvB%2FbtsLs2SSvXD%2FpX4jJqjYsN3Kvm6kVeCH9K%2Fimg.jpg',2), + ('2024-12-15 15:45:14','네이버 부스트캠프 9기 웹 풀스택 과정 후기',2,'https://laurent.tistory.com/entry/네이버-부스트캠프-9기-웹-풀스택-과정-후기','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo4aHa%2FbtsLiZhAwKH%2FmuiPRCUCK5sVcm1U35KknK%2Fimg.png',2), + ('2024-12-12 17:11:51','[React] useEffect의 내부적인 동작',0,'https://laurent.tistory.com/entry/React-useEffect의-내부적인-동작','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FybzbA%2FbtsLgdtxOZ1%2FbPJG88Zopbg3GYCCv66UA1%2Fimg.jpg',2), + ('2024-12-01 14:20:33','[Javascript] 브라우저 팝업 차단으로 인한 문제와 해결책',0,'https://laurent.tistory.com/entry/Javascript-브라우저-팝업-차단으로-인한-문제와-해결책','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhYyL%2FbtsK2oa0BmV%2FvCOtbQW29hHJtDY1mdX8kk%2Fimg.jpg',2); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-11-30 09:14:29','2024년 11월 정기회고',0,'https://laurent.tistory.com/entry/2024년-11월-정기회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlCF0Y%2FbtsK19rAVhX%2FkyiZZMCXaLKr4zTIQkVCK1%2Fimg.jpg',2), + ('2024-10-27 10:58:51','[서평] 올인원 개발 키트 - 헬로 Bun',0,'https://laurent.tistory.com/entry/서평-올인원-개발-키트-헬로-Bun','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblruHt%2FbtsKlXYphWT%2Fqhkp0koc7ibgNJZAl6gGak%2Fimg.png',2), + ('2024-10-27 06:55:13','[서평] 클라우드 입문서 - 비전공자를 위한 AWS',0,'https://laurent.tistory.com/entry/서평-클라우드-입문서-비전공자를-위한-AWS','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwwQQd%2FbtsKkhRx56w%2Fbry8wS93h3I3yfnZdPpK01%2Fimg.png',2), + ('2024-10-25 07:48:08','[서평] 효과적인 활용을 위해 - 이펙티브 러스트',0,'https://laurent.tistory.com/entry/서평-효과적인-활용을-위해-이펙티브-러스트','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfMtYX%2FbtsKkbJzJtz%2F53JkhTBvH8ymeLjAB28K31%2Fimg.png',2), + ('2024-10-23 03:53:03','[부스트캠프 9기 멤버십] 8주차 회고록',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-8주차-회고록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3pAmf%2FbtsKf6ajeYD%2FAYwELS26sfvhPQjXj4EQzk%2Fimg.png',2), + ('2024-10-13 11:51:15','[부스트캠프 9기 멤버십] 7주차 회고록',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-7주차-회고록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtB2gg%2FbtsJ4pudcJS%2FvgwvJLtbNZHA5zj9I74S9k%2Fimg.png',2), + ('2024-10-05 16:13:14','[부스트캠프 9기 멤버십] 6주차 회고록',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-6주차-회고록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8dRVZ%2FbtsJV0V95O8%2FuWWxRXKOVGHf6hi5XIKON1%2Fimg.jpg',2), + ('2024-09-28 17:22:33','[부스트캠프 9기 멤버십] 5주차 회고록',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-5주차-회고록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKnyGb%2FbtsJQ53m67B%2F4QKnbv1UJFLP7ioz7gGeyk%2Fimg.jpg',2), + ('2024-09-28 17:22:04','[부스트캠프 9기 멤버십] 3주차 회고록',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-3주차-회고록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSbOGD%2FbtsJPTQx8Jn%2F9VKR5i6hYy83ZDAWnRczok%2Fimg.jpg',2), + ('2024-09-28 17:21:14','[부스트캠프 9기 멤버십] 1주차 회고록',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-1주차-회고록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGreTN%2FbtsJQoCs5Iw%2FTOpUWIZ19vgFXXuN8p7Ctk%2Fimg.jpg',2); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-09-28 17:06:28','[서평] 중요한 내용만 빠르게 - 컴퓨터 구조와 운영체제 핵심 노트',0,'https://laurent.tistory.com/entry/서평-중요한-내용만-빠르게-컴퓨터-구조와-운영체제-핵심-노트','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj2tZc%2FbtsJPmTlq81%2FKJ5hVNI4vpoDNCi1kLZh40%2Fimg.png',2), + ('2024-09-28 17:03:04','2024년 9월 정기회고',0,'https://laurent.tistory.com/entry/2024년-9월-정기회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVJNy0%2FbtsJQD7u9mB%2FYtkPRWkjZG0Mcu88eD6zxK%2Fimg.jpg',2), + ('2024-09-28 11:21:19','[서평] CS 익힘책 - 이것이 취업을 위한 컴퓨터 과학이다',0,'https://laurent.tistory.com/entry/서평-CS-익힘책-이것이-취업을-위한-컴퓨터-과학이다','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHi3fm%2FbtsJRcBiuc2%2Fg42a8KfGjBeYpg3xSdmZJK%2Fimg.png',2), + ('2024-09-19 12:20:31','[Typescript] 사진과 영상을 FormData로 서버에 전송하기',0,'https://laurent.tistory.com/entry/Typescript-사진과-영상을-FormData로-서버에-전송하기','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnF8gk%2FbtsJElHhxEA%2FvEHYqgusACTezGW9kdXNc1%2Fimg.jpg',2), + ('2024-09-09 00:46:28','2024년 8월 정기회고',0,'https://laurent.tistory.com/entry/2024년-8월-정기회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8rzkH%2FbtsJuXMqI8r%2Fk4zdUJzPj541WY81lx6MbK%2Fimg.png',2), + ('2024-09-03 16:22:28','[서평] 모던 자바 기능으로 전문가 되기 - 기본기가 탄탄한 자바 개발자',0,'https://laurent.tistory.com/entry/서평-모던-자바-기능으로-전문가-되기-기본기가-탄탄한-자바-개발자','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5wDyE%2FbtsJqFdebVC%2FFgt1xtxl46o6PkEatQpSK0%2Fimg.jpg',2), + ('2024-09-03 16:12:27','[서평] 인공지능 시대의 경제 - 금융 AI의 이해',0,'https://laurent.tistory.com/entry/서평-인공지능-시대의-경제-금융-AI의-이해','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7fBGc%2FbtsJpKffzUM%2FOpZRjbdCJF6kNzqvEXtkV0%2Fimg.jpg',2), + ('2024-08-26 15:21:31','[Javascript] 이벤트 전파',0,'https://laurent.tistory.com/entry/Javascript-이벤트-전파','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEQK4T%2FbtsJggL7myP%2F0uDMZyyPaQbxQzdvecI8h0%2Fimg.jpg',2), + ('2024-08-26 15:00:10','[Javascript] 이벤트 핸들러 등록',0,'https://laurent.tistory.com/entry/Javascript-이벤트-핸들러-등록','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNhzEi%2FbtsJg9FmunA%2F0lbnnxPPU3XwEqFrx3Vzp1%2Fimg.jpg',2), + ('2024-08-22 12:04:41','[부스트캠프 9기 멤버십] 수료생과의 밋업',0,'https://laurent.tistory.com/entry/부스트캠프-9기-멤버십-수료생과의-밋업','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMVDPH%2FbtsJb6iLOFj%2Fd1du0CyDy1djGk9Wf3ndbK%2Fimg.png',2); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-08-11 12:27:57','네이버 부스트캠프 9기 챌린지 회고',0,'https://laurent.tistory.com/entry/네이버-부스트캠프-9기-챌린지-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMMWks%2FbtsIZW2mVH8%2Fkksak8FH1k9zGfAAPo5NX0%2Fimg.jpg',2), + ('2024-08-11 05:50:17','[부스트캠프 9기 챌린지] 4주차 회고',0,'https://laurent.tistory.com/entry/부스트캠프-9기-챌린지-4주차-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHLEx6%2FbtsI1emPYnH%2FLa1V6pjwDoZHozoFiUVlm1%2Fimg.jpg',2), + ('2024-08-02 09:26:06','[부스트캠프 9기 챌린지] 3주차 회고',0,'https://laurent.tistory.com/entry/부스트캠프-9기-챌린지-3주차-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv0hjY%2FbtsITOvtlkj%2F0T0GXiGKr6plU9fkcOYwkk%2Fimg.jpg',2), + ('2024-07-31 17:06:41','2024년 7월 정기회고',0,'https://laurent.tistory.com/entry/2024년-7월-정기회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq9IWm%2FbtsJjqnfLQl%2F4kohwz3l65AUmSFI4D2J20%2Fimg.jpg',2), + ('2024-07-28 14:03:03','[서평] 실무로 통하는 타입스크립트',0,'https://laurent.tistory.com/entry/서평-실무로-통하는-타입스크립트','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8eX67%2FbtsIPlOC2IU%2F2nuAWVpbEXd1arAR9MRe71%2Fimg.jpg',2), + ('2024-07-26 10:24:54','[부스트캠프 9기 챌린지] 2주차 회고',0,'https://laurent.tistory.com/entry/부스트캠프-9기-챌린지-2주차-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4CJ77%2FbtsIPUPOczX%2FMsyquWz74cKDMvbKGsi37K%2Fimg.jpg',2), + ('2024-07-20 15:43:33','[부스트캠프 9기 챌린지] 수료생과의 밋업',0,'https://laurent.tistory.com/entry/부스트캠프-9기-챌린지-수료생과의-밋업','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRhljL%2FbtsIHJ1S6gh%2F78kj3yhIVSKGbGUOB91iVk%2Fimg.jpg',2), + ('2024-07-19 09:30:40','[부스트캠프 9기 챌린지] 1주차 회고',0,'https://laurent.tistory.com/entry/부스트캠프-9기-챌린지-1주차-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuGO8u%2FbtsIG9MY4ab%2F9WMLLYBS3tZzR6h3iKfuIK%2Fimg.jpg',2), + ('2024-07-19 03:00:27','[서평] 문제와 해설을 한 번에 - 이기적 정보처리기사 실기 핵심 600제',0,'https://laurent.tistory.com/entry/서평-문제와-해설을-한-번에-이기적-정보처리기사-실기-핵심-600제','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYvw9l%2FbtsIDNLtYQL%2Fu7Fbx7VohtlFEXi90yk2M1%2Fimg.png',2), + ('2024-07-14 09:00:26','[C언어] 문자와 문자열',0,'https://laurent.tistory.com/entry/C언어-문자와-문자열','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtXhp1%2FbtsIz9eJRwJ%2F4igdXEjgNN5OGKRpKuLKa0%2Fimg.jpg',2); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-07-13 05:37:07','[서평] 테스트 개론 - 프런트엔드 개발을 위한 테스트 입문',0,'https://laurent.tistory.com/entry/서평-테스트-개론-프런트엔드-개발을-위한-테스트-입문','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn7gN3%2FbtsIy1uUESB%2FahrYTfKoelGqbDI6LqdPGK%2Fimg.png',2), + ('2024-07-12 11:28:43','인프콘 2024 랠릿 허브 등록 이벤트',0,'https://laurent.tistory.com/entry/인프콘-2024-랠릿-허브-등록-이벤트','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqWtzj%2FbtsIy5jkhJQ%2F1AVJpBTtwKcyxliewua34K%2Fimg.jpg',2), + ('2024-07-06 15:00:42','네이버 부스트캠프 9기 베이직 + 2차 문제 해결력 테스트 회고',0,'https://laurent.tistory.com/entry/네이버-부스트캠프-9기-베이직-2차-문제-해결력-테스트-회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmPLDo%2FbtsIo65xUjY%2FKA2gq1K4dkue2D1Bxt8xS1%2Fimg.jpg',2), + ('2024-07-04 18:22:54','[서평] 그림으로 쉽고 빠르게 배우는 - AWS 시스템 개발 스킬업',0,'https://laurent.tistory.com/entry/서평-그림으로-쉽고-빠르게-배우는-AWS-시스템-개발-스킬업','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrBlJ2%2FbtsInLNet1p%2Fs0RCF5VbRNzywH9LxocHz0%2Fimg.png',2), + ('2024-06-30 13:00:59','2024년 6월 정기회고',0,'https://laurent.tistory.com/entry/2024년-6월-정기회고','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCMgsb%2FbtsIixtRGu5%2FMb4UuXsK1Kliv6JE2A2q3k%2Fimg.jpg',2), + ('2025-01-08 11:57:16','프론트엔드 단위 테스트 이해하기',3,'https://laurent.tistory.com/entry/프론트엔드-단위-테스트-이해하기','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi6Ai4%2FbtsLHpmHc17%2FPPfyHuka096AmSwXchKK21%2Fimg.jpg',2), + ('2025-01-11 06:28:52','[Network] OSI Model과 7 Layer 별 장비',1,'https://laurent.tistory.com/entry/Network-OSI-Model과-7-Layer-별-장비','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnEMAv%2FbtsLJcn0jw1%2FZZZ3LXNzrEPCfFvvBCgN50%2Fimg.jpg',2), + ('2025-01-15 23:12:08','시나리오 구성 및 테스트 코드 작성',3,'https://laurent.tistory.com/entry/시나리오-구성-및-테스트-코드-작성','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCGsNc%2FbtsLOXb7DRZ%2FAahZMI6epsYhKCYOLjnoHK%2Fimg.png',2), + ('2025-01-19 04:05:51','테스트 커버리지가 제대로 인식되지 않는 현상 해결',1,'https://laurent.tistory.com/entry/테스트-커버리지가-제대로-인식되지-않는-현상-해결','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDOJJC%2FbtsLTHlFiOc%2FAUsjbbbcIRZ5r8CQZX6wJK%2Fimg.jpg',2), + ('2024-12-30 08:33:30','LeetCode - Numberof Different Integer in a String',0,'https://tunaspace.tistory.com/entry/LeetCode-Numberof-Different-Integer-in-a-String','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-12-27 15:10:31','LeetCode - Restore IP Addresses',0,'https://tunaspace.tistory.com/entry/LeetCode-Restore-IP-Addresses','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2024-12-25 14:22:20','LeetCode - Path Sum',0,'https://tunaspace.tistory.com/entry/LeetCode-Path-Sum','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2024-12-23 12:50:04','LeetCode - Maximum Average Subarray 1',0,'https://tunaspace.tistory.com/entry/LeetCode-Maximum-Average-Subarray-1','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',4), + ('2024-09-30 09:42:54','바닐라 JS로 리액트 만들기 - 4',0,'https://tunaspace.tistory.com/entry/바닐라-JS로-리액트-만들기-4','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2024-09-24 14:57:44','바닐라 JS로 리액트 만들기 - 3',0,'https://tunaspace.tistory.com/entry/바닐라-JS로-리액트-만들기-3','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2024-09-24 13:44:17','바닐라 JS로 리액트 만들기 - 2',0,'https://tunaspace.tistory.com/entry/바닐라-JS로-리액트-만들기-2','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2024-09-24 13:16:56','바닐라 JS로 리액트 만들기 - 1',0,'https://tunaspace.tistory.com/entry/바닐라-JS로-리액트-만들기-1','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2024-09-23 11:55:19','SPA란?',0,'https://tunaspace.tistory.com/entry/SPA란','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0TiaW%2FbtsJIftdEP2%2F4oyi8Qp8XKmNApDdQCvpP0%2Fimg.png',4), + ('2024-09-01 06:04:50','TASKIFY Day-5 학습정리',0,'https://tunaspace.tistory.com/entry/TASKIFY-Day-학습정리','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSmcvY%2FbtsJnGwsPa6%2FZFfRmwJMhO1RZsTRq3mUEK%2Fimg.gif',3), + ('2025-01-02 12:44:14','LeetCode - Set Matrix Zeroes',1,'https://tunaspace.tistory.com/entry/LeetCode-Set-Matrix-Zeroes','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2025-01-02 12:37:25','LeetCode - Minimum Add to Make Parentheses Valid',0,'https://tunaspace.tistory.com/entry/LeetCode-Minimum-Add-to-Make-Parentheses-Valid','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Ft1.daumcdn.net%2Ftistory_admin%2Fstatic%2Fimages%2FopenGraph%2Fopengraph.png',3), + ('2025-01-04 13:35:45','HTML의 역사',5,'https://tunaspace.tistory.com/entry/HTML의-역사','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBDgk9%2FbtsLDO7Er25%2Fd5tF9fS5KYWrkoJ8sKv4CK%2Fimg.png',3), + ('2025-01-15 16:12:24','[네트워크] 네크워크 기초',6,'https://tunaspace.tistory.com/entry/네트워크-네크워크-기초','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZuK6y%2FbtsLP4Vv2dw%2FoKp9rKYtYglrzkdzxBzrWk%2Fimg.png',3), + ('2024-12-22 10:15:29','네이버 클라우드 플랫폼(Ncloud) 사용 후기',0,'https://asn6878.tistory.com/13','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzmjo1%2FbtsLqzQ9Ovg%2FDXjqkrNmllwBqkxzKSPGJ1%2Fimg.png',4), + ('2024-12-11 13:23:29','부스트캠프 웹・모바일 9기 멤버십 과정 회고',0,'https://asn6878.tistory.com/12','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frd4s6%2FbtsLd7tRHtG%2FzLdrkltHSjDkctSq1O9Rf1%2Fimg.png',4), + ('2024-09-22 23:00:51','자바스크립트의 구조와 실행 방식 (Ignition, TurboFan, EventLoop)',0,'https://asn6878.tistory.com/9','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wH52%2FbtsJIskiFgS%2FQlF4XqMVZsM8y51w67dxj1%2Fimg.png',4), + ('2024-08-15 17:37:32','부스트캠프 웹・모바일 9기 챌린지 과정 회고',0,'https://asn6878.tistory.com/8','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5oZKx%2FbtsI2pi4Vdz%2FlK6ITtEr1foWfmEGGBBDW0%2Fimg.png',4), + ('2024-08-04 08:32:17','페어(짝) 프로그래밍에 대해서',0,'https://asn6878.tistory.com/7','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo0I0n%2FbtsITiXYkG9%2FhpD50L7TcKlhU08D2jok4k%2Fimg.jpg',4), + ('2024-07-06 19:20:07','2024 네이버 부스트캠프 웹 · 모바일 2차 코딩테스트 후기',0,'https://asn6878.tistory.com/6','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzvZIm%2FbtsIpvcWnzY%2FnkR2JuxsNhKIyeeKHnMo1k%2Fimg.png',4), + ('2024-05-22 16:19:34','코딩테스트 준비를 위한 Java 입출력 정리',0,'https://asn6878.tistory.com/5','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYY34s%2FbtsHykim0k7%2FT7YBZJfvIEKvPmtLbXJkIk%2Fimg.png',4); +INSERT INTO feed (created_at,title,view_count,`path`,thumbnail,blog_id) VALUES + ('2024-05-03 16:30:23','[Docker] 간단한 도커 명령어 모음집',2,'https://asn6878.tistory.com/4','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI3y45%2FbtsHcIbDPUe%2FpWNfGE2V3YX35MauB1Hb60%2Fimg.gif',4), + ('2024-03-10 08:49:55','Java record 에 대하여',0,'https://asn6878.tistory.com/3','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddtCkc%2FbtsFGEvHLSY%2FIPqWLZZfYlojZyLCB4dPg1%2Fimg.gif',4), + ('2024-01-04 11:37:46','인증(Authentication)과 인가(Authorization)의 개념에 대해',0,'https://asn6878.tistory.com/2','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4Psk9%2FbtsC00h6SuP%2FZp2x8yPLdLLheMrGqJeHG0%2Fimg.png',4), + ('2025-01-16 19:29:50','NestJS + TypeORM + Testcontainers 를 사용한 통합 테스트 DB환경 구축하기',3,'https://asn6878.tistory.com/14','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2GhHh%2FbtsLPtpiK1d%2FtKiZjT4WEVz1sy4LIgFDn1%2Fimg.png',4), + ('2025-01-18 07:12:05','자바 vs 노드 당신의 선택은?!',4,'https://asn6878.tistory.com/15','https://img1.daumcdn.net/thumb/R800x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdofQSP%2FbtsLKJyhso1%2FREdhKR9vDlzDYREytkK0v1%2Fimg.png',4); diff --git a/feed-crawler/.gitignore b/feed-crawler/.gitignore index 93f56297..31b06d98 100644 --- a/feed-crawler/.gitignore +++ b/feed-crawler/.gitignore @@ -1,5 +1,7 @@ /node_modules -.env +.env.* +!.env.local /dist -feed-crawler.log -/test/coverage \ No newline at end of file +/logs +/test/coverage +.env \ No newline at end of file diff --git a/feed-crawler/env/.env.local b/feed-crawler/env/.env.local new file mode 100644 index 00000000..5d814cba --- /dev/null +++ b/feed-crawler/env/.env.local @@ -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 \ No newline at end of file diff --git a/feed-crawler/src/common/constant.ts b/feed-crawler/src/common/constant.ts index 662b189b..57a05e48 100644 --- a/feed-crawler/src/common/constant.ts +++ b/feed-crawler/src/common/constant.ts @@ -1,8 +1,3 @@ -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:*', @@ -10,10 +5,10 @@ export const redisConstant = { }; 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가 요약을 진행중인 게시글 이에요! 💭`; diff --git a/feed-crawler/src/common/env-load.ts b/feed-crawler/src/common/env-load.ts new file mode 100644 index 00000000..be6db9b6 --- /dev/null +++ b/feed-crawler/src/common/env-load.ts @@ -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] || "", +}); diff --git a/feed-crawler/src/common/logger.ts b/feed-crawler/src/common/logger.ts index 2e7dcc7f..b668b833 100644 --- a/feed-crawler/src/common/logger.ts +++ b/feed-crawler/src/common/logger.ts @@ -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( @@ -28,6 +25,7 @@ const logger = winston.createLogger({ logFormat, ), transports: transports, + silent: process.env.NODE_ENV === 'test', }); export default logger; diff --git a/feed-crawler/src/common/mysql-access.ts b/feed-crawler/src/common/mysql-access.ts index ef27d681..3771a55b 100644 --- a/feed-crawler/src/common/mysql-access.ts +++ b/feed-crawler/src/common/mysql-access.ts @@ -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; @@ -38,7 +33,7 @@ export class MySQLConnection implements DatabaseConnection { logger.error( `${this.nameTag} 쿼리 ${query} 실행 중 오류 발생 오류 메시지: ${error.message} - 스택 트레이스: ${error.stack}`, + 스택 트레이스: ${error.stack}` ); } finally { if (connection) { @@ -48,7 +43,7 @@ export class MySQLConnection implements DatabaseConnection { logger.error( `${this.nameTag} connection release 중 오류 발생 오류 메시지: ${error.message} - 스택 트레이스: ${error.stack}`, + 스택 트레이스: ${error.stack}` ); } } diff --git a/feed-crawler/src/common/redis-access.ts b/feed-crawler/src/common/redis-access.ts index 3e0dcac1..452c8576 100644 --- a/feed-crawler/src/common/redis-access.ts +++ b/feed-crawler/src/common/redis-access.ts @@ -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; diff --git a/feed-crawler/src/common/rss-parser.ts b/feed-crawler/src/common/rss-parser.ts new file mode 100644 index 00000000..02c80e5a --- /dev/null +++ b/feed-crawler/src/common/rss-parser.ts @@ -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); + } +} diff --git a/feed-crawler/src/container.ts b/feed-crawler/src/container.ts index 92de3a7a..d7b6ce0d 100644 --- a/feed-crawler/src/container.ts +++ b/feed-crawler/src/container.ts @@ -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( DEPENDENCY_SYMBOLS.DatabaseConnection, - MySQLConnection + MySQLConnection, ); + container.registerSingleton( DEPENDENCY_SYMBOLS.RedisConnection, - RedisConnection + RedisConnection, ); + container.registerSingleton( DEPENDENCY_SYMBOLS.RssRepository, - RssRepository + RssRepository, ); + container.registerSingleton( DEPENDENCY_SYMBOLS.FeedRepository, - FeedRepository + FeedRepository, ); + container.registerSingleton( DEPENDENCY_SYMBOLS.TagMapRepository, - TagMapRepository + TagMapRepository, ); + container.registerSingleton( DEPENDENCY_SYMBOLS.ClaudeService, - ClaudeService + ClaudeService, ); + container.registerSingleton(DEPENDENCY_SYMBOLS.RssParser, RssParser); export { container }; diff --git a/feed-crawler/src/feed-crawler.ts b/feed-crawler/src/feed-crawler.ts index b8b0d303..38846508 100644 --- a/feed-crawler/src/feed-crawler.ts +++ b/feed-crawler/src/feed-crawler.ts @@ -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( @@ -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; @@ -39,7 +37,6 @@ export class FeedCrawler { return; } logger.info(`총 ${newFeeds.length}개의 새로운 피드가 있습니다.`); - const insertedData: FeedDetail[] = await this.feedRepository.insertFeeds( newFeeds, ); @@ -58,9 +55,7 @@ export class FeedCrawler { now: number, ): Promise { 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); @@ -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 = { - '·': '·', - ' ': ' ', - }; - Object.keys(escapeEntity).forEach((escapeKey) => { - const value = escapeEntity[escapeKey]; - const regex = new RegExp(escapeKey, 'g'); - feedTitle = feedTitle.replace(regex, value); - }); - - return unescape(feedTitle); - } -} diff --git a/feed-crawler/src/main.ts b/feed-crawler/src/main.ts index c0c13faa..c2c21cae 100644 --- a/feed-crawler/src/main.ts +++ b/feed-crawler/src/main.ts @@ -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'; diff --git a/feed-crawler/src/repository/feed.repository.ts b/feed-crawler/src/repository/feed.repository.ts index 27bb5f3b..02c7f740 100644 --- a/feed-crawler/src/repository/feed.repository.ts +++ b/feed-crawler/src/repository/feed.repository.ts @@ -12,7 +12,7 @@ export class FeedRepository { @inject(DEPENDENCY_SYMBOLS.DatabaseConnection) private readonly dbConnection: DatabaseConnection, @inject(DEPENDENCY_SYMBOLS.RedisConnection) - private readonly redisConnection: RedisConnection, + private readonly redisConnection: RedisConnection ) {} public async insertFeeds(resultData: FeedDetail[]) { @@ -47,9 +47,7 @@ export class FeedRepository { .filter((feed) => feed); logger.info( - `${process.env.NODE_ENV === 'test' ? '[SQLite]' : '[MySQL]'} ${ - insertedFeeds.length - }개의 피드 데이터가 성공적으로 데이터베이스에 삽입되었습니다.`, + `[MySQL] ${insertedFeeds.length}개의 피드 데이터가 성공적으로 데이터베이스에 삽입되었습니다.` ); return insertedFeeds; } @@ -62,7 +60,7 @@ export class FeedRepository { const [newCursor, keys] = await this.redisConnection.scan( cursor, redisConstant.FEED_RECENT_ALL_KEY, - 100, + 100 ); keysToDelete.push(...keys); cursor = newCursor; @@ -71,14 +69,14 @@ export class FeedRepository { if (keysToDelete.length > 0) { await this.redisConnection.del(...keysToDelete); } + logger.info(`[Redis] 최근 게시글 캐시가 정상적으로 삭제되었습니다.`); } catch (error) { logger.error( `[Redis] 최근 게시글 캐시를 삭제하는 도중 에러가 발생했습니다. 에러 메시지: ${error.message} - 스택 트레이스: ${error.stack}`, + 스택 트레이스: ${error.stack}` ); } - logger.info(`[Redis] 최근 게시글 캐시가 정상적으로 삭제되었습니다.`); } async setRecentFeedList(feedLists: FeedDetail[]) { @@ -98,14 +96,14 @@ export class FeedRepository { }); } }); + logger.info(`[Redis] 최근 게시글 캐시가 정상적으로 저장되었습니다.`); } catch (error) { logger.error( `[Redis] 최근 게시글 캐시를 저장하는 도중 에러가 발생했습니다. 에러 메시지: ${error.message} - 스택 트레이스: ${error.stack}`, + 스택 트레이스: ${error.stack}` ); } - logger.info(`[Redis] 최근 게시글 캐시가 정상적으로 저장되었습니다.`); } public updateSummary(feedId: number, summary: string) { diff --git a/feed-crawler/test/feed-crawling.e2e-spec.ts b/feed-crawler/test/feed-crawling.e2e-spec.ts index 98a5186a..49e1a81b 100644 --- a/feed-crawler/test/feed-crawling.e2e-spec.ts +++ b/feed-crawler/test/feed-crawling.e2e-spec.ts @@ -47,6 +47,7 @@ describe('feed crawling e2e-test', () => { 'SELECT * FROM feed', [], ); + const recentFeedsKeys = []; let cursor = '0'; do { diff --git a/feed-crawler/test/setup/testContext.setup.ts b/feed-crawler/test/setup/testContext.setup.ts index 6e763bb8..4094c07c 100644 --- a/feed-crawler/test/setup/testContext.setup.ts +++ b/feed-crawler/test/setup/testContext.setup.ts @@ -7,9 +7,9 @@ import { RssRepository } from '../../src/repository/rss.repository'; import { FeedRepository } from '../../src/repository/feed.repository'; import { container } from 'tsyringe'; import { DependencyContainer } from 'tsyringe'; +import { RssParser } from '../../src/common/rss-parser'; import { ClaudeService } from '../../src/claude.service'; import { TagMapRepository } from '../../src/repository/tag-map.repository'; -import { RssParser } from '../../src/feed-crawler'; export interface TestContext { container: DependencyContainer; diff --git a/server/.gitignore b/server/.gitignore index 1e495a9f..28c5b6f6 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -37,11 +37,7 @@ lerna-debug.log* # dotenv environment variable files .env.* -.env.development.local -.env.test.local -.env.production.local -.env.local -!.env.db.test +!.env.local # temp directory .temp .tmp diff --git a/server/env/.env.local b/server/env/.env.local new file mode 100644 index 00000000..674b957f --- /dev/null +++ b/server/env/.env.local @@ -0,0 +1,13 @@ +PORT=8080 +DB_TYPE=mysql +DB_DATABASE=denamu +DB_HOST=mysql-db +DB_PORT=3306 +DB_USERNAME=denamu-db-user +DB_PASSWORD=denamu-db-pw +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_USERNAME=denamu-redis-user +REDIS_PASSWORD=denamu-redis-pw +EMAIL_USER=? +EMAIL_PASSWORD=? \ No newline at end of file diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 5e83fd1e..85570184 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -16,7 +16,11 @@ import { TestModule } from './common/test/test.module'; ConfigModule.forRoot({ isGlobal: true, envFilePath: - process.env.ENV_PATH || `${process.cwd()}/configs/.env.db.production`, + { + PROD: `${process.cwd()}/env/.env.prod`, + LOCAL: `${process.cwd()}/env/.env.local`, + DEV: `${process.cwd()}/env/.env.local`, + }[process.env.NODE_ENV] || '', }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], diff --git a/server/src/common/cookie/cookie.config.ts b/server/src/common/cookie/cookie.config.ts index 09e092b1..3096065c 100644 --- a/server/src/common/cookie/cookie.config.ts +++ b/server/src/common/cookie/cookie.config.ts @@ -1,17 +1,17 @@ export const cookieConfig = { - production: { + PROD: { httpOnly: true, secure: true, sameSite: 'none', path: '/', }, - development: { + LOCAL: { httpOnly: true, - secure: false, - sameSite: 'lax', + secure: true, + sameSite: 'none', path: '/', }, - test: { + DEV: { httpOnly: true, secure: false, sameSite: 'lax', diff --git a/server/src/common/database/load.config.ts b/server/src/common/database/load.config.ts index 5e670a55..473209b8 100644 --- a/server/src/common/database/load.config.ts +++ b/server/src/common/database/load.config.ts @@ -9,7 +9,8 @@ export function loadDBSetting(configService: ConfigService) { const password = configService.get('DB_PASSWORD'); const entities = [`${__dirname}/../../**/*.entity.{js,ts}`]; const synchronize = true; - const logging = process.env.NODE_ENV === 'debug' ? true : false; + const logging = + process.env.NODE_ENV === 'LOCAL' || process.env.NODE_ENV === 'DEV'; return { type, diff --git a/server/src/common/logger/logger.config.ts b/server/src/common/logger/logger.config.ts index f93a9f43..04bd0f75 100644 --- a/server/src/common/logger/logger.config.ts +++ b/server/src/common/logger/logger.config.ts @@ -1,8 +1,5 @@ import * as winston from 'winston'; -import * as process from 'node:process'; - -// 로그 저장 경로 -export const logDir = `${__dirname}/../../../../logs`; +import * as DailyRotateFile from 'winston-daily-rotate-file'; // 로그 출력 포맷 정의 함수 export const logFormat = winston.format.printf( @@ -11,5 +8,48 @@ export const logFormat = winston.format.printf( }, ); -// production 환경과 development 환경을 구분하기 위한 flag -export const productionFlag = process.env.NODE_ENV === 'production'; +const logDir = `${process.cwd()}/logs`; + +export function getLogTransport() { + const transports = []; + transports.push(new winston.transports.Console()); + + if (process.env.NODE_ENV === 'LOCAL' || process.env.NODE_ENV === 'PROD') { + transports.push( + ...[ + //info 레벨 로그 파일 설정 + new DailyRotateFile({ + level: 'info', + datePattern: 'YYYY-MM-DD', + dirname: logDir, + filename: `%DATE%.log`, + maxFiles: 30, + zippedArchive: true, + }), + //error 레벨 로그 파일 설정 + new DailyRotateFile({ + level: 'error', + datePattern: 'YYYY-MM-DD', + dirname: `${logDir}/error`, + filename: `%DATE%.error.log`, + maxFiles: 30, + zippedArchive: true, + }), + ], + ); + } + + if (process.env.NODE_ENV === 'LOCAL' || process.env.NODE_ENV === 'DEV') { + transports.push( + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + logFormat, + ), + }), + ); + } + + return transports; +} diff --git a/server/src/common/logger/logger.interceptor.ts b/server/src/common/logger/logger.interceptor.ts index 301b772c..6dc0b8b2 100644 --- a/server/src/common/logger/logger.interceptor.ts +++ b/server/src/common/logger/logger.interceptor.ts @@ -28,7 +28,10 @@ export class LoggingInterceptor implements NestInterceptor { } return next.handle().pipe( finalize(() => { - if (process.env.NODE_ENV === 'development') { + if ( + process.env.NODE_ENV === 'LOCAL' || + process.env.NODE_ENV === 'DEV' + ) { const endTime = Date.now(); const elapsedTime = endTime - startTime; console.log(`Request Processing Time ${elapsedTime}ms`); diff --git a/server/src/common/logger/logger.module.ts b/server/src/common/logger/logger.module.ts index 6cef565b..240ca6f8 100644 --- a/server/src/common/logger/logger.module.ts +++ b/server/src/common/logger/logger.module.ts @@ -1,6 +1,5 @@ import { WinstonModule } from 'nest-winston'; -import { logDir, logFormat, productionFlag } from './logger.config'; -import * as DailyRotateFile from 'winston-daily-rotate-file'; +import { getLogTransport, logFormat } from './logger.config'; import * as winston from 'winston'; import { Global, Module } from '@nestjs/common'; import { WinstonLoggerService } from './logger.service'; @@ -11,40 +10,8 @@ const winstonModule = WinstonModule.forRoot({ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat, ), - - transports: [ - ...(productionFlag - ? [ - //info 레벨 로그 파일 설정 - new DailyRotateFile({ - level: 'info', - datePattern: 'YYYY-MM-DD', - dirname: logDir, - filename: `%DATE%.log`, - maxFiles: 30, - zippedArchive: true, - }), - //error 레벨 로그 파일 설정 - new DailyRotateFile({ - level: 'error', - datePattern: 'YYYY-MM-DD', - dirname: `${logDir}/error`, - filename: `%DATE%.error.log`, - maxFiles: 30, - zippedArchive: true, - }), - ] - : [ - // 콘솔에 로그 출력 - new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - logFormat, - ), - }), - ]), - ], + transports: getLogTransport(), + silent: process.env.NODE_ENV === 'test', }); @Global()