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', {