diff --git a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx
index c19cb00..91f999d 100644
--- a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx
+++ b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx
@@ -3,11 +3,47 @@ import { useQuery, keepPreviousData } from '@tanstack/react-query';
import { useEffect, useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-react';
-import Linkify from 'react-linkify';
import { getSchedule } from '@/api';
import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTimeWithTime } from '@/utils';
import Birthday from './Birthday';
+/**
+ * URL을 링크로 변환하는 함수
+ */
+function linkifyText(text) {
+ if (!text) return null;
+
+ // URL 패턴: http(s)://로 시작하거나 일반적인 단축 URL 도메인
+ const urlPattern = /(https?:\/\/[^\s]+|(?:bit\.ly|youtu\.be|t\.co|goo\.gl|tinyurl\.com)\/[^\s]+)/gi;
+
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+
+ while ((match = urlPattern.exec(text)) !== null) {
+ if (match.index > lastIndex) {
+ parts.push(text.slice(lastIndex, match.index));
+ }
+
+ let url = match[0];
+ const href = url.startsWith('http') ? url : `https://${url}`;
+
+ parts.push(
+
+ {url}
+
+ );
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ if (lastIndex < text.length) {
+ parts.push(text.slice(lastIndex));
+ }
+
+ return parts.length > 0 ? parts : text;
+}
+
/**
* 특수 일정 ID 파싱
* @param {string} id - 일정 ID
@@ -228,12 +264,6 @@ function MobileXSection({ schedule }) {
return () => window.removeEventListener('popstate', handlePopState);
}, [lightboxOpen]);
- // 링크 데코레이터
- const linkDecorator = (href, text, key) => (
-
- {text}
-
- );
return (
<>
@@ -268,7 +298,7 @@ function MobileXSection({ schedule }) {
{/* 본문 */}
- {decodeHtmlEntities(schedule.content || schedule.title)}
+ {linkifyText(decodeHtmlEntities(schedule.content || schedule.title))}
diff --git a/frontend/src/pages/pc/public/schedule/sections/XSection.jsx b/frontend/src/pages/pc/public/schedule/sections/XSection.jsx
index 05f91a1..2996f36 100644
--- a/frontend/src/pages/pc/public/schedule/sections/XSection.jsx
+++ b/frontend/src/pages/pc/public/schedule/sections/XSection.jsx
@@ -1,9 +1,49 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { motion } from 'framer-motion';
-import Linkify from 'react-linkify';
import { decodeHtmlEntities, formatXDateTimeWithTime } from './utils';
import { Lightbox } from '@/components/common';
+/**
+ * URL을 링크로 변환하는 함수
+ */
+function linkifyText(text) {
+ if (!text) return null;
+
+ // URL 패턴: http(s)://로 시작하거나 일반적인 도메인 패턴
+ const urlPattern = /(https?:\/\/[^\s]+|(?:bit\.ly|youtu\.be|t\.co|goo\.gl|tinyurl\.com)\/[^\s]+)/gi;
+
+ const parts = [];
+ let lastIndex = 0;
+ let match;
+
+ while ((match = urlPattern.exec(text)) !== null) {
+ // 매치 전 텍스트
+ if (match.index > lastIndex) {
+ parts.push(text.slice(lastIndex, match.index));
+ }
+
+ // URL
+ let url = match[0];
+ // http(s)://가 없으면 추가
+ const href = url.startsWith('http') ? url : `https://${url}`;
+
+ parts.push(
+
+ {url}
+
+ );
+
+ lastIndex = match.index + match[0].length;
+ }
+
+ // 나머지 텍스트
+ if (lastIndex < text.length) {
+ parts.push(text.slice(lastIndex));
+ }
+
+ return parts.length > 0 ? parts : text;
+}
+
/**
* PC X(트위터) 섹션 컴포넌트
*/
@@ -46,13 +86,6 @@ function XSection({ schedule }) {
return () => window.removeEventListener('popstate', handlePopState);
}, [lightboxOpen]);
- // 링크 데코레이터 (새 탭에서 열기)
- const linkDecorator = (href, text, key) => (
-
- {text}
-
- );
-
return (
{/* X 스타일 카드 */}
@@ -88,7 +121,7 @@ function XSection({ schedule }) {
{/* 본문 */}
- {decodeHtmlEntities(schedule.content || schedule.title)}
+ {linkifyText(decodeHtmlEntities(schedule.content || schedule.title))}