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:
caadiq 2026-01-10 19:44:07 +09:00
parent a3960489d4
commit 0e6e13fe65
7 changed files with 379 additions and 69 deletions

85
backend/lib/date.js Normal file
View 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;

View file

@ -10,7 +10,9 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.700.0",
"bcrypt": "^6.0.0",
"dayjs": "^1.11.19",
"express": "^4.18.2",
"inko": "^1.1.1",
"jsonwebtoken": "^9.0.3",
"meilisearch": "^0.55.0",
"multer": "^1.4.5-lts.1",
@ -2055,6 +2057,12 @@
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@ -2099,6 +2107,22 @@
"integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
"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": {
"version": "1.0.1",
"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"
}
},
"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": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
@ -2258,6 +2294,12 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"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": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -2304,6 +2346,15 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -2387,6 +2438,15 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"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": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -2496,6 +2556,12 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -2551,6 +2617,24 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -2563,6 +2647,24 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -2587,6 +2689,15 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@ -2619,12 +2730,35 @@
"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": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -2839,6 +2973,18 @@
"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": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
@ -2860,6 +3006,60 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -3004,6 +3204,15 @@
"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": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -3013,6 +3222,15 @@
"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": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
@ -3381,6 +3599,18 @@
],
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -3448,6 +3678,12 @@
"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": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",

View file

@ -9,7 +9,9 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.700.0",
"bcrypt": "^6.0.0",
"dayjs": "^1.11.19",
"express": "^4.18.2",
"inko": "^1.1.1",
"jsonwebtoken": "^9.0.3",
"meilisearch": "^0.55.0",
"multer": "^1.4.5-lts.1",

View file

@ -12,17 +12,22 @@ router.get("/", async (req, res) => {
// 검색어가 있으면 Meilisearch 사용
if (search && search.trim()) {
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(), {
offset,
limit: pageLimit,
limit: 1000, // 내부적으로 1000개까지 검색
});
// 페이징 적용
const paginatedHits = results.hits.slice(offset, offset + pageLimit);
return res.json({
schedules: results.hits,
schedules: paginatedHits,
total: results.total,
offset: results.offset,
limit: results.limit,
hasMore: results.offset + results.hits.length < results.total,
offset: offset,
limit: pageLimit,
hasMore: offset + paginatedHits.length < results.total,
});
}

View file

@ -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;
}
// 원본 검색어로 검색
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 미만인 결과 필터링
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 {

View file

@ -8,6 +8,12 @@
import pool from "../lib/db.js";
import { addOrUpdateSchedule } from "./meilisearch.js";
import {
toKST,
formatDate,
formatTime,
parseNitterDateTime,
} from "../lib/date.js";
// YouTube API 키
const YOUTUBE_API_KEY =
@ -19,43 +25,6 @@ const X_CATEGORY_ID = 12;
// 유튜브 카테고리 ID
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용)
*/
@ -625,5 +594,4 @@ export default {
syncAllTweets,
extractTitle,
extractYoutubeVideoIds,
toKST,
};

View file

@ -1,6 +1,7 @@
import Parser from "rss-parser";
import pool from "../lib/db.js";
import { addOrUpdateSchedule } from "./meilisearch.js";
import { toKST, formatDate, formatTime } from "../lib/date.js";
// YouTube API 키
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 조회 (없으면 생성)
*/
@ -666,5 +645,4 @@ export default {
syncNewVideos,
syncAllVideos,
getYoutubeCategory,
toKST,
};