SJLee-0525
[CHORE] test3
ebcf639
"""
커스텀 모달 컴포넌트 (Gradio 6 호환)
순수 gr.HTML 기반 구현
사용법:
from frontend.components.custom_modal import Modal
with gr.Blocks() as demo:
modal = Modal(elem_id="my-modal")
modal.render()
# 모달 열기
show_btn.click(
fn=lambda content: Modal.show(content),
outputs=[modal.component]
)
# 모달 닫기 (자동 - X 버튼, ESC, 배경 클릭)
"""
import gradio as gr
class Modal:
"""
Gradio 6 호환 커스텀 모달 컴포넌트
gr.HTML의 html_template, css_template, js_on_load를 활용하여 모달 기능 구현
"""
backup_button = """
<button class="modal-close" data-modal-close="true">
<svg width="12" height="12" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L9 9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M9 1L1 9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>"""
# 모달 HTML 템플릿 - visible이 true일 때만 렌더링
# 다크모드 감지: document.body나 .gradio-container에 .dark 클래스가 있는지 확인
HTML_TEMPLATE = """
${value && value.visible ? `
<div class="modal-overlay ${document.body.classList.contains('dark') || document.querySelector('.dark') ? 'modal-dark' : ''}" data-modal-overlay="true">
<div class="modal-container">
<div class="modal-content">
${value.content || ''}
</div>
</div>
</div>
` : ''}
"""
# 모달 CSS 스타일
CSS_TEMPLATE = """
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(2px);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
/* 다크모드: 블러 제거, 반투명 배경만 */
.modal-overlay.modal-dark {
background-color: rgba(0, 0, 0, 0.3) !important;
}
.modal-container {
background: var(--background-fill-primary, #ffffff);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
width: 92%;
min-width: 350px;
max-width: 600px;
max-height: 95vh;
overflow-y: auto;
position: relative;
animation: modalFadeIn 0.2s ease-out;
}
/* 다크모드 모달 컨테이너 */
.modal-dark .modal-container {
background: #1a1a1b;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.modal-dark .modal-close {
background: #3a3a3c;
border-color: #4a4a4c;
color: #ffffff;
}
.modal-dark .modal-close:hover {
background: #4a4a4c;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-close {
position: absolute;
top: 12px;
right: 12px;
background: var(--background-fill-secondary, #f0f0f0);
border: 1px solid var(--border-color-primary, #ddd);
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--body-text-color, #333);
transition: all 0.2s ease;
z-index: 10;
padding: 0;
}
.modal-close:hover {
background: var(--background-fill-secondary-hover, #e0e0e0);
transition: colors 0.2s ease;
}
.modal-content {
padding: 20px;
}
"""
# 모달 JavaScript (이벤트 핸들링)
JS_ON_LOAD = """
// ESC 키로 닫기
const handleKeydown = (e) => {
if (e.key === 'Escape' && props.value && props.value.visible) {
props.value = { visible: false, content: '' };
}
};
document.addEventListener('keydown', handleKeydown);
// 클릭 이벤트 (배경 클릭, X 버튼)
element.addEventListener('click', (e) => {
// 배경 클릭
if (e.target.dataset.modalOverlay === 'true') {
props.value = { visible: false, content: '' };
}
// X 버튼 클릭
if (e.target.closest('[data-modal-close="true"]')) {
props.value = { visible: false, content: '' };
}
});
"""
def __init__(
self,
visible: bool = False,
content: str = "",
elem_id: str | None = None,
elem_classes: list[str] | str | None = None,
):
"""
모달 초기화
Args:
visible: 모달 표시 여부
content: 모달 내부 HTML 콘텐츠
elem_id: HTML element ID
elem_classes: HTML element classes
"""
self.visible = visible
self.content = content
self.elem_id = elem_id
self.elem_classes = elem_classes
self.component = None
def render(self) -> gr.HTML:
"""
모달 컴포넌트 렌더링
Returns:
gr.HTML: 렌더링된 모달 컴포넌트
"""
self.component = gr.HTML(
value={"visible": self.visible, "content": self.content},
html_template=self.HTML_TEMPLATE,
css_template=self.CSS_TEMPLATE,
js_on_load=self.JS_ON_LOAD,
elem_id=self.elem_id,
elem_classes=self.elem_classes,
)
return self.component
@staticmethod
def show(content: str = "") -> dict:
"""모달 열기"""
return {"visible": True, "content": content}
@staticmethod
def hide() -> dict:
"""모달 닫기"""
return {"visible": False, "content": ""}