fix: 드롭다운 z-index 수정 및 YouTubeBotDialog 커스텀 드롭다운 적용
- WordItem, ScheduleDict 드롭다운 z-index를 z-20에서 z-40으로 변경 (테이블 헤더의 z-30보다 높게 설정하여 가려지는 문제 해결) - YouTubeBotDialog에 커스텀 Dropdown 컴포넌트 추가 - 네이티브 select 요소를 커스텀 드롭다운으로 교체 - 시간 선택을 위한 TIME_OPTIONS (00:00~23:00) 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3fa9f1520a
commit
f3f99c7428
3 changed files with 97 additions and 39 deletions
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* YouTube 봇 추가/수정 다이얼로그
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Youtube, Search, X, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Youtube, Search, X, ChevronDown, ChevronUp, Clock } from 'lucide-react';
|
||||
|
||||
// 동기화 간격 옵션
|
||||
const INTERVAL_OPTIONS = [
|
||||
|
|
@ -27,6 +27,79 @@ const DAY_OPTIONS = [
|
|||
{ value: 6, label: '토요일' },
|
||||
];
|
||||
|
||||
// 시간 옵션 (00:00 ~ 23:00)
|
||||
const TIME_OPTIONS = Array.from({ length: 24 }, (_, i) => ({
|
||||
value: `${String(i).padStart(2, '0')}:00`,
|
||||
label: `${String(i).padStart(2, '0')}:00`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 커스텀 드롭다운 컴포넌트
|
||||
*/
|
||||
function Dropdown({ value, options, onChange, placeholder = '선택', className = '' }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen]);
|
||||
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`} ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 w-full px-4 py-2.5 bg-gray-100 hover:bg-gray-200 rounded-lg text-sm transition-colors justify-between"
|
||||
>
|
||||
<span className={selectedOption ? 'text-gray-900' : 'text-gray-400'}>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={`text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
className="absolute top-full left-0 mt-1 w-full bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-50 max-h-60 overflow-y-auto"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(opt.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full px-4 py-2 text-left hover:bg-gray-50 transition-colors text-sm ${
|
||||
value === opt.value ? 'bg-red-50 text-red-600' : ''
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function YouTubeBotDialog({ isOpen, onClose, bot = null, onSubmit }) {
|
||||
const isEdit = !!bot;
|
||||
|
||||
|
|
@ -202,17 +275,12 @@ function YouTubeBotDialog({ isOpen, onClose, bot = null, onSubmit }) {
|
|||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||
동기화 간격
|
||||
</label>
|
||||
<select
|
||||
<Dropdown
|
||||
value={interval}
|
||||
onChange={(e) => setInterval(Number(e.target.value))}
|
||||
className="w-full px-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 bg-white"
|
||||
>
|
||||
{INTERVAL_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={INTERVAL_OPTIONS}
|
||||
onChange={setInterval}
|
||||
placeholder="간격 선택"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 예정 일정 자동 생성 */}
|
||||
|
|
@ -244,25 +312,20 @@ function YouTubeBotDialog({ isOpen, onClose, bot = null, onSubmit }) {
|
|||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-600 mb-1">요일</label>
|
||||
<select
|
||||
<Dropdown
|
||||
value={scheduleDayOfWeek}
|
||||
onChange={(e) => setScheduleDayOfWeek(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 bg-white"
|
||||
>
|
||||
{DAY_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={DAY_OPTIONS}
|
||||
onChange={setScheduleDayOfWeek}
|
||||
placeholder="요일 선택"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-600 mb-1">시간</label>
|
||||
<input
|
||||
type="time"
|
||||
<Dropdown
|
||||
value={scheduleTime}
|
||||
onChange={(e) => setScheduleTime(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500"
|
||||
options={TIME_OPTIONS}
|
||||
onChange={setScheduleTime}
|
||||
placeholder="시간 선택"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -285,17 +348,12 @@ function YouTubeBotDialog({ isOpen, onClose, bot = null, onSubmit }) {
|
|||
{/* 마감 요일 */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-600 mb-1">마감 요일</label>
|
||||
<select
|
||||
<Dropdown
|
||||
value={deadlineDayOfWeek}
|
||||
onChange={(e) => setDeadlineDayOfWeek(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 bg-white"
|
||||
>
|
||||
{DAY_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={DAY_OPTIONS}
|
||||
onChange={setDeadlineDayOfWeek}
|
||||
placeholder="요일 선택"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
이 요일까지 영상이 없으면 예정 일정을 삭제합니다
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ function WordItem({ id, word, pos, index, onUpdate, onDelete }) {
|
|||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
className="absolute top-full left-0 mt-1 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-20"
|
||||
className="absolute top-full left-0 mt-1 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-40"
|
||||
>
|
||||
{POS_TAGS.map((tag) => (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ function ScheduleDict() {
|
|||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
className="absolute top-full left-0 mt-1 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-20"
|
||||
className="absolute top-full left-0 mt-1 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-40"
|
||||
>
|
||||
{POS_TAGS.map((tag) => (
|
||||
<button
|
||||
|
|
@ -471,7 +471,7 @@ function ScheduleDict() {
|
|||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
className="absolute top-full right-0 mt-1 w-48 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-20"
|
||||
className="absolute top-full right-0 mt-1 w-48 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-40"
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue