wing-ops/frontend/src/common/components/ui/ComboBox.tsx
htlee 3fc8f03238 refactor(css): 인라인 스타일 → Tailwind 유틸리티 클래스 변환 (Phase 2, ~990건)
Phase 2: 정적 인라인 스타일을 Tailwind className으로 변환
- common/: MapView, BacktrackReplayBar, LoginPage, LayerTree, ComboBox, SubMenuBar
- hns/: HNSSubstanceView, HNSScenarioView, HNSView, HNSLeftPanel 등 8파일
- prediction/: BoomDeploymentTheoryView, OilBoomSection, RecalcModal, RightPanel 등 8파일
- incidents/: IncidentsView, IncidentsLeftPanel, IncidentsRightPanel
- rescue/: RescueScenarioView
- aerial/: SatelliteRequest, AerialTheoryView
- assets/: ShipInsurance, AssetTheory, AssetManagement 등 5파일
- board/: BoardView
- reports/: ReportsView, OilSpillReportTemplate, ReportGenerator
- weather/: WeatherMapOverlay, WeatherView, WeatherRightPanel

변환 패턴: color/background/border/borderRadius/display/flex/gap/fontSize/fontWeight → Tailwind
동적 스타일(rgba, gradient, 삼항 조건부, 런타임 변수)은 style prop에 유지
JS 번들: 2,921KB → 2,897KB (-24KB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:24:13 +09:00

97 lines
3.0 KiB
TypeScript
Executable File

import { useState, useRef, useEffect } from 'react'
interface ComboBoxOption {
value: string
label: string
}
interface ComboBoxProps {
value: string | number
onChange: (value: string) => void
options: ComboBoxOption[]
placeholder?: string
className?: string
}
export function ComboBox({ value, onChange, options, placeholder, className }: ComboBoxProps) {
const [isOpen, setIsOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const selectedOption = options.find(opt => opt.value === String(value))
const displayText = selectedOption?.label || placeholder || '선택'
return (
<div ref={containerRef} className="relative">
<div
className={`cursor-pointer flex items-center justify-between pr-2 ${className ?? ''}`}
onClick={() => setIsOpen(!isOpen)}
>
<span>{displayText}</span>
<span
className="text-[8px] text-text-3"
style={{
transition: 'transform 0.2s',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
}}
>
</span>
</div>
{isOpen && (
<div
className="absolute left-0 right-0 bg-bg-0 border border-border overflow-y-auto z-[1000]"
style={{
top: 'calc(100% + 2px)',
borderRadius: 'var(--rS)',
maxHeight: '200px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
animation: 'fadeSlideDown 0.15s ease-out'
}}
>
{options.map((option) => (
<div
key={option.value}
onClick={() => {
onChange(option.value)
setIsOpen(false)
}}
className="text-[11px] cursor-pointer"
style={{
padding: '8px 10px',
color: option.value === String(value) ? 'var(--cyan)' : 'var(--t2)',
background: option.value === String(value) ? 'rgba(6,182,212,0.1)' : 'transparent',
transition: '0.1s',
borderLeft: option.value === String(value) ? '2px solid var(--cyan)' : '2px solid transparent'
}}
onMouseEnter={(e) => {
if (option.value !== String(value)) {
e.currentTarget.style.background = 'rgba(255,255,255,0.03)'
}
}}
onMouseLeave={(e) => {
if (option.value !== String(value)) {
e.currentTarget.style.background = 'transparent'
}
}}
>
{option.label}
</div>
))}
</div>
)}
</div>
)
}