refactor: 백엔드에 dayjs 도입 및 날짜 유틸리티 통합
- lib/date.js: dayjs 기반 공통 날짜 유틸리티 모듈 추가 - toKST, formatDate, formatTime, utcToKSTDateTime, nowKST - parseNitterDateTime (Nitter 날짜 파싱) - dayjs/plugin/utc, timezone, customParseFormat 사용 - youtube-bot.js: 로컬 날짜 함수 제거, lib/date.js 사용 - x-bot.js: 로컬 날짜 함수 제거, lib/date.js 사용 - 중복 코드 제거로 유지보수성 향상
This commit is contained in:
parent
a3960489d4
commit
0e6e13fe65
7 changed files with 379 additions and 69 deletions
85
backend/lib/date.js
Normal file
85
backend/lib/date.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* 날짜/시간 유틸리티 (dayjs 기반)
|
||||||
|
* 백엔드 전체에서 공통으로 사용
|
||||||
|
*/
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import utc from "dayjs/plugin/utc.js";
|
||||||
|
import timezone from "dayjs/plugin/timezone.js";
|
||||||
|
import customParseFormat from "dayjs/plugin/customParseFormat.js";
|
||||||
|
|
||||||
|
// 플러그인 등록
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
|
// 기본 시간대: KST
|
||||||
|
const KST = "Asia/Seoul";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTC 시간을 KST로 변환
|
||||||
|
* @param {Date|string} utcDate - UTC 시간
|
||||||
|
* @returns {dayjs.Dayjs} KST 시간
|
||||||
|
*/
|
||||||
|
export function toKST(utcDate) {
|
||||||
|
return dayjs(utcDate).tz(KST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜를 YYYY-MM-DD 형식으로 포맷
|
||||||
|
* @param {Date|string|dayjs.Dayjs} date - 날짜
|
||||||
|
* @returns {string} YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
export function formatDate(date) {
|
||||||
|
return dayjs(date).format("YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시간을 HH:mm:ss 형식으로 포맷
|
||||||
|
* @param {Date|string|dayjs.Dayjs} date - 시간
|
||||||
|
* @returns {string} HH:mm:ss
|
||||||
|
*/
|
||||||
|
export function formatTime(date) {
|
||||||
|
return dayjs(date).format("HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UTC 시간을 KST로 변환 후 날짜/시간 분리
|
||||||
|
* @param {Date|string} utcDate - UTC 시간
|
||||||
|
* @returns {{date: string, time: string}} KST 날짜/시간
|
||||||
|
*/
|
||||||
|
export function utcToKSTDateTime(utcDate) {
|
||||||
|
const kst = toKST(utcDate);
|
||||||
|
return {
|
||||||
|
date: kst.format("YYYY-MM-DD"),
|
||||||
|
time: kst.format("HH:mm:ss"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 KST 시간 반환
|
||||||
|
* @returns {dayjs.Dayjs} 현재 KST 시간
|
||||||
|
*/
|
||||||
|
export function nowKST() {
|
||||||
|
return dayjs().tz(KST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nitter 날짜 문자열 파싱 (UTC 반환)
|
||||||
|
* 예: "Jan 9, 2026 · 4:00 PM UTC" → Date 객체
|
||||||
|
* @param {string} timeStr - Nitter 날짜 문자열
|
||||||
|
* @returns {dayjs.Dayjs|null} UTC 시간
|
||||||
|
*/
|
||||||
|
export function parseNitterDateTime(timeStr) {
|
||||||
|
if (!timeStr) return null;
|
||||||
|
try {
|
||||||
|
const cleaned = timeStr.replace(" · ", " ").replace(" UTC", "");
|
||||||
|
const date = dayjs.utc(cleaned, "MMM D, YYYY h:mm A");
|
||||||
|
if (!date.isValid()) return null;
|
||||||
|
return date;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dayjs;
|
||||||
236
backend/package-lock.json
generated
236
backend/package-lock.json
generated
|
|
@ -10,7 +10,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.700.0",
|
"@aws-sdk/client-s3": "^3.700.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"inko": "^1.1.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"meilisearch": "^0.55.0",
|
"meilisearch": "^0.55.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|
@ -2055,6 +2057,12 @@
|
||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/bcrypt": {
|
"node_modules/bcrypt": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
|
@ -2099,6 +2107,22 @@
|
||||||
"integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
|
"integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browser-stdout": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
|
@ -2201,6 +2225,18 @@
|
||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
|
||||||
|
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "1.6.2",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||||
|
|
@ -2258,6 +2294,12 @@
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||||
|
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
|
@ -2304,6 +2346,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -2387,6 +2438,15 @@
|
||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/etag": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
|
@ -2496,6 +2556,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -2551,6 +2617,24 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
|
@ -2563,6 +2647,24 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/growl": {
|
||||||
|
"version": "1.10.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
|
||||||
|
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
|
@ -2587,6 +2689,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/he": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"he": "bin/he"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
|
@ -2619,12 +2730,35 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/inko": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/inko/-/inko-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Lr+HH4xr1eT0OKokYXjr+lRk6ubVEw6iCpOEGsdeokwLsvLXODWZG2XuzvTOlCl5hEVApK8TVWSkWnf55c6RJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mocha": "^5.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|
@ -2839,6 +2973,18 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
|
@ -2860,6 +3006,60 @@
|
||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mocha": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"browser-stdout": "1.3.1",
|
||||||
|
"commander": "2.15.1",
|
||||||
|
"debug": "3.1.0",
|
||||||
|
"diff": "3.5.0",
|
||||||
|
"escape-string-regexp": "1.0.5",
|
||||||
|
"glob": "7.1.2",
|
||||||
|
"growl": "1.10.5",
|
||||||
|
"he": "1.1.1",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
|
"mkdirp": "0.5.1",
|
||||||
|
"supports-color": "5.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"_mocha": "bin/_mocha",
|
||||||
|
"mocha": "bin/mocha"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mocha/node_modules/debug": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mocha/node_modules/minimist": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mocha/node_modules/mkdirp": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==",
|
||||||
|
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "0.0.8"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
|
@ -3004,6 +3204,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
|
@ -3013,6 +3222,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
|
|
@ -3381,6 +3599,18 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -3448,6 +3678,12 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/xml2js": {
|
"node_modules/xml2js": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.700.0",
|
"@aws-sdk/client-s3": "^3.700.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"inko": "^1.1.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"meilisearch": "^0.55.0",
|
"meilisearch": "^0.55.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,22 @@ router.get("/", async (req, res) => {
|
||||||
// 검색어가 있으면 Meilisearch 사용
|
// 검색어가 있으면 Meilisearch 사용
|
||||||
if (search && search.trim()) {
|
if (search && search.trim()) {
|
||||||
const offset = parseInt(req.query.offset) || 0;
|
const offset = parseInt(req.query.offset) || 0;
|
||||||
const pageLimit = parseInt(req.query.limit) || 20;
|
const pageLimit = parseInt(req.query.limit) || 100;
|
||||||
|
|
||||||
|
// Meilisearch에서 큰 limit으로 검색 (유사도 필터링 후 클라이언트 페이징)
|
||||||
const results = await searchSchedules(search.trim(), {
|
const results = await searchSchedules(search.trim(), {
|
||||||
offset,
|
limit: 1000, // 내부적으로 1000개까지 검색
|
||||||
limit: pageLimit,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 페이징 적용
|
||||||
|
const paginatedHits = results.hits.slice(offset, offset + pageLimit);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
schedules: results.hits,
|
schedules: paginatedHits,
|
||||||
total: results.total,
|
total: results.total,
|
||||||
offset: results.offset,
|
offset: offset,
|
||||||
limit: results.limit,
|
limit: pageLimit,
|
||||||
hasMore: results.offset + results.hits.length < results.total,
|
hasMore: offset + paginatedHits.length < results.total,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,19 @@ export async function deleteSchedule(scheduleId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import Inko from "inko";
|
||||||
|
const inko = new Inko();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영문 자판으로 입력된 검색어인지 확인 (대부분 영문으로만 구성)
|
||||||
|
*/
|
||||||
|
function isEnglishKeyboard(text) {
|
||||||
|
const englishChars = text.match(/[a-zA-Z]/g) || [];
|
||||||
|
const koreanChars = text.match(/[가-힣ㄱ-ㅎㅏ-ㅣ]/g) || [];
|
||||||
|
// 영문이 50% 이상이고 한글이 없으면 영문 자판 입력으로 간주
|
||||||
|
return englishChars.length > 0 && koreanChars.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 일정 검색 (페이징 지원)
|
* 일정 검색 (페이징 지원)
|
||||||
*/
|
*/
|
||||||
|
|
@ -134,10 +147,33 @@ export async function searchSchedules(query, options = {}) {
|
||||||
searchOptions.sort = options.sort;
|
searchOptions.sort = options.sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 원본 검색어로 검색
|
||||||
const results = await index.search(query, searchOptions);
|
const results = await index.search(query, searchOptions);
|
||||||
|
let allHits = [...results.hits];
|
||||||
|
|
||||||
|
// 영문 자판 입력인 경우 한글로 변환하여 추가 검색
|
||||||
|
if (isEnglishKeyboard(query)) {
|
||||||
|
const koreanQuery = inko.en2ko(query);
|
||||||
|
if (koreanQuery !== query) {
|
||||||
|
const koreanResults = await index.search(koreanQuery, searchOptions);
|
||||||
|
// 중복 제거하며 병합 (id 기준)
|
||||||
|
const existingIds = new Set(allHits.map((h) => h.id));
|
||||||
|
for (const hit of koreanResults.hits) {
|
||||||
|
if (!existingIds.has(hit.id)) {
|
||||||
|
allHits.push(hit);
|
||||||
|
existingIds.add(hit.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 유사도 0.5 미만인 결과 필터링
|
// 유사도 0.5 미만인 결과 필터링
|
||||||
const filteredHits = results.hits.filter((hit) => hit._rankingScore >= 0.5);
|
const filteredHits = allHits.filter((hit) => hit._rankingScore >= 0.5);
|
||||||
|
|
||||||
|
// 유사도 순으로 정렬
|
||||||
|
filteredHits.sort(
|
||||||
|
(a, b) => (b._rankingScore || 0) - (a._rankingScore || 0)
|
||||||
|
);
|
||||||
|
|
||||||
// 페이징 정보 포함 반환
|
// 페이징 정보 포함 반환
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@
|
||||||
|
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { addOrUpdateSchedule } from "./meilisearch.js";
|
import { addOrUpdateSchedule } from "./meilisearch.js";
|
||||||
|
import {
|
||||||
|
toKST,
|
||||||
|
formatDate,
|
||||||
|
formatTime,
|
||||||
|
parseNitterDateTime,
|
||||||
|
} from "../lib/date.js";
|
||||||
|
|
||||||
// YouTube API 키
|
// YouTube API 키
|
||||||
const YOUTUBE_API_KEY =
|
const YOUTUBE_API_KEY =
|
||||||
|
|
@ -19,43 +25,6 @@ const X_CATEGORY_ID = 12;
|
||||||
// 유튜브 카테고리 ID
|
// 유튜브 카테고리 ID
|
||||||
const YOUTUBE_CATEGORY_ID = 7;
|
const YOUTUBE_CATEGORY_ID = 7;
|
||||||
|
|
||||||
/**
|
|
||||||
* UTC → KST 변환
|
|
||||||
*/
|
|
||||||
export function toKST(utcDate) {
|
|
||||||
const date = new Date(utcDate);
|
|
||||||
return new Date(date.getTime() + 9 * 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 날짜를 YYYY-MM-DD 형식으로 변환
|
|
||||||
*/
|
|
||||||
export function formatDate(date) {
|
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 시간을 HH:MM:SS 형식으로 변환
|
|
||||||
*/
|
|
||||||
export function formatTime(date) {
|
|
||||||
return date.toTimeString().split(" ")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nitter 날짜 파싱 ("Jan 7, 2026 · 12:00 PM UTC" → Date)
|
|
||||||
*/
|
|
||||||
function parseNitterDateTime(timeStr) {
|
|
||||||
if (!timeStr) return null;
|
|
||||||
try {
|
|
||||||
const cleaned = timeStr.replace(" · ", " ").replace(" UTC", "");
|
|
||||||
const date = new Date(cleaned + " UTC");
|
|
||||||
if (isNaN(date.getTime())) return null;
|
|
||||||
return date;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 트윗 텍스트에서 첫 문단 추출 (title용)
|
* 트윗 텍스트에서 첫 문단 추출 (title용)
|
||||||
*/
|
*/
|
||||||
|
|
@ -625,5 +594,4 @@ export default {
|
||||||
syncAllTweets,
|
syncAllTweets,
|
||||||
extractTitle,
|
extractTitle,
|
||||||
extractYoutubeVideoIds,
|
extractYoutubeVideoIds,
|
||||||
toKST,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import Parser from "rss-parser";
|
import Parser from "rss-parser";
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { addOrUpdateSchedule } from "./meilisearch.js";
|
import { addOrUpdateSchedule } from "./meilisearch.js";
|
||||||
|
import { toKST, formatDate, formatTime } from "../lib/date.js";
|
||||||
|
|
||||||
// YouTube API 키
|
// YouTube API 키
|
||||||
const YOUTUBE_API_KEY =
|
const YOUTUBE_API_KEY =
|
||||||
|
|
@ -41,28 +42,6 @@ const rssParser = new Parser({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* UTC → KST 변환
|
|
||||||
*/
|
|
||||||
export function toKST(utcDate) {
|
|
||||||
const date = new Date(utcDate);
|
|
||||||
return new Date(date.getTime() + 9 * 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 날짜를 YYYY-MM-DD 형식으로 변환
|
|
||||||
*/
|
|
||||||
export function formatDate(date) {
|
|
||||||
return date.toISOString().split("T")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 시간을 HH:MM:SS 형식으로 변환
|
|
||||||
*/
|
|
||||||
export function formatTime(date) {
|
|
||||||
return date.toTimeString().split(" ")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* '유튜브' 카테고리 ID 조회 (없으면 생성)
|
* '유튜브' 카테고리 ID 조회 (없으면 생성)
|
||||||
*/
|
*/
|
||||||
|
|
@ -666,5 +645,4 @@ export default {
|
||||||
syncNewVideos,
|
syncNewVideos,
|
||||||
syncAllVideos,
|
syncAllVideos,
|
||||||
getYoutubeCategory,
|
getYoutubeCategory,
|
||||||
toKST,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue