lexistudio / templates /chat_v02.html
scipious's picture
Update templates/chat_v02.html
79130d9 verified
<!DOCTYPE html>
<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">&nbsp&nbsp관련 법규 내용을 검색으로 편리하게 찾아보세요! ※ 모든 결과물은 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>