Spaces:
Running
Running
| <html lang="ko"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LexiMind - 자동차 인증 법규를 더 쉽게</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #0066FF; | |
| --primary-hover: #4da8ff; | |
| --bg-primary: #1a1a1a; | |
| --bg-secondary: #2c2c2c; | |
| --bg-tertiary: #333; | |
| --bg-quaternary: #444; | |
| --text-primary: #e0e0e0; | |
| --text-secondary: #b0b0b0; | |
| --text-muted: #888; | |
| --border-color: #444; | |
| --shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| --radius: 8px; | |
| --success-color: #4caf50; | |
| --warning-color: #ff9800; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif; | |
| margin: 0; padding: 0; | |
| background-color: var(--bg-primary); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| } | |
| /* ===== 헤더 ===== */ | |
| .header { | |
| background: var(--bg-secondary); | |
| padding: 16px 24px; | |
| box-shadow: var(--shadow); | |
| display: flex; justify-content: space-between; align-items: center; | |
| position: sticky; top: 0; z-index: 100; | |
| } | |
| .logo { | |
| font-size: 28px; font-weight: 700; color: var(--primary-color); letter-spacing: -0.5px; | |
| } | |
| .nav { | |
| display: flex; list-style: none; gap: 32px; margin: 0; padding: 0; | |
| } | |
| .nav a { | |
| text-decoration: none; color: var(--text-secondary); font-weight: 500; | |
| padding: 8px 12px; border-radius: var(--radius); transition: all 0.2s ease; | |
| } | |
| .nav a:hover { | |
| color: var(--primary-hover); background: rgba(77, 168, 255, 0.1); | |
| } | |
| /* ===== 사이드바 컨트롤 ===== */ | |
| .sidebar-toggle { | |
| position: fixed; | |
| top: 100px; | |
| left: 30px; | |
| width: 40px; | |
| height: 40px; | |
| background: rgba(0, 102, 255, 0.7); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| z-index: 1001; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 32px rgba(0, 102, 255, 0.3); | |
| } | |
| .sidebar-toggle:hover { | |
| background: rgba(77, 168, 255, 0.8); | |
| transform: scale(1.05); | |
| } | |
| .hamburger { | |
| width: 15px; | |
| height: 2px; | |
| background: white; | |
| position: relative; | |
| transition: all 0.3s ease; | |
| transform: translateX(-7px); | |
| } | |
| .hamburger::before, | |
| .hamburger::after { | |
| content: ''; | |
| position: absolute; | |
| width: 15px; | |
| height: 2px; | |
| background: white; | |
| transition: all 0.3s ease; | |
| } | |
| .hamburger::before { top: -6px; } | |
| .hamburger::after { top: 6px; } | |
| .sidebar-toggle.active .hamburger { background: transparent; } | |
| .sidebar-toggle.active .hamburger::before { transform: rotate(45deg); top: 0; } | |
| .sidebar-toggle.active .hamburger::after { transform: rotate(-45deg); top: 0; } | |
| .new-page-btn { | |
| position: fixed; | |
| top: 100px; | |
| left: 85px; | |
| min-width: 120px; | |
| height: 40px; | |
| padding: 8px 16px; | |
| background: rgba(255, 255, 255, 1); | |
| border: 2px solid rgba(0, 102, 255, 0.2); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| z-index: 1001; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| opacity: 0; | |
| visibility: hidden; | |
| transform: translateX(-20px); | |
| } | |
| .new-page-btn.show { | |
| opacity: 1; | |
| visibility: visible; | |
| transform: translateX(0); | |
| } | |
| .new-page-btn:hover { | |
| background: rgba(255, 255, 255, 0.8); | |
| transform: translateY(-2px); | |
| } | |
| .plus-icon { | |
| color: #0066FF; | |
| font-size: 16px; | |
| font-weight: 700; | |
| } | |
| /* ===== 사이드바 ===== */ | |
| .sidebar { | |
| position: fixed; top: 76px; left: -300px; width: 300px; | |
| height: calc(100vh - 76px); background: #2c2c2c; | |
| transition: left 0.3s ease; z-index: 1000; overflow-y: auto; | |
| } | |
| .sidebar.open { left: 0; } | |
| .sidebar-content { | |
| padding: 75px 30px 30px 30px; | |
| } | |
| .sidebar-overlay { | |
| position: fixed; top: 76px; left: 0; width: 100%; | |
| height: calc(100vh - 76px); background: rgba(0, 0, 0, 0.5); | |
| opacity: 0; visibility: hidden; transition: all 0.3s ease; z-index: 999; | |
| } | |
| .sidebar-overlay.active { opacity: 1; visibility: visible; } | |
| .date-ranges { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .date-range-item { | |
| display: block; | |
| padding: 16px; | |
| background: var(--bg-tertiary); | |
| border: 2px solid transparent; | |
| border-radius: var(--radius); | |
| text-decoration: none; | |
| color: var(--text-primary); | |
| transition: all 0.3s ease; | |
| } | |
| .date-range-item:hover { | |
| background: var(--bg-quaternary); | |
| border-color: var(--primary-color); | |
| transform: translateY(-2px); | |
| } | |
| .date-period { | |
| display: block; | |
| font-weight: 600; | |
| font-size: 14px; | |
| color: var(--primary-color); | |
| margin-bottom: 4px; | |
| } | |
| .date-count { | |
| display: block; | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| /* ===== 메인 레이아웃 ===== */ | |
| .main { | |
| display: flex; | |
| height: calc(100vh - 76px); | |
| max-width: 100%; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| /* ===== 필터 패널 ===== */ | |
| .filter-panel { | |
| width: 350px; | |
| background: var(--bg-secondary); | |
| border-right: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .filter-header { | |
| padding: 100px 20px 20px 20px; | |
| border-bottom: 1px solid var(--border-color); | |
| background: var(--bg-tertiary); | |
| } | |
| .filter-title { | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin: 0 0 16px 0; | |
| } | |
| /* 지역 선택 토글 */ | |
| .toggle-group { margin-bottom: 24px; } | |
| .toggle-label { | |
| display: block; | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| margin-bottom: 12px; | |
| } | |
| .toggle-options { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .toggle-btn { | |
| padding: 8px 16px; | |
| background: var(--bg-quaternary); | |
| border: 2px solid transparent; | |
| border-radius: 20px; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| font-size: 13px; | |
| font-weight: 500; | |
| } | |
| .toggle-btn:hover { | |
| background: #3a3a3a; | |
| color: var(--text-primary); | |
| } | |
| .toggle-btn.active { | |
| background: var(--primary-color); | |
| border-color: var(--primary-hover); | |
| color: white; | |
| } | |
| /* 법규 목록 스타일 */ | |
| .regulation-list-container { | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 24px; | |
| max-height: 250px; | |
| overflow-y: auto; | |
| } | |
| .regulation-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .selected-count { | |
| font-size: 12px; | |
| color: var(--primary-color); | |
| background: rgba(77, 168, 255, 0.1); | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| } | |
| .regulation-item { | |
| padding: 12px; | |
| margin: 8px 0; | |
| background: var(--bg-quaternary); | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| border: 2px solid transparent; | |
| user-select: none; | |
| } | |
| .regulation-item:hover { | |
| background: #3a3a3a; | |
| border-color: var(--primary-color); | |
| } | |
| .regulation-item.selected { | |
| background: var(--primary-color); | |
| color: white; | |
| border-color: var(--primary-hover); | |
| } | |
| /* ===== 상세 법규 리스트 ===== */ | |
| .regulation-details-container { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .regulation-details-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 16px 24px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .btn { | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: var(--radius); | |
| background: var(--primary-color); | |
| color: white; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: 600; | |
| transition: all 0.2s ease; | |
| } | |
| .btn:hover { | |
| background: var(--primary-hover); | |
| transform: translateY(-1px); | |
| } | |
| .btn-small { | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| } | |
| .btn-load { | |
| background: var(--success-color); | |
| } | |
| .btn-load:hover { | |
| background: #45a049; | |
| } | |
| .btn-secondary { | |
| background: var(--bg-quaternary); | |
| } | |
| .btn-secondary:hover { | |
| background: #555; | |
| } | |
| .btn-danger { | |
| background: #ff4757; | |
| } | |
| .btn-danger:hover { | |
| background: #ff3838; | |
| } | |
| .regulation-search { | |
| padding: 12px 24px; | |
| background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .regulation-search-input { | |
| width: 100%; | |
| padding: 8px 12px; | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius); | |
| background: var(--bg-quaternary); | |
| color: var(--text-primary); | |
| font-size: 13px; | |
| transition: border-color 0.2s ease; | |
| } | |
| .regulation-search-input:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| } | |
| .regulation-search-input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .regulation-details-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 12px 24px; | |
| } | |
| .regulation-detail-item { | |
| padding: 10px 12px; | |
| margin: 6px 0; | |
| background: var(--bg-quaternary); | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| border: 2px solid transparent; | |
| user-select: none; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| } | |
| .regulation-detail-item:hover { | |
| background: #3a3a3a; | |
| border-color: var(--primary-color); | |
| } | |
| .regulation-detail-item.selected { | |
| background: var(--primary-color); | |
| color: white; | |
| border-color: var(--primary-hover); | |
| } | |
| .regulation-detail-item.hidden { | |
| display: none; | |
| } | |
| .action-buttons { | |
| display: flex; | |
| gap: 12px; | |
| padding: 16px 24px; | |
| border-top: 1px solid var(--border-color); | |
| background: var(--bg-secondary); | |
| } | |
| /* ===== 채팅 패널 ===== */ | |
| .chat-panel { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: var(--bg-primary); | |
| } | |
| .chat-header { | |
| padding: 20px 24px; | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .chat-title { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin: 0; | |
| display: flex; | |
| align-items: baseline; | |
| gap: 10px; | |
| } | |
| .title-subtitle { | |
| font-size: 14px; | |
| font-weight: 400; | |
| color: var(--text-muted); | |
| } | |
| .status-text { | |
| margin-top: 12px; | |
| padding: 16px 20px; | |
| background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-quaternary) 100%); | |
| border: 2px solid var(--border-color); | |
| border-radius: 12px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| position: relative; | |
| box-shadow: 0 4px 12px rgba(0, 102, 255, 0.15); | |
| min-height: 60px; | |
| display: flex; | |
| align-items: center; | |
| transition: all 0.3s ease; | |
| } | |
| .status-text:hover { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 6px 20px rgba(0, 102, 255, 0.25); | |
| } | |
| /* 수정된 progress-bar 스타일 - 더 두껍게 */ | |
| .progress-bar { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| height: 6px; | |
| background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)); | |
| border-radius: 0 0 10px 10px; | |
| transition: width 0.3s ease; | |
| width: 0%; | |
| box-shadow: 0 2px 8px rgba(0, 102, 255, 0.4); | |
| } | |
| .cancel-search-btn { | |
| position: absolute; | |
| right: 16px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| padding: 8px 16px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| background: var(--warning-color); | |
| border: 2px solid transparent; | |
| border-radius: 8px; | |
| color: white; | |
| cursor: pointer; | |
| display: none; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3); | |
| } | |
| .cancel-search-btn:hover { | |
| background: #e68900; | |
| border-color: #ff9800; | |
| transform: translateY(-50%) scale(1.05); | |
| box-shadow: 0 4px 12px rgba(255, 152, 0, 0.4); | |
| } | |
| .cancel-search-btn:active { | |
| transform: translateY(-50%) scale(0.95); | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| /* 메시지 버블 */ | |
| .message { | |
| display: flex; | |
| gap: 12px; | |
| max-width: 80%; | |
| animation: slideIn 0.3s ease; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .message.user { | |
| align-self: flex-end; | |
| flex-direction: row-reverse; | |
| } | |
| .message.assistant { | |
| align-self: flex-start; | |
| } | |
| .message-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: 700; | |
| font-size: 14px; | |
| flex-shrink: 0; | |
| } | |
| .message.user .message-avatar { | |
| background: var(--success-color); | |
| } | |
| .message-content { | |
| background: var(--bg-secondary); | |
| padding: 12px 16px; | |
| border-radius: 16px; | |
| color: var(--text-primary); | |
| line-height: 1.5; | |
| word-wrap: break-word; | |
| } | |
| .message.user .message-content { | |
| background: var(--primary-color); | |
| color: white; | |
| } | |
| .message-time { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-top: 4px; | |
| } | |
| /* 메시지 콘텐츠 내 HTML 스타일링 */ | |
| .message-content h1, | |
| .message-content h2 { | |
| color: var(--primary-color); | |
| margin: 12px 0 8px 0; | |
| font-weight: 600; | |
| } | |
| .message-content h1 { font-size: 18px; } | |
| .message-content h2 { | |
| font-size: 16px; | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 4px; | |
| } | |
| .message-content ul, | |
| .message-content ol { | |
| margin: 8px 0; | |
| padding-left: 20px; | |
| } | |
| .message-content li { | |
| margin: 4px 0; | |
| line-height: 1.4; | |
| } | |
| .message-content strong { | |
| color: var(--primary-color); | |
| } | |
| .message-content p { | |
| margin: 8px 0; | |
| line-height: 1.5; | |
| } | |
| .message.user .message-content h1, | |
| .message.user .message-content h2, | |
| .message.user .message-content strong { | |
| color: white; | |
| } | |
| /* 채팅 입력 영역 */ | |
| .chat-input-container { | |
| padding: 20px 24px; | |
| background: var(--bg-secondary); | |
| border-top: 1px solid var(--border-color); | |
| } | |
| .chat-input-wrapper { | |
| display: flex; | |
| gap: 12px; | |
| align-items: flex-end; | |
| } | |
| .chat-input { | |
| flex: 1; | |
| padding: 12px 16px; | |
| background: var(--bg-tertiary); | |
| border: 2px solid var(--border-color); | |
| border-radius: 24px; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| resize: none; | |
| max-height: 120px; | |
| min-height: 44px; | |
| font-family: inherit; | |
| transition: border-color 0.2s ease; | |
| } | |
| .chat-input:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| } | |
| .chat-input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .send-btn { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| border: none; | |
| color: white; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover { | |
| background: var(--primary-hover); | |
| transform: scale(1.05); | |
| } | |
| .send-btn:active { | |
| transform: scale(0.95); | |
| } | |
| .send-btn:disabled { | |
| background: var(--bg-quaternary); | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| /* 로딩 인디케이터 */ | |
| .typing-indicator { | |
| display: flex; | |
| gap: 4px; | |
| padding: 12px 16px; | |
| background: var(--bg-secondary); | |
| border-radius: 16px; | |
| width: fit-content; | |
| } | |
| .typing-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| animation: typing 1.4s infinite; | |
| } | |
| .typing-dot:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-dot:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes typing { | |
| 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); } | |
| 30% { opacity: 1; transform: translateY(-10px); } | |
| } | |
| /* 스크롤바 스타일 */ | |
| .chat-messages::-webkit-scrollbar, | |
| .filter-panel::-webkit-scrollbar, | |
| .regulation-list-container::-webkit-scrollbar, | |
| .regulation-details-list::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .chat-messages::-webkit-scrollbar-track, | |
| .filter-panel::-webkit-scrollbar-track, | |
| .regulation-list-container::-webkit-scrollbar-track, | |
| .regulation-details-list::-webkit-scrollbar-track { | |
| background: var(--bg-tertiary); | |
| } | |
| .chat-messages::-webkit-scrollbar-thumb, | |
| .filter-panel::-webkit-scrollbar-thumb, | |
| .regulation-list-container::-webkit-scrollbar-thumb, | |
| .regulation-details-list::-webkit-scrollbar-thumb { | |
| background: var(--bg-quaternary); | |
| border-radius: 4px; | |
| } | |
| .chat-messages::-webkit-scrollbar-thumb:hover, | |
| .filter-panel::-webkit-scrollbar-thumb:hover, | |
| .regulation-list-container::-webkit-scrollbar-thumb:hover, | |
| .regulation-details-list::-webkit-scrollbar-thumb:hover { | |
| background: #555; | |
| } | |
| .search-mode-group { | |
| margin-bottom: 20px; | |
| padding: 16px; | |
| background: var(--bg-quaternary); | |
| border-radius: var(--radius); | |
| border: 1px solid var(--border-color); | |
| } | |
| .search-mode-toggle { | |
| position: relative; | |
| display: inline-block; | |
| width: 60px; | |
| height: 34px; | |
| } | |
| .search-mode-toggle input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: var(--bg-tertiary); | |
| transition: 0.4s; | |
| border-radius: 34px; | |
| border: 2px solid var(--border-color); | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 24px; | |
| width: 24px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: var(--text-secondary); | |
| transition: 0.4s; | |
| border-radius: 50%; | |
| } | |
| input:checked + .slider { | |
| background-color: var(--primary-color); | |
| border-color: var(--primary-hover); | |
| } | |
| input:checked + .slider:before { | |
| transform: translateX(24px); | |
| background-color: white; | |
| } | |
| .search-mode-label { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| .search-mode-description { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| line-height: 1.4; | |
| margin-top: 8px; | |
| } | |
| .mode-indicator { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| } | |
| .regulation-type-tabs { | |
| display: flex; | |
| background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border-color); | |
| padding: 8px 12px; | |
| gap: 4px; | |
| } | |
| .type-tab { | |
| padding: 8px 16px; | |
| background: var(--bg-quaternary); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 12px; | |
| font-weight: 600; | |
| transition: all 0.2s ease; | |
| flex: 1; | |
| text-align: center; | |
| } | |
| .type-tab:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| } | |
| .type-tab.active { | |
| background: var(--primary-color); | |
| border-color: var(--primary-hover); | |
| color: white; | |
| } | |
| /* 기존 스타일에 타입별 구분을 위한 스타일 추가 */ | |
| .regulation-detail-item[data-type="part"]::before { | |
| content: "[P] "; | |
| color: var(--primary-color); | |
| font-weight: 700; | |
| font-size: 11px; | |
| } | |
| .regulation-detail-item[data-type="section"]::before { | |
| content: "[S] "; | |
| color: var(--success-color); | |
| font-weight: 700; | |
| font-size: 11px; | |
| } | |
| .regulation-detail-item[data-type="chapter"]::before { | |
| content: "[C] "; | |
| color: var(--warning-color); | |
| font-weight: 700; | |
| font-size: 11px; | |
| } | |
| .regulation-detail-item[data-type="jo"]::before { | |
| content: "[조] "; | |
| color: #ff6b6b; | |
| font-weight: 700; | |
| font-size: 11px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="header"> | |
| <div class="logo">LexiMind</div> | |
| <nav> | |
| <ul class="nav"> | |
| <li><a href="#">법규 검색</a></li> | |
| <li><a href="#">체크리스트 생성</a></li> | |
| <li><a href="#">유권해석</a></li> | |
| <li><a href="#">사용문의</a></li> | |
| </ul> | |
| </nav> | |
| </header> | |
| <!-- 햄버거 버튼 --> | |
| <button class="sidebar-toggle" id="sidebarToggle"> | |
| <span class="hamburger"></span> | |
| </button> | |
| <!-- 새 페이지 버튼 --> | |
| <button class="new-page-btn" id="newPageBtn"> | |
| <span class="plus-icon">+ 새 대화 생성</span> | |
| </button> | |
| <!-- 사이드바 오버레이 --> | |
| <div class="sidebar-overlay" id="sidebarOverlay"></div> | |
| <!-- 사이드바 --> | |
| <nav class="sidebar" id="sidebar"> | |
| <div class="sidebar-content"> | |
| <h3>과거 이력</h3> | |
| <div class="date-ranges"> | |
| <a href="/history/202511" class="date-range-item"> | |
| <span class="date-period">2025.11.01 ~ 2025.11.30</span> | |
| <span class="date-count">검색 15건</span> | |
| </a> | |
| <a href="/history/202510" class="date-range-item"> | |
| <span class="date-period">2025.10.01 ~ 2025.10.31</span> | |
| <span class="date-count">검색 8건</span> | |
| </a> | |
| <a href="/history/202509" class="date-range-item"> | |
| <span class="date-period">2025.09.01 ~ 2025.09.30</span> | |
| <span class="date-count">검색 12건</span> | |
| </a> | |
| </div> | |
| </div> | |
| </nav> | |
| <main class="main"> | |
| <!-- 필터 패널 --> | |
| <aside class="filter-panel"> | |
| <div class="filter-header"> | |
| <h2 class="filter-title">검색 필터</h2> | |
| <div class="toggle-group"> | |
| <span class="toggle-label">지역</span> | |
| <div class="toggle-options"> | |
| <button class="toggle-btn region-toggle" data-value="국내">국내</button> | |
| <button class="toggle-btn region-toggle" data-value="북미">북미</button> | |
| <button class="toggle-btn region-toggle" data-value="유럽">유럽</button> | |
| <button class="toggle-btn region-toggle active" data-value="전체">전체</button> | |
| </div> | |
| </div> | |
| <!-- 검색 모드 토글 추가 --> | |
| <div class="search-mode-group"> | |
| <div class="search-mode-label"> | |
| <span>선택된 법규 각각에서 검색하기</span> | |
| <label class="search-mode-toggle"> | |
| <input type="checkbox" id="searchEachModeToggle"> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <div class="mode-indicator" id="searchModeIndicator">각각 검색 모드</div> | |
| <div class="search-mode-description" id="searchModeDescription"> | |
| 선택된 각 법규별로 개별 검색을 수행하고 결과를 따로 표시합니다. | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 상세 법규 리스트 --> | |
| <div class="regulation-details-container"> | |
| <div class="regulation-details-header"> | |
| <span class="toggle-label">상세 법규 리스트</span> | |
| <button class="btn btn-load btn-small" onclick="loadDetailedRegulationList()">리스트 불러오기</button> | |
| </div> | |
| <!-- 법규 타입 선택 탭 추가 --> | |
| <div class="regulation-type-tabs" id="regulationTypeTabs" style="display: none;"> | |
| <button class="type-tab active" data-type="part">Part</button> | |
| <button class="type-tab" data-type="section">Section</button> | |
| <button class="type-tab" data-type="chapter">Chapter</button> | |
| <button class="type-tab" data-type="jo">조</button> | |
| </div> | |
| <div class="regulation-search" id="regulationSearchContainer" style="display: none;"> | |
| <input type="text" id="regulationSearchInput" class="regulation-search-input" placeholder="법규 검색..."> | |
| </div> | |
| <div class="regulation-details-list" id="regulationDetailsList"> | |
| <p style="color: var(--text-muted); padding: 20px; text-align: center; margin: 0;"> | |
| 리스트 불러오기를 눌러 상세 법규를 확인하세요. | |
| </p> | |
| </div> | |
| </div> | |
| <div class="action-buttons"> | |
| <button class="btn btn-secondary" onclick="clearAllSelections()">선택 초기화</button> | |
| </div> | |
| </aside> | |
| <!-- 채팅 패널 --> | |
| <section class="chat-panel"> | |
| <div class="chat-header"> | |
| <h1 class="chat-title">자동차 인증 법규 검색 | |
| <span class="title-subtitle">  관련 법규 내용을 검색으로 편리하게 찾아보세요! ※ 모든 결과물은 AI에 의해 생성된 것으로 오류가 있을 수 있습니다</span> | |
| </h1> | |
| <div id="statusText" class="status-text"> | |
| 시스템 준비 중... | |
| <div class="progress-bar" id="progressBar"></div> | |
| <button class="cancel-search-btn" id="cancelSearchBtn" onclick="cancelSearch()">취소</button> | |
| </div> | |
| </div> | |
| <div class="chat-messages" id="chatMessages"> | |
| <div class="message assistant"> | |
| <div class="message-avatar">Lexi</div> | |
| <div> | |
| <div class="message-content"> | |
| 안녕하세요! 자동차 인증 법규 검색 도우미입니다.<br> | |
| 궁금하신 법규 내용을 검색해보세요.<br><br> | |
| (ex : ISA의 작동 요건을 알려주고, 표로 정리해줘) | |
| </div> | |
| <div class="message-time" id="welcomeTime"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 채팅 입력 영역 --> | |
| <div class="chat-input-container"> | |
| <div class="chat-input-wrapper"> | |
| <textarea | |
| id="chatInput" | |
| class="chat-input" | |
| placeholder="검색하고 싶은 내용을 입력하세요..." | |
| rows="1" | |
| ></textarea> | |
| <button class="send-btn" id="sendBtn" title="전송"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| // ===== 전역 변수 ===== | |
| let selectedRegulations = []; | |
| let selectedRegions = []; | |
| let socket; | |
| let currentSessionId = null; | |
| let isSearching = false; | |
| let searchEachMode = false; | |
| let regulationData = {}; // 모든 법규 데이터 저장 | |
| let currentRegulationType = 'part'; // 현재 선택된 법규 타입 | |
| // ===== 초기화 ===== | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeSocket(); | |
| initializeSidebar(); | |
| initializeToggles(); | |
| initializeChat(); | |
| initializeRegulationList(); | |
| initializeSearchModeToggle(); | |
| initializeRegulationTypeTabs(); // 탭 초기화 추가 | |
| setWelcomeTime(); | |
| updateSelectedCount(); | |
| }); | |
| // ===== 법규 타입 탭 초기화 ===== | |
| function initializeRegulationTypeTabs() { | |
| const tabs = document.querySelectorAll('.type-tab'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| const type = this.getAttribute('data-type'); | |
| switchRegulationType(type); | |
| }); | |
| }); | |
| } | |
| function switchRegulationType(type) { | |
| currentRegulationType = type; | |
| // 탭 활성화 상태 변경 | |
| document.querySelectorAll('.type-tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| document.querySelector(`.type-tab[data-type="${type}"]`).classList.add('active'); | |
| // 해당 타입의 데이터 표시 | |
| if (regulationData[`reg_list_${type}`]) { | |
| displayDetailedRegulationList(regulationData[`reg_list_${type}`], type); | |
| } | |
| console.log('법규 타입 변경:', type); | |
| } | |
| // ===== 검색 모드 토글 초기화 ===== | |
| function initializeSearchModeToggle() { | |
| const searchModeToggle = document.getElementById('searchEachModeToggle'); | |
| const modeIndicator = document.getElementById('searchModeIndicator'); | |
| const modeDescription = document.getElementById('searchModeDescription'); | |
| searchModeToggle.addEventListener('change', function() { | |
| searchEachMode = this.checked; | |
| updateSearchModeDisplay(); | |
| }); | |
| updateSearchModeDisplay(); // 초기 상태 설정 | |
| } | |
| function updateSearchModeDisplay() { | |
| const modeIndicator = document.getElementById('searchModeIndicator'); | |
| const modeDescription = document.getElementById('searchModeDescription'); | |
| if (searchEachMode) { | |
| modeIndicator.textContent = '각각 검색 모드'; | |
| modeDescription.textContent = '선택된 각 법규별로 개별 검색을 수행하고 결과를 따로 표시합니다.'; | |
| } else { | |
| modeIndicator.textContent = '통합 검색 모드'; | |
| modeDescription.textContent = '선택된 모든 법규를 통합하여 한 번에 검색하고 결과를 종합하여 표시합니다.'; | |
| } | |
| console.log('검색 모드 변경:', searchEachMode ? '각각 검색' : '통합 검색'); | |
| } | |
| // 환영 메시지 시간 설정 | |
| function setWelcomeTime() { | |
| const timeElement = document.getElementById('welcomeTime'); | |
| if (timeElement) { | |
| const now = new Date(); | |
| timeElement.textContent = formatTime(now); | |
| } | |
| } | |
| // 시간 포맷팅 | |
| function formatTime(date) { | |
| const hours = date.getHours().toString().padStart(2, '0'); | |
| const minutes = date.getMinutes().toString().padStart(2, '0'); | |
| return `${hours}:${minutes}`; | |
| } | |
| // ===== Socket.IO 초기화 ===== | |
| function initializeSocket() { | |
| socket = io(); | |
| // 기존 시스템 메시지 | |
| socket.on('message', function(data) { | |
| updateSearchStatus(data.message); | |
| }); | |
| // 검색 시작 - session_id 받기 (추가됨) | |
| socket.on('search_started', function(data) { | |
| currentSessionId = data.session_id; | |
| console.log('Search started with session ID:', currentSessionId); | |
| }); | |
| // 검색 상태 업데이트 | |
| socket.on('search_status', function(data) { | |
| updateSearchStatus(data.message); | |
| if (data.progress !== undefined) { | |
| updateProgressBar(data.progress); | |
| } | |
| }); | |
| // 법규별 결과 수신 (여러 법규 선택 시) | |
| socket.on('regulation_result', function(data) { | |
| const message = `**${data.regulation_title} (${data.regulation_index}/${data.total_regulations})**\n\n${data.result}`; | |
| addMessage(message, 'assistant'); | |
| }); | |
| // 단일 검색 결과 수신 | |
| socket.on('search_result', function(data) { | |
| addMessage(data.result, 'assistant'); | |
| }); | |
| // 검색 완료 | |
| socket.on('search_complete', function(data) { | |
| hideTypingIndicator(); | |
| updateSearchStatus(data.message); | |
| enableChatInput(); | |
| hideCancelButton(); | |
| isSearching = false; | |
| currentSessionId = null; // 세션 ID 초기화 | |
| }); | |
| // 검색 오류 | |
| socket.on('search_error', function(data) { | |
| hideTypingIndicator(); | |
| addMessage(`죄송합니다. ${data.message}\n오류 내용: ${data.error}`, 'assistant'); | |
| enableChatInput(); | |
| hideCancelButton(); | |
| isSearching = false; | |
| currentSessionId = null; // 세션 ID 초기화 | |
| }); | |
| // 검색 취소 | |
| socket.on('search_cancelled', function(data) { | |
| hideTypingIndicator(); | |
| addMessage('검색이 취소되었습니다.', 'assistant'); | |
| enableChatInput(); | |
| hideCancelButton(); | |
| isSearching = false; | |
| currentSessionId = null; // 세션 ID 초기화 | |
| }); | |
| } | |
| function updateSearchStatus(message) { | |
| const statusElement = document.getElementById('statusText'); | |
| if (statusElement) { | |
| // 첫 번째 텍스트 노드 업데이트 | |
| const textNode = statusElement.childNodes[0]; | |
| if (textNode && textNode.nodeType === Node.TEXT_NODE) { | |
| textNode.textContent = message; | |
| } else { | |
| // 텍스트 노드가 없으면 새로 생성 | |
| statusElement.insertBefore(document.createTextNode(message), statusElement.firstChild); | |
| } | |
| } | |
| } | |
| function updateProgressBar(progress) { | |
| const progressBar = document.getElementById('progressBar'); | |
| if (progressBar) { | |
| progressBar.style.width = `${progress}%`; | |
| } | |
| } | |
| function showCancelButton() { | |
| const cancelBtn = document.getElementById('cancelSearchBtn'); | |
| if (cancelBtn) { | |
| cancelBtn.style.display = 'block'; | |
| console.log('Cancel button shown'); | |
| } | |
| } | |
| function hideCancelButton() { | |
| const cancelBtn = document.getElementById('cancelSearchBtn'); | |
| if (cancelBtn) { | |
| cancelBtn.style.display = 'none'; | |
| console.log('Cancel button hidden'); | |
| } | |
| updateProgressBar(0); | |
| } | |
| function enableChatInput() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| chatInput.disabled = false; | |
| sendBtn.disabled = false; | |
| chatInput.placeholder = '검색하고 싶은 내용을 입력하세요...'; | |
| } | |
| function disableChatInput() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| chatInput.disabled = true; | |
| sendBtn.disabled = true; | |
| chatInput.placeholder = '검색 진행 중...'; | |
| } | |
| // ===== 사이드바 초기화 ===== | |
| function initializeSidebar() { | |
| const sidebarToggle = document.getElementById('sidebarToggle'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const sidebarOverlay = document.getElementById('sidebarOverlay'); | |
| const newPageBtn = document.getElementById('newPageBtn'); | |
| sidebarToggle.addEventListener('click', function() { | |
| const isOpen = sidebar.classList.contains('open'); | |
| if (isOpen) { | |
| closeSidebar(); | |
| } else { | |
| openSidebar(); | |
| } | |
| }); | |
| sidebarOverlay.addEventListener('click', closeSidebar); | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Escape') { | |
| closeSidebar(); | |
| } | |
| }); | |
| if (newPageBtn) { | |
| newPageBtn.addEventListener('click', function() { | |
| window.open('/', '_blank'); | |
| }); | |
| } | |
| } | |
| function openSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const sidebarToggle = document.getElementById('sidebarToggle'); | |
| const sidebarOverlay = document.getElementById('sidebarOverlay'); | |
| const newPageBtn = document.getElementById('newPageBtn'); | |
| sidebar.classList.add('open'); | |
| sidebarToggle.classList.add('active'); | |
| sidebarOverlay.classList.add('active'); | |
| if (newPageBtn) newPageBtn.classList.add('show'); | |
| } | |
| function closeSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const sidebarToggle = document.getElementById('sidebarToggle'); | |
| const sidebarOverlay = document.getElementById('sidebarOverlay'); | |
| const newPageBtn = document.getElementById('newPageBtn'); | |
| sidebar.classList.remove('open'); | |
| sidebarToggle.classList.remove('active'); | |
| sidebarOverlay.classList.remove('active'); | |
| if (newPageBtn) newPageBtn.classList.remove('show'); | |
| } | |
| // ===== 토글 버튼 초기화 ===== | |
| function initializeToggles() { | |
| document.querySelectorAll('.region-toggle').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| handleRegionToggle(this); | |
| }); | |
| }); | |
| } | |
| function handleRegionToggle(clickedBtn) { | |
| const value = clickedBtn.getAttribute('data-value'); | |
| const allButtons = document.querySelectorAll('.region-toggle'); | |
| if (value === '전체') { | |
| // 전체 선택 - 다른 모든 버튼 비활성화 | |
| allButtons.forEach(btn => btn.classList.remove('active')); | |
| clickedBtn.classList.add('active'); | |
| selectedRegions.length = 0; | |
| } else { | |
| // 개별 선택 - 전체 버튼 비활성화 | |
| const allBtn = document.querySelector('.region-toggle[data-value="전체"]'); | |
| if (allBtn) allBtn.classList.remove('active'); | |
| clickedBtn.classList.toggle('active'); | |
| if (clickedBtn.classList.contains('active')) { | |
| if (!selectedRegions.includes(value)) { | |
| selectedRegions.push(value); | |
| } | |
| } else { | |
| selectedRegions = selectedRegions.filter(region => region !== value); | |
| } | |
| // 아무것도 선택되지 않았으면 전체 활성화 | |
| if (selectedRegions.length === 0 && allBtn) { | |
| allBtn.classList.add('active'); | |
| } | |
| } | |
| console.log('선택된 지역:', selectedRegions); | |
| } | |
| function getSelectedRegions() { | |
| return selectedRegions.length > 0 ? selectedRegions : []; | |
| } | |
| // ===== 법규 리스트 초기화 ===== | |
| function initializeRegulationList() { | |
| // 이미 상세 법규 리스트에서 처리되므로 여기서는 빈 함수로 유지 | |
| } | |
| function toggleRegulationSelection(element) { | |
| const id = element.getAttribute('data-id'); | |
| if (selectedRegulations.includes(id)) { | |
| selectedRegulations = selectedRegulations.filter(item => item !== id); | |
| element.classList.remove('selected'); | |
| } else { | |
| selectedRegulations.push(id); | |
| element.classList.add('selected'); | |
| } | |
| updateSelectedCount(); | |
| } | |
| function updateSelectedCount() { | |
| const countElement = document.getElementById('selectedCount'); | |
| if (countElement) { | |
| countElement.textContent = `선택됨: ${selectedRegulations.length}`; | |
| } | |
| } | |
| // ===== 채팅 초기화 (수정된 버전) ===== | |
| function initializeChat() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| // 자동 높이 조절 | |
| chatInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 120) + 'px'; | |
| }); | |
| // Enter 키로 전송 | |
| chatInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && !e.shiftKey && !isSearching) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // 전송 버튼 클릭 | |
| sendBtn.addEventListener('click', function() { | |
| if (!isSearching) { | |
| sendMessage(); | |
| } | |
| }); | |
| } | |
| // ===== 메시지 전송 (수정된 버전) ===== | |
| function sendMessage() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const message = chatInput.value.trim(); | |
| if (!message || isSearching) return; | |
| // 사용자 메시지 추가 | |
| addMessage(message, 'user'); | |
| // 입력창 초기화 및 비활성화 | |
| chatInput.value = ''; | |
| chatInput.style.height = 'auto'; | |
| disableChatInput(); | |
| isSearching = true; | |
| // 로딩 표시 | |
| showTypingIndicator(); | |
| showCancelButton(); | |
| // 서버에 소켓으로 요청 (검색 모드 포함) | |
| const requestData = { | |
| query: message, | |
| regions: getSelectedRegions(), | |
| searchEachMode: searchEachMode, // 검색 모드 추가 | |
| selectedRegulations: selectedRegulations.map(id => { | |
| const element = document.querySelector(`[data-id="${id}"]`); | |
| return { | |
| id: id, | |
| title: element ? element.getAttribute('data-title') || element.textContent.trim() : id | |
| }; | |
| }) | |
| }; | |
| // 소켓으로 검색 요청 전송 | |
| socket.emit('search_query', requestData); | |
| } | |
| // 검색 취소 함수 (수정됨) | |
| function cancelSearch() { | |
| console.log('Cancel search clicked. Session ID:', currentSessionId, 'Is searching:', isSearching); | |
| if (currentSessionId && isSearching) { | |
| socket.emit('cancel_search', { session_id: currentSessionId }); | |
| console.log('Cancel request sent with session ID:', currentSessionId); | |
| } else { | |
| console.log('Cannot cancel: no session ID or not searching'); | |
| // 로컬에서 검색 상태 초기화 | |
| hideTypingIndicator(); | |
| enableChatInput(); | |
| hideCancelButton(); | |
| isSearching = false; | |
| currentSessionId = null; | |
| updateSearchStatus('검색이 취소되었습니다.'); | |
| } | |
| } | |
| function addMessage(text, type) { | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `message ${type}`; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = type === 'user' ? 'Me' : 'Lexi'; | |
| const contentWrapper = document.createElement('div'); | |
| const content = document.createElement('div'); | |
| content.className = 'message-content'; | |
| if (type === 'assistant') { | |
| // 마크다운 스타일 텍스트를 HTML로 변환 | |
| //const formattedText = formatMarkdownToHtml(text); | |
| content.innerHTML = text; | |
| } else { | |
| content.textContent = text; | |
| } | |
| const time = document.createElement('div'); | |
| time.className = 'message-time'; | |
| time.textContent = formatTime(new Date()); | |
| contentWrapper.appendChild(content); | |
| contentWrapper.appendChild(time); | |
| messageDiv.appendChild(avatar); | |
| messageDiv.appendChild(contentWrapper); | |
| // 타이핑 인디케이터가 있으면 그 위에 추가 | |
| const typingIndicator = document.getElementById('typingIndicator'); | |
| if (typingIndicator) { | |
| chatMessages.insertBefore(messageDiv, typingIndicator); | |
| } else { | |
| chatMessages.appendChild(messageDiv); | |
| } | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| function formatMarkdownToHtml(text) { | |
| // 간단한 마크다운 변환 | |
| let html = text; | |
| // **굵은 글씨** 변환 | |
| html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); | |
| // 줄바꿈 변환 | |
| html = html.replace(/\n/g, '<br>'); | |
| return html; | |
| } | |
| function showTypingIndicator() { | |
| // 기존 타이핑 인디케이터 제거 | |
| hideTypingIndicator(); | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const indicator = document.createElement('div'); | |
| indicator.className = 'message assistant'; | |
| indicator.id = 'typingIndicator'; | |
| const avatar = document.createElement('div'); | |
| avatar.className = 'message-avatar'; | |
| avatar.textContent = 'Lexi'; | |
| const typing = document.createElement('div'); | |
| typing.className = 'typing-indicator'; | |
| typing.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>'; | |
| indicator.appendChild(avatar); | |
| indicator.appendChild(typing); | |
| chatMessages.appendChild(indicator); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| function hideTypingIndicator() { | |
| const indicator = document.getElementById('typingIndicator'); | |
| if (indicator) { | |
| indicator.remove(); | |
| } | |
| } | |
| function decodeHtmlEntities(text) { | |
| const textarea = document.createElement('textarea'); | |
| textarea.innerHTML = text; | |
| return textarea.value; | |
| } | |
| // ===== 상세 법규 리스트 관련 함수 ===== | |
| function loadDetailedRegulationList() { | |
| const selectedRegions = getSelectedRegions(); | |
| const detailsListDiv = document.getElementById('regulationDetailsList'); | |
| const searchContainer = document.getElementById('regulationSearchContainer'); | |
| const typeTabs = document.getElementById('regulationTypeTabs'); | |
| detailsListDiv.innerHTML = '<p style="color: var(--text-muted); padding: 20px; text-align: center; margin: 0;">상세 법규 리스트 로딩 중...</p>'; | |
| fetch('/get_reg_list', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ regions: selectedRegions }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| // 모든 법규 데이터 저장 | |
| regulationData = data; | |
| if (data.reg_list_part || data.reg_list_section || data.reg_list_chapter || data.reg_list_jo) { | |
| // 탭과 검색창 표시 | |
| typeTabs.style.display = 'flex'; | |
| searchContainer.style.display = 'block'; | |
| // 기본적으로 part 타입 표시 | |
| switchRegulationType('part'); | |
| initializeRegulationSearch(); | |
| } else { | |
| detailsListDiv.innerHTML = '<p style="color: var(--text-muted); padding: 20px; text-align: center; margin: 0;">상세 법규 리스트를 불러올 수 없습니다.</p>'; | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('상세 법규 리스트 로딩 오류:', error); | |
| detailsListDiv.innerHTML = '<p style="color: #ff6b6b; padding: 20px; text-align: center; margin: 0;">리스트 로딩 중 오류가 발생했습니다.</p>'; | |
| }); | |
| } | |
| function displayDetailedRegulationList(data, type) { | |
| const detailsListDiv = document.getElementById('regulationDetailsList'); | |
| let listHTML = ''; | |
| if (typeof data === 'string' && data.trim()) { | |
| const lines = data.split('\n').filter(line => line.trim()); | |
| lines.forEach((line, index) => { | |
| const regId = `detail_reg_${type}_${Date.now()}_${index}`; | |
| const isSelected = selectedRegulations.includes(regId); | |
| listHTML += ` | |
| <div class="regulation-detail-item ${isSelected ? 'selected' : ''}" | |
| data-id="${regId}" | |
| data-title="${line.trim()}" | |
| data-type="${type}" | |
| data-search-text="${line.trim().toLowerCase()}"> | |
| ${line.trim()} | |
| </div> | |
| `; | |
| }); | |
| } else if (Array.isArray(data)) { | |
| data.forEach((item, index) => { | |
| const regId = item.id || `detail_reg_${type}_${Date.now()}_${index}`; | |
| const title = item.title || item.name || item.toString(); | |
| const isSelected = selectedRegulations.includes(regId); | |
| listHTML += ` | |
| <div class="regulation-detail-item ${isSelected ? 'selected' : ''}" | |
| data-id="${regId}" | |
| data-title="${title}" | |
| data-type="${type}" | |
| data-search-text="${title.toLowerCase()}"> | |
| ${title} | |
| </div> | |
| `; | |
| }); | |
| } else { | |
| listHTML = '<p style="color: var(--text-muted); padding: 20px; text-align: center; margin: 0;">해당 타입의 법규 데이터가 없습니다.</p>'; | |
| } | |
| detailsListDiv.innerHTML = listHTML; | |
| updateSelectedCount(); | |
| // 상세 법규 리스트 클릭 이벤트 추가 (기존 이벤트 리스너 제거 후 재생성) | |
| detailsListDiv.removeEventListener('click', handleRegulationClick); | |
| detailsListDiv.addEventListener('click', handleRegulationClick); | |
| } | |
| function handleRegulationClick(e) { | |
| const item = e.target.closest('.regulation-detail-item'); | |
| if (item) { | |
| toggleRegulationSelection(item); | |
| } | |
| } | |
| function toggleRegulationSelection(element) { | |
| const id = element.getAttribute('data-id'); | |
| const isSelected = selectedRegulations.find(reg => reg.id === id); | |
| if (isSelected) { | |
| selectedRegulations = selectedRegulations.filter(reg => reg.id !== id); | |
| element.classList.remove('selected'); | |
| } else { | |
| const regulationData = { | |
| id: id, | |
| title: element.getAttribute('data-title') || element.textContent.trim(), | |
| type: element.getAttribute('data-type') | |
| }; | |
| selectedRegulations.push(regulationData); | |
| element.classList.add('selected'); | |
| } | |
| updateSelectedCount(); | |
| } | |
| function updateSelectedCount() { | |
| const countElement = document.getElementById('selectedCount'); | |
| if (countElement) { | |
| countElement.textContent = `선택됨: ${selectedRegulations.length}`; | |
| } | |
| } | |
| // ===== 메시지 전송 (수정된 버전) ===== | |
| function sendMessage() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const message = chatInput.value.trim(); | |
| if (!message || isSearching) return; | |
| // 사용자 메시지 추가 | |
| addMessage(message, 'user'); | |
| // 입력창 초기화 및 비활성화 | |
| chatInput.value = ''; | |
| chatInput.style.height = 'auto'; | |
| disableChatInput(); | |
| isSearching = true; | |
| // 로딩 표시 | |
| showTypingIndicator(); | |
| showCancelButton(); | |
| // 서버에 소켓으로 요청 (법규 타입 정보 포함) | |
| const requestData = { | |
| query: message, | |
| regions: getSelectedRegions(), | |
| searchEachMode: searchEachMode, | |
| selectedRegulations: selectedRegulations.map(reg => { | |
| return { | |
| id: reg.id, | |
| title: reg.title, | |
| type: reg.type // 법규 타입 정보 추가 | |
| }; | |
| }) | |
| }; | |
| console.log('전송 데이터:', requestData); // 디버깅용 | |
| // 소켓으로 검색 요청 전송 | |
| socket.emit('search_query', requestData); | |
| } | |
| function clearAllSelections() { | |
| selectedRegulations = []; | |
| document.querySelectorAll('.regulation-item').forEach(item => { | |
| item.classList.remove('selected'); | |
| }); | |
| document.querySelectorAll('.regulation-detail-item').forEach(item => { | |
| item.classList.remove('selected'); | |
| }); | |
| updateSelectedCount(); | |
| // 검색창 초기화 | |
| const searchInput = document.getElementById('regulationSearchInput'); | |
| if (searchInput) { | |
| searchInput.value = ''; | |
| filterRegulationList(''); | |
| } | |
| } | |
| function initializeRegulationSearch() { | |
| const searchInput = document.getElementById('regulationSearchInput'); | |
| // 기존 이벤트 리스너 제거 후 새로 추가 | |
| searchInput.removeEventListener('input', handleSearchInput); | |
| searchInput.addEventListener('input', handleSearchInput); | |
| } | |
| function handleSearchInput() { | |
| const searchTerm = this.value.toLowerCase().trim(); | |
| filterRegulationList(searchTerm); | |
| } | |
| function filterRegulationList(searchTerm) { | |
| const detailItems = document.querySelectorAll('.regulation-detail-item'); | |
| detailItems.forEach(item => { | |
| const searchText = item.getAttribute('data-search-text') || ''; | |
| if (searchTerm === '' || searchText.includes(searchTerm)) { | |
| item.classList.remove('hidden'); | |
| } else { | |
| item.classList.add('hidden'); | |
| } | |
| }); | |
| } | |
| function clearAllSelections() { | |
| selectedRegulations = []; | |
| document.querySelectorAll('.regulation-item').forEach(item => { | |
| item.classList.remove('selected'); | |
| }); | |
| document.querySelectorAll('.regulation-detail-item').forEach(item => { | |
| item.classList.remove('selected'); | |
| }); | |
| updateSelectedCount(); | |
| // 검색창 초기화 | |
| const searchInput = document.getElementById('regulationSearchInput'); | |
| if (searchInput) { | |
| searchInput.value = ''; | |
| filterRegulationList(''); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |