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:
caadiq 2026-01-21 13:52:46 +09:00
parent 7593004bd6
commit 46469fd324
2 changed files with 45 additions and 38 deletions

View file

@ -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) => {

View file

@ -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>