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:
caadiq 2026-02-06 18:30:34 +09:00
parent 3fa9f1520a
commit f3f99c7428
3 changed files with 97 additions and 39 deletions

View file

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

View file

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

View file

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