From 8ec2b1d6bec6508cfac870b38f74a32dfca648df Mon Sep 17 00:00:00 2001 From: caadiq Date: Thu, 15 Jan 2026 21:02:48 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EC=9D=BC?= =?UTF-8?q?=EC=A0=95=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?X=20=EC=84=B9=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - X 스타일 카드 UI (프로필, 본문, 이미지, 날짜) - 프로필 정보 API 연동 (getXProfile) - X 스타일 날짜/시간 포맷팅 - X에서 보기 버튼 Co-Authored-By: Claude Opus 4.5 --- .../pages/mobile/public/ScheduleDetail.jsx | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/frontend/src/pages/mobile/public/ScheduleDetail.jsx b/frontend/src/pages/mobile/public/ScheduleDetail.jsx index 1978462..49616fe 100644 --- a/frontend/src/pages/mobile/public/ScheduleDetail.jsx +++ b/frontend/src/pages/mobile/public/ScheduleDetail.jsx @@ -90,6 +90,33 @@ const formatTime = (timeStr) => { return timeStr.slice(0, 5); }; +// X URL에서 username 추출 +const extractXUsername = (url) => { + if (!url) return null; + const match = url.match(/(?:twitter\.com|x\.com)\/([^/]+)/); + return match ? match[1] : null; +}; + +// X용 날짜/시간 포맷팅 (오후 2:30 · 2026년 1월 15일) +const formatXDateTime = (dateStr, timeStr) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + let result = `${year}년 ${month}월 ${day}일`; + + if (timeStr) { + const [hours, minutes] = timeStr.split(':').map(Number); + const period = hours < 12 ? '오전' : '오후'; + const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; + result = `${period} ${hour12}:${String(minutes).padStart(2, '0')} · ${result}`; + } + + return result; +}; + // 유튜브 섹션 컴포넌트 function YoutubeSection({ schedule }) { const videoId = extractYoutubeVideoId(schedule.source_url); @@ -194,6 +221,104 @@ function YoutubeSection({ schedule }) { ); } +// X(트위터) 섹션 컴포넌트 +function XSection({ schedule }) { + const username = extractXUsername(schedule.source_url); + + // 프로필 정보 조회 + const { data: profile } = useQuery({ + queryKey: ['x-profile', username], + queryFn: () => getXProfile(username), + enabled: !!username, + staleTime: 1000 * 60 * 60, // 1시간 + }); + + const displayName = profile?.displayName || schedule.source_name || username || 'Unknown'; + const avatarUrl = profile?.avatarUrl; + + return ( + + {/* 헤더 */} +
+
+ {/* 프로필 이미지 */} + {avatarUrl ? ( + {displayName} + ) : ( +
+ + {displayName.charAt(0).toUpperCase()} + +
+ )} +
+
+ + {displayName} + + + + +
+ {username && ( + @{username} + )} +
+
+
+ + {/* 본문 */} +
+

+ {decodeHtmlEntities(schedule.description || schedule.title)} +

+
+ + {/* 이미지 */} + {schedule.image_url && ( +
+ +
+ )} + + {/* 날짜/시간 */} +
+ + {formatXDateTime(schedule.date, schedule.time)} + +
+ + {/* X에서 보기 버튼 */} +
+ + + + + X에서 보기 + +
+
+ ); +} + // 기본 섹션 컴포넌트 (다른 카테고리용 - 임시) function DefaultSection({ schedule }) { return ( @@ -341,6 +466,8 @@ function MobileScheduleDetail() { switch (schedule.category_id) { case CATEGORY_ID.YOUTUBE: return ; + case CATEGORY_ID.X: + return ; default: return ; }