YouTube 상세 페이지 API 응답 구조 변경 대응
- schedule.source.url → schedule.videoUrl - schedule.source.name → schedule.channelName - schedule.date/time → schedule.datetime - videoId를 URL에서 추출 대신 API에서 직접 사용 - formatXDateTime 유틸리티 사용 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
beabcc094f
commit
bdd5b90870
2 changed files with 15 additions and 56 deletions
|
|
@ -144,19 +144,6 @@ const decodeHtmlEntities = (text) => {
|
||||||
return textarea.value;
|
return textarea.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 유튜브 비디오 ID 추출 (YoutubeSection용)
|
|
||||||
const extractYoutubeVideoId = (url) => {
|
|
||||||
if (!url) return null;
|
|
||||||
const shortMatch = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
|
|
||||||
if (shortMatch) return shortMatch[1];
|
|
||||||
const watchMatch = url.match(/youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/);
|
|
||||||
if (watchMatch) return watchMatch[1];
|
|
||||||
const shortsMatch = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
|
|
||||||
if (shortsMatch) return shortsMatch[1];
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 날짜 포맷팅
|
// 날짜 포맷팅
|
||||||
const formatFullDate = (dateStr) => {
|
const formatFullDate = (dateStr) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
|
|
@ -180,8 +167,8 @@ const extractXUsername = (url) => {
|
||||||
|
|
||||||
// 유튜브 섹션 컴포넌트
|
// 유튜브 섹션 컴포넌트
|
||||||
function YoutubeSection({ schedule }) {
|
function YoutubeSection({ schedule }) {
|
||||||
const videoId = extractYoutubeVideoId(schedule.source?.url);
|
const videoId = schedule.videoId;
|
||||||
const isShorts = schedule.source?.url?.includes('/shorts/');
|
const isShorts = schedule.videoType === 'shorts';
|
||||||
|
|
||||||
// 전체화면 시 가로 회전 (숏츠 제외)
|
// 전체화면 시 가로 회전 (숏츠 제외)
|
||||||
useFullscreenOrientation(isShorts);
|
useFullscreenOrientation(isShorts);
|
||||||
|
|
@ -229,18 +216,12 @@ function YoutubeSection({ schedule }) {
|
||||||
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500 mb-3">
|
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500 mb-3">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Calendar size={12} />
|
<Calendar size={12} />
|
||||||
<span>{formatFullDate(schedule.date)}</span>
|
<span>{formatXDateTime(schedule.datetime)}</span>
|
||||||
</div>
|
</div>
|
||||||
{schedule.time && (
|
{schedule.channelName && (
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Clock size={12} />
|
|
||||||
<span>{formatTime(schedule.time)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{schedule.source?.name && (
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Link2 size={12} />
|
<Link2 size={12} />
|
||||||
<span>{schedule.source?.name}</span>
|
<span>{schedule.channelName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -267,7 +248,7 @@ function YoutubeSection({ schedule }) {
|
||||||
|
|
||||||
{/* 유튜브에서 보기 버튼 */}
|
{/* 유튜브에서 보기 버튼 */}
|
||||||
<a
|
<a
|
||||||
href={schedule.source?.url}
|
href={schedule.videoUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center gap-2 w-full py-3 bg-red-500 active:bg-red-600 text-white rounded-xl font-medium transition-colors"
|
className="flex items-center justify-center gap-2 w-full py-3 bg-red-500 active:bg-red-600 text-white rounded-xl font-medium transition-colors"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Calendar, Clock, Link2 } from 'lucide-react';
|
import { Calendar, Clock, Link2 } from 'lucide-react';
|
||||||
import { decodeHtmlEntities, formatFullDate, formatTime } from './utils';
|
import { decodeHtmlEntities } from './utils';
|
||||||
|
import { formatXDateTime } from '../../../../utils/date';
|
||||||
|
|
||||||
// 영상 정보 컴포넌트 (공통)
|
// 영상 정보 컴포넌트 (공통)
|
||||||
function VideoInfo({ schedule, isShorts }) {
|
function VideoInfo({ schedule, isShorts }) {
|
||||||
|
|
@ -16,30 +17,19 @@ function VideoInfo({ schedule, isShorts }) {
|
||||||
|
|
||||||
{/* 메타 정보 */}
|
{/* 메타 정보 */}
|
||||||
<div className={`flex flex-wrap items-center gap-4 text-sm ${isShorts ? 'gap-3' : ''}`}>
|
<div className={`flex flex-wrap items-center gap-4 text-sm ${isShorts ? 'gap-3' : ''}`}>
|
||||||
{/* 날짜 */}
|
{/* 날짜/시간 */}
|
||||||
<div className="flex items-center gap-1.5 text-gray-500">
|
<div className="flex items-center gap-1.5 text-gray-500">
|
||||||
<Calendar size={14} />
|
<Calendar size={14} />
|
||||||
<span>{formatFullDate(schedule.date)}</span>
|
<span>{formatXDateTime(schedule.datetime)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 시간 */}
|
|
||||||
{schedule.time && (
|
|
||||||
<>
|
|
||||||
<div className="w-px h-4 bg-gray-300" />
|
|
||||||
<div className="flex items-center gap-1.5 text-gray-500">
|
|
||||||
<Clock size={14} />
|
|
||||||
<span>{formatTime(schedule.time)}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 채널명 */}
|
{/* 채널명 */}
|
||||||
{schedule.source?.name && (
|
{schedule.channelName && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-4 bg-gray-300" />
|
<div className="w-px h-4 bg-gray-300" />
|
||||||
<div className="flex items-center gap-2 text-gray-500">
|
<div className="flex items-center gap-2 text-gray-500">
|
||||||
<Link2 size={14} className="opacity-60" />
|
<Link2 size={14} className="opacity-60" />
|
||||||
<span className="font-medium">{schedule.source.name}</span>
|
<span className="font-medium">{schedule.channelName}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -68,7 +58,7 @@ function VideoInfo({ schedule, isShorts }) {
|
||||||
{/* 유튜브에서 보기 버튼 */}
|
{/* 유튜브에서 보기 버튼 */}
|
||||||
<div className="mt-6 pt-5 border-t border-gray-200">
|
<div className="mt-6 pt-5 border-t border-gray-200">
|
||||||
<a
|
<a
|
||||||
href={schedule.source?.url}
|
href={schedule.videoUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl font-medium transition-colors shadow-lg shadow-red-500/20"
|
className="inline-flex items-center gap-2 px-5 py-2.5 bg-red-500 hover:bg-red-600 text-white rounded-xl font-medium transition-colors shadow-lg shadow-red-500/20"
|
||||||
|
|
@ -83,22 +73,10 @@ function VideoInfo({ schedule, isShorts }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 유튜브 비디오 ID 추출
|
|
||||||
const extractYoutubeVideoId = (url) => {
|
|
||||||
if (!url) return null;
|
|
||||||
const shortMatch = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
|
|
||||||
if (shortMatch) return shortMatch[1];
|
|
||||||
const watchMatch = url.match(/youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/);
|
|
||||||
if (watchMatch) return watchMatch[1];
|
|
||||||
const shortsMatch = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
|
|
||||||
if (shortsMatch) return shortsMatch[1];
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 유튜브 섹션 컴포넌트
|
// 유튜브 섹션 컴포넌트
|
||||||
function YoutubeSection({ schedule }) {
|
function YoutubeSection({ schedule }) {
|
||||||
const videoId = extractYoutubeVideoId(schedule.source?.url);
|
const videoId = schedule.videoId;
|
||||||
const isShorts = schedule.source?.url?.includes('/shorts/');
|
const isShorts = schedule.videoType === 'shorts';
|
||||||
|
|
||||||
if (!videoId) return null;
|
if (!videoId) return null;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue