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; | |
| } | |
| .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: 8px; | |
| padding: 8px 12px; | |
| background: var(--bg-tertiary); | |
| border-radius: var(--radius); | |
| font-size: 12px; | |
| color: var(--primary-color); | |
| } | |
| .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; | |
| } | |
| </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> | |
| <!-- 상세 법규 리스트 --> | |
| <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-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> | |
| </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; | |
| // ===== 초기화 ===== | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeSocket(); | |
| initializeSidebar(); | |
| initializeToggles(); | |
| initializeChat(); | |
| initializeRegulationList(); | |
| setWelcomeTime(); | |
| updateSelectedCount(); | |
| }); | |
| // 환영 메시지 시간 설정 | |
| 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) { | |
| document.getElementById('statusText').innerText = data.message; | |
| }); | |
| } | |
| // ===== 사이드바 초기화 ===== | |
| 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() { | |
| const regulationListDiv = document.getElementById('regulationList'); | |
| regulationListDiv.addEventListener('click', function(e) { | |
| const item = e.target.closest('.regulation-item'); | |
| if (item) { | |
| toggleRegulationSelection(item); | |
| } | |
| }); | |
| } | |
| 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) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| // 전송 버튼 클릭 | |
| sendBtn.addEventListener('click', sendMessage); | |
| } | |
| // ===== 메시지 전송 ===== | |
| function sendMessage() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // 사용자 메시지 추가 | |
| addMessage(message, 'user'); | |
| // 입력창 초기화 | |
| chatInput.value = ''; | |
| chatInput.style.height = 'auto'; | |
| // 로딩 표시 | |
| showTypingIndicator(); | |
| // 서버에 요청 | |
| const requestData = { | |
| query: message, | |
| regions: getSelectedRegions(), | |
| selectedRegulations: selectedRegulations.map(id => { | |
| const element = document.querySelector(`[data-id="${id}"]`); | |
| return { | |
| id: id, | |
| title: element ? element.getAttribute('data-title') || element.textContent.trim() : id | |
| }; | |
| }) | |
| }; | |
| fetch('/get_message', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(requestData) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| hideTypingIndicator(); | |
| if (data.message) { | |
| addMessage(data.message, 'assistant'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('검색 오류:', error); | |
| hideTypingIndicator(); | |
| addMessage('죄송합니다. 검색 중 오류가 발생했습니다.', 'assistant'); | |
| }); | |
| } | |
| 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') { | |
| const decodedText = decodeHtmlEntities(text); | |
| content.innerHTML = decodedText; | |
| } 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); | |
| chatMessages.appendChild(messageDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| function decodeHtmlEntities(text) { | |
| const textarea = document.createElement('textarea'); | |
| textarea.innerHTML = text; | |
| return textarea.value; | |
| } | |
| function showTypingIndicator() { | |
| 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 loadDetailedRegulationList() { | |
| const selectedRegions = getSelectedRegions(); | |
| const detailsListDiv = document.getElementById('regulationDetailsList'); | |
| const searchContainer = document.getElementById('regulationSearchContainer'); | |
| 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 => { | |
| if (data.reg_list_part) { | |
| displayDetailedRegulationList(data.reg_list_part); | |
| searchContainer.style.display = 'block'; | |
| 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) { | |
| const detailsListDiv = document.getElementById('regulationDetailsList'); | |
| let listHTML = ''; | |
| if (typeof data === 'string') { | |
| const lines = data.split('\n').filter(line => line.trim()); | |
| lines.forEach((line, index) => { | |
| const regId = `detail_reg_${Date.now()}_${index}`; | |
| const isSelected = selectedRegulations.includes(regId); | |
| listHTML += ` | |
| <div class="regulation-detail-item ${isSelected ? 'selected' : ''}" | |
| data-id="${regId}" | |
| data-title="${line.trim()}" | |
| data-search-text="${line.trim().toLowerCase()}"> | |
| ${line.trim()} | |
| </div> | |
| `; | |
| }); | |
| } else if (Array.isArray(data)) { | |
| data.forEach((item, index) => { | |
| const regId = item.id || `detail_reg_${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-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.addEventListener('click', function(e) { | |
| const item = e.target.closest('.regulation-detail-item'); | |
| if (item) { | |
| toggleRegulationSelection(item); | |
| } | |
| }); | |
| } | |
| function initializeRegulationSearch() { | |
| const searchInput = document.getElementById('regulationSearchInput'); | |
| searchInput.addEventListener('input', function() { | |
| 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> |