From 149e85ebd970a669fd4be04f0aedd816d6a97587 Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 19 Jan 2026 10:19:32 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20X=20=EB=B4=87=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=A0=95=EB=B3=B4=20DB=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - x_profiles 테이블 생성 (username, display_name, avatar_url) - saveProfile 함수로 DB + Redis 캐시 동시 저장 - getProfile 함수 Redis → DB 폴백 지원 Co-Authored-By: Claude Opus 4.5 --- backend/sql/x_profiles.sql | 10 +++++++ backend/src/services/x/index.js | 53 ++++++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 backend/sql/x_profiles.sql diff --git a/backend/sql/x_profiles.sql b/backend/sql/x_profiles.sql new file mode 100644 index 0000000..19ae9ae --- /dev/null +++ b/backend/sql/x_profiles.sql @@ -0,0 +1,10 @@ +-- X 프로필 테이블 +CREATE TABLE IF NOT EXISTS x_profiles ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + display_name VARCHAR(100), + avatar_url TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_username (username) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/src/services/x/index.js b/backend/src/services/x/index.js index 82eaf09..699ac19 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -20,11 +20,22 @@ async function xBotPlugin(fastify, opts) { } /** - * X 프로필 캐시 저장 + * X 프로필 저장 (DB + Redis 캐시) */ - async function cacheProfile(username, profile) { + async function saveProfile(username, profile) { if (!profile.displayName && !profile.avatarUrl) return; + // DB에 저장 (upsert) + await fastify.db.query(` + INSERT INTO x_profiles (username, display_name, avatar_url) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE + display_name = VALUES(display_name), + avatar_url = VALUES(avatar_url), + updated_at = CURRENT_TIMESTAMP + `, [username, profile.displayName, profile.avatarUrl]); + + // Redis 캐시에도 저장 const data = { username, displayName: profile.displayName, @@ -139,8 +150,8 @@ async function xBotPlugin(fastify, opts) { async function syncNewTweets(bot) { const { tweets, profile } = await fetchTweets(bot.nitterUrl, bot.username); - // 프로필 캐시 업데이트 - await cacheProfile(bot.username, profile); + // 프로필 저장 (DB + 캐시) + await saveProfile(bot.username, profile); let addedCount = 0; let ytAddedCount = 0; @@ -178,11 +189,39 @@ async function xBotPlugin(fastify, opts) { } /** - * X 프로필 조회 + * X 프로필 조회 (Redis 캐시 → DB) */ async function getProfile(username) { - const data = await fastify.redis.get(`${PROFILE_CACHE_PREFIX}${username}`); - return data ? JSON.parse(data) : null; + // Redis 캐시 확인 + const cached = await fastify.redis.get(`${PROFILE_CACHE_PREFIX}${username}`); + if (cached) { + return JSON.parse(cached); + } + + // DB에서 조회 + const [rows] = await fastify.db.query( + 'SELECT username, display_name, avatar_url, updated_at FROM x_profiles WHERE username = ?', + [username] + ); + + if (rows.length > 0) { + const row = rows[0]; + const data = { + username: row.username, + displayName: row.display_name, + avatarUrl: row.avatar_url, + updatedAt: row.updated_at?.toISOString(), + }; + // Redis 캐시에 저장 + await fastify.redis.setex( + `${PROFILE_CACHE_PREFIX}${username}`, + PROFILE_TTL, + JSON.stringify(data) + ); + return data; + } + + return null; } fastify.decorate('xBot', {