fix(frontend): YouTube 수정 페이지 API 대응 및 사전 페이지 중복 요청 수정
- YouTubeEditForm: 변경된 API 응답 구조에 맞게 수정 - schedule.youtube?.videoId → schedule.videoId - schedule.youtube?.channelName → schedule.channelName - schedule.date/time → schedule.datetime 파싱 - AdminScheduleDict: useEffect → useQuery 변경으로 중복 요청 방지 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7593004bd6
commit
46469fd324
2 changed files with 45 additions and 38 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Home, ChevronRight, Book, Plus, Trash2, Search, ChevronDown } from 'lucide-react';
|
import { Home, ChevronRight, Book, Plus, Trash2, Search, ChevronDown } from 'lucide-react';
|
||||||
import Toast from '../../../components/Toast';
|
import Toast from '../../../components/Toast';
|
||||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||||
|
|
@ -171,8 +172,8 @@ function WordItem({ id, word, pos, index, onUpdate, onDelete }) {
|
||||||
function AdminScheduleDict() {
|
function AdminScheduleDict() {
|
||||||
const { user, isAuthenticated } = useAdminAuth();
|
const { user, isAuthenticated } = useAdminAuth();
|
||||||
const { toast, setToast } = useToast();
|
const { toast, setToast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [entries, setEntries] = useState([]); // [{word, pos, isComment, id}]
|
const [entries, setEntries] = useState([]); // [{word, pos, isComment, id}]
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [filterPos, setFilterPos] = useState('all');
|
const [filterPos, setFilterPos] = useState('all');
|
||||||
|
|
@ -239,55 +240,59 @@ function AdminScheduleDict() {
|
||||||
return stats;
|
return stats;
|
||||||
}, [entries]);
|
}, [entries]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isAuthenticated) {
|
|
||||||
fetchDict();
|
|
||||||
}
|
|
||||||
}, [isAuthenticated]);
|
|
||||||
|
|
||||||
// 고유 ID 생성
|
// 고유 ID 생성
|
||||||
const generateId = () => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const generateId = useCallback(() => `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, []);
|
||||||
|
|
||||||
// 사전 파일 파싱
|
// 사전 파일 파싱
|
||||||
const parseDict = (content) => {
|
const parseDict = useCallback((content) => {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
return lines.map(line => {
|
return lines.map(line => {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed || trimmed.startsWith('#')) {
|
if (!trimmed || trimmed.startsWith('#')) {
|
||||||
return { isComment: true, raw: line, id: generateId() };
|
return { isComment: true, raw: line, id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}` };
|
||||||
}
|
}
|
||||||
const parts = trimmed.split('\t');
|
const parts = trimmed.split('\t');
|
||||||
return {
|
return {
|
||||||
word: parts[0] || '',
|
word: parts[0] || '',
|
||||||
pos: parts[1] || 'NNP',
|
pos: parts[1] || 'NNP',
|
||||||
isComment: false,
|
isComment: false,
|
||||||
id: generateId(),
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
};
|
};
|
||||||
}).filter(e => e.isComment || e.word); // 빈 줄 제거하되 주석은 유지
|
}).filter(e => e.isComment || e.word); // 빈 줄 제거하되 주석은 유지
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 사전 파일 생성
|
// 사전 파일 생성
|
||||||
const serializeDict = (entries) => {
|
const serializeDict = useCallback((entries) => {
|
||||||
return entries.map(e => {
|
return entries.map(e => {
|
||||||
if (e.isComment) return e.raw;
|
if (e.isComment) return e.raw;
|
||||||
return `${e.word}\t${e.pos}`;
|
return `${e.word}\t${e.pos}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 사전 내용 조회
|
// 사전 내용 조회 (useQuery)
|
||||||
const fetchDict = async () => {
|
const { data: dictContent, isLoading: loading, isError } = useQuery({
|
||||||
setLoading(true);
|
queryKey: ['admin', 'dict'],
|
||||||
try {
|
queryFn: async () => {
|
||||||
const data = await suggestionsApi.getDict();
|
const data = await suggestionsApi.getDict();
|
||||||
const parsed = parseDict(data.content || '');
|
return data.content || '';
|
||||||
|
},
|
||||||
|
enabled: isAuthenticated,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 사전 데이터 로드 후 파싱
|
||||||
|
useEffect(() => {
|
||||||
|
if (dictContent !== undefined) {
|
||||||
|
const parsed = parseDict(dictContent);
|
||||||
setEntries(parsed);
|
setEntries(parsed);
|
||||||
} catch (error) {
|
|
||||||
console.error('사전 조회 오류:', error);
|
|
||||||
setToast({ type: 'error', message: '사전을 불러올 수 없습니다.' });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
}, [dictContent, parseDict]);
|
||||||
|
|
||||||
|
// 에러 처리
|
||||||
|
useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
|
setToast({ type: 'error', message: '사전을 불러올 수 없습니다.' });
|
||||||
|
}
|
||||||
|
}, [isError, setToast]);
|
||||||
|
|
||||||
// 사전 저장 (entries 배열을 받아서 저장)
|
// 사전 저장 (entries 배열을 받아서 저장)
|
||||||
const saveDict = async (newEntries) => {
|
const saveDict = async (newEntries) => {
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ function YouTubeEditForm() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedMembers(schedule.members?.map((m) => m.id) || []);
|
setSelectedMembers(schedule.members?.map((m) => m.id) || []);
|
||||||
setVideoType(schedule.youtube?.videoType || "video");
|
setVideoType(schedule.videoType || "video");
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
}, [schedule, isInitialized, navigate, setToast]);
|
}, [schedule, isInitialized, navigate, setToast]);
|
||||||
|
|
@ -178,12 +178,14 @@ function YouTubeEditForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoUrl = videoType === "shorts"
|
const videoUrl = videoType === "shorts"
|
||||||
? `https://www.youtube.com/shorts/${schedule.youtube?.videoId}`
|
? `https://www.youtube.com/shorts/${schedule.videoId}`
|
||||||
: `https://www.youtube.com/watch?v=${schedule.youtube?.videoId}`;
|
: `https://www.youtube.com/watch?v=${schedule.videoId}`;
|
||||||
|
|
||||||
// 날짜 포맷팅 함수
|
// 날짜 포맷팅 함수 (datetime 문자열 파싱)
|
||||||
const formatDate = (dateStr, timeStr) => {
|
const formatDatetime = (datetime) => {
|
||||||
if (!dateStr) return "";
|
if (!datetime) return "";
|
||||||
|
// datetime: "2025-01-20 14:00" 또는 "2025-01-20"
|
||||||
|
const [dateStr, timeStr] = datetime.split(" ");
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
|
|
@ -239,7 +241,7 @@ function YouTubeEditForm() {
|
||||||
<div className="bg-black rounded-xl overflow-hidden">
|
<div className="bg-black rounded-xl overflow-hidden">
|
||||||
<div className="relative aspect-[9/16]">
|
<div className="relative aspect-[9/16]">
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://www.youtube.com/embed/${schedule.youtube?.videoId}`}
|
src={`https://www.youtube.com/embed/${schedule.videoId}`}
|
||||||
title={schedule.title}
|
title={schedule.title}
|
||||||
className="absolute inset-0 w-full h-full"
|
className="absolute inset-0 w-full h-full"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
|
@ -264,11 +266,11 @@ function YouTubeEditForm() {
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 mb-3">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 mb-3">
|
||||||
<span>
|
<span>
|
||||||
<span className="text-gray-400">채널:</span>{" "}
|
<span className="text-gray-400">채널:</span>{" "}
|
||||||
{schedule.youtube?.channelName}
|
{schedule.channelName}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span className="text-gray-400">업로드:</span>{" "}
|
<span className="text-gray-400">업로드:</span>{" "}
|
||||||
{formatDate(schedule.date, schedule.time)}
|
{formatDatetime(schedule.datetime)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -379,7 +381,7 @@ function YouTubeEditForm() {
|
||||||
<div className="w-full bg-black rounded-xl overflow-hidden mb-6">
|
<div className="w-full bg-black rounded-xl overflow-hidden mb-6">
|
||||||
<div className="relative aspect-video">
|
<div className="relative aspect-video">
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://www.youtube.com/embed/${schedule.youtube?.videoId}`}
|
src={`https://www.youtube.com/embed/${schedule.videoId}`}
|
||||||
title={schedule.title}
|
title={schedule.title}
|
||||||
className="absolute inset-0 w-full h-full"
|
className="absolute inset-0 w-full h-full"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
|
@ -396,11 +398,11 @@ function YouTubeEditForm() {
|
||||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-600">
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-gray-600">
|
||||||
<span>
|
<span>
|
||||||
<span className="text-gray-400">채널:</span>{" "}
|
<span className="text-gray-400">채널:</span>{" "}
|
||||||
{schedule.youtube?.channelName}
|
{schedule.channelName}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<span className="text-gray-400">업로드:</span>{" "}
|
<span className="text-gray-400">업로드:</span>{" "}
|
||||||
{formatDate(schedule.date, schedule.time)}
|
{formatDatetime(schedule.datetime)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-gray-400">유형:</span>
|
<span className="text-gray-400">유형:</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue