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))}