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 { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Home, ChevronRight, Book, Plus, Trash2, Search, ChevronDown } from 'lucide-react';
|
||||
import Toast from '../../../components/Toast';
|
||||
import AdminLayout from '../../../components/admin/AdminLayout';
|
||||
|
|
@ -171,8 +172,8 @@ function WordItem({ id, word, pos, index, onUpdate, onDelete }) {
|
|||
function AdminScheduleDict() {
|
||||
const { user, isAuthenticated } = useAdminAuth();
|
||||
const { toast, setToast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [entries, setEntries] = useState([]); // [{word, pos, isComment, id}]
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterPos, setFilterPos] = useState('all');
|
||||
|
|
@ -239,55 +240,59 @@ function AdminScheduleDict() {
|
|||
return stats;
|
||||
}, [entries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
fetchDict();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// 고유 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');
|
||||
return lines.map(line => {
|
||||
const trimmed = line.trim();
|
||||
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');
|
||||
return {
|
||||
word: parts[0] || '',
|
||||
pos: parts[1] || 'NNP',
|
||||
isComment: false,
|
||||
id: generateId(),
|
||||
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
};
|
||||
}).filter(e => e.isComment || e.word); // 빈 줄 제거하되 주석은 유지
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 사전 파일 생성
|
||||
const serializeDict = (entries) => {
|
||||
const serializeDict = useCallback((entries) => {
|
||||
return entries.map(e => {
|
||||
if (e.isComment) return e.raw;
|
||||
return `${e.word}\t${e.pos}`;
|
||||
}).join('\n');
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 사전 내용 조회
|
||||
const fetchDict = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 사전 내용 조회 (useQuery)
|
||||
const { data: dictContent, isLoading: loading, isError } = useQuery({
|
||||
queryKey: ['admin', 'dict'],
|
||||
queryFn: async () => {
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error('사전 조회 오류:', error);
|
||||
setToast({ type: 'error', message: '사전을 불러올 수 없습니다.' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [dictContent, parseDict]);
|
||||
|
||||
// 에러 처리
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
setToast({ type: 'error', message: '사전을 불러올 수 없습니다.' });
|
||||
}
|
||||
}, [isError, setToast]);
|
||||
|
||||
// 사전 저장 (entries 배열을 받아서 저장)
|
||||
const saveDict = async (newEntries) => {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ function YouTubeEditForm() {
|
|||
return;
|
||||
}
|
||||
setSelectedMembers(schedule.members?.map((m) => m.id) || []);
|
||||
setVideoType(schedule.youtube?.videoType || "video");
|
||||
setVideoType(schedule.videoType || "video");
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [schedule, isInitialized, navigate, setToast]);
|
||||
|
|
@ -178,12 +178,14 @@ function YouTubeEditForm() {
|
|||
}
|
||||
|
||||
const videoUrl = videoType === "shorts"
|
||||
? `https://www.youtube.com/shorts/${schedule.youtube?.videoId}`
|
||||
: `https://www.youtube.com/watch?v=${schedule.youtube?.videoId}`;
|
||||
? `https://www.youtube.com/shorts/${schedule.videoId}`
|
||||
: `https://www.youtube.com/watch?v=${schedule.videoId}`;
|
||||
|
||||
// 날짜 포맷팅 함수
|
||||
const formatDate = (dateStr, timeStr) => {
|
||||
if (!dateStr) return "";
|
||||
// 날짜 포맷팅 함수 (datetime 문자열 파싱)
|
||||
const formatDatetime = (datetime) => {
|
||||
if (!datetime) return "";
|
||||
// datetime: "2025-01-20 14:00" 또는 "2025-01-20"
|
||||
const [dateStr, timeStr] = datetime.split(" ");
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
|
|
@ -239,7 +241,7 @@ function YouTubeEditForm() {
|
|||
<div className="bg-black rounded-xl overflow-hidden">
|
||||
<div className="relative aspect-[9/16]">
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${schedule.youtube?.videoId}`}
|
||||
src={`https://www.youtube.com/embed/${schedule.videoId}`}
|
||||
title={schedule.title}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
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">
|
||||
<span>
|
||||
<span className="text-gray-400">채널:</span>{" "}
|
||||
{schedule.youtube?.channelName}
|
||||
{schedule.channelName}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-gray-400">업로드:</span>{" "}
|
||||
{formatDate(schedule.date, schedule.time)}
|
||||
{formatDatetime(schedule.datetime)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -379,7 +381,7 @@ function YouTubeEditForm() {
|
|||
<div className="w-full bg-black rounded-xl overflow-hidden mb-6">
|
||||
<div className="relative aspect-video">
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${schedule.youtube?.videoId}`}
|
||||
src={`https://www.youtube.com/embed/${schedule.videoId}`}
|
||||
title={schedule.title}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
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">
|
||||
<span>
|
||||
<span className="text-gray-400">채널:</span>{" "}
|
||||
{schedule.youtube?.channelName}
|
||||
{schedule.channelName}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-gray-400">업로드:</span>{" "}
|
||||
{formatDate(schedule.date, schedule.time)}
|
||||
{formatDatetime(schedule.datetime)}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400">유형:</span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue