fix(schedule): X 일정에서 단축 URL 인식 개선

- react-linkify 대신 커스텀 linkifyText 함수 사용
- bit.ly, t.co, youtu.be 등 단축 URL 도메인 지원
- PC, 모바일 양쪽 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-02-08 22:28:41 +09:00
parent d01f7e60dc
commit 406098d1b9
2 changed files with 80 additions and 17 deletions

View file

@ -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(
<a key={match.index} href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">
{url}
</a>
);
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) => (
<a key={key} href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500">
{text}
</a>
);
return (
<>
@ -268,7 +298,7 @@ function MobileXSection({ schedule }) {
{/* 본문 */}
<div className="p-4">
<p className="text-gray-900 text-[15px] leading-relaxed whitespace-pre-wrap">
<Linkify componentDecorator={linkDecorator}>{decodeHtmlEntities(schedule.content || schedule.title)}</Linkify>
{linkifyText(decodeHtmlEntities(schedule.content || schedule.title))}
</p>
</div>

View file

@ -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(
<a key={match.index} href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">
{url}
</a>
);
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) => (
<a key={key} href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">
{text}
</a>
);
return (
<div className="max-w-2xl mx-auto">
{/* X 스타일 카드 */}
@ -88,7 +121,7 @@ function XSection({ schedule }) {
{/* 본문 */}
<div className="p-5">
<p className="text-gray-900 text-[17px] leading-relaxed whitespace-pre-wrap">
<Linkify componentDecorator={linkDecorator}>{decodeHtmlEntities(schedule.content || schedule.title)}</Linkify>
{linkifyText(decodeHtmlEntities(schedule.content || schedule.title))}
</p>
</div>