@@ -166,23 +220,73 @@ function renderMailList(tabType, mailsToShow = null) {
`).join('');
- // 초기 로드 시 active가 있다면 본문도 업데이트
const activeIdx = mails.findIndex(m => m.active);
- if (activeIdx !== -1) {
- updateMailContent(mails[activeIdx]);
- }
+ if (activeIdx !== -1) updateMailContent(mails[activeIdx]);
}
-function handleCheckboxClick(event) {
- event.stopPropagation();
+function selectMailItem(el, index) {
+ document.querySelectorAll('.mail-item').forEach(item => {
+ item.classList.remove('active');
+ const nameSpan = item.querySelector('.mail-item-content span:first-child');
+ if (nameSpan) nameSpan.style.color = 'var(--text-main)';
+ });
+ el.classList.add('active');
+ const currentNameSpan = el.querySelector('.mail-item-content span:first-child');
+ if (currentNameSpan) currentNameSpan.style.color = 'var(--primary-color)';
+
+ const mail = filteredMails[index];
+ if (mail) updateMailContent(mail);
}
+function updateMailContent(mail) {
+ const headerTitle = document.querySelector('.mail-content-header h2');
+ const senderInfo = document.querySelectorAll('.mail-content-header div')[0];
+ const dateInfo = document.querySelectorAll('.mail-content-header div')[1];
+ const bodyInfo = document.querySelector('.mail-body');
+
+ if (headerTitle) headerTitle.innerText = mail.title;
+ if (senderInfo) senderInfo.innerHTML = `
${currentMailTab === 'outbound' ? '받는사람' : '보낸사람'} ${mail.email} (${mail.person})`;
+ if (dateInfo) dateInfo.innerHTML = `
날짜 ${mail.time}`;
+ if (bodyInfo) bodyInfo.innerHTML = mail.summary.replace(/\n/g, '
') + "
본 내용은 샘플 데이터입니다.";
+}
+
+function switchMailTab(el, tabType) {
+ document.querySelectorAll('.mail-tab').forEach(tab => tab.classList.remove('active'));
+ el.classList.add('active');
+ MAIL_SAMPLES[tabType].forEach((m, idx) => m.active = (idx === 0));
+ renderMailList(tabType);
+}
+
+// --- 검색 및 필터 ---
+function searchMails() {
+ const query = document.querySelector('.search-bar input[type="text"]').value.toLowerCase();
+ const startDate = document.getElementById('startDate').value;
+ const endDate = document.getElementById('endDate').value;
+
+ const results = (MAIL_SAMPLES[currentMailTab] || []).filter(mail => {
+ const matchQuery = mail.title.toLowerCase().includes(query) ||
+ mail.person.toLowerCase().includes(query) ||
+ mail.summary.toLowerCase().includes(query);
+ let matchDate = true;
+ if (startDate) matchDate = matchDate && (mail.time >= startDate);
+ if (endDate) matchDate = matchDate && (mail.time <= endDate);
+ return matchQuery && matchDate;
+ });
+ renderMailList(currentMailTab, results);
+}
+
+function resetSearch() {
+ document.querySelector('.search-bar input[type="text"]').value = '';
+ document.getElementById('startDate').value = '';
+ document.getElementById('endDate').value = '';
+ renderMailList(currentMailTab);
+}
+
+// --- 액션바 관리 ---
function updateBulkActionBar() {
const checkboxes = document.querySelectorAll('.mail-item-checkbox:checked');
const actionBar = document.getElementById('mailBulkActions');
const selectedCountSpan = document.getElementById('selectedCount');
- const selectAllCheckbox = document.getElementById('selectAllMails');
-
if (!actionBar || !selectedCountSpan) return;
if (checkboxes.length > 0) {
@@ -190,13 +294,13 @@ function updateBulkActionBar() {
selectedCountSpan.innerText = `${checkboxes.length}개 선택됨`;
} else {
actionBar.classList.remove('active');
- if (selectAllCheckbox) selectAllCheckbox.checked = false;
+ const selectAll = document.getElementById('selectAllMails');
+ if (selectAll) selectAll.checked = false;
}
}
function toggleSelectAll(el) {
- const checkboxes = document.querySelectorAll('.mail-item-checkbox');
- checkboxes.forEach(cb => cb.checked = el.checked);
+ document.querySelectorAll('.mail-item-checkbox').forEach(cb => cb.checked = el.checked);
updateBulkActionBar();
}
@@ -222,145 +326,41 @@ function deleteSelectedMails() {
renderMailList(currentMailTab);
}
-function selectMailItem(el, index) {
- // UI 업데이트
- document.querySelectorAll('.mail-item').forEach(item => {
- item.classList.remove('active');
- const nameSpan = item.querySelector('.mail-item-content span:first-child');
- if (nameSpan) nameSpan.style.color = 'var(--text-main)';
- });
- el.classList.add('active');
- const currentNameSpan = el.querySelector('.mail-item-content span:first-child');
- if (currentNameSpan) currentNameSpan.style.color = 'var(--primary-color)';
-
- // 본문 업데이트
- const mail = filteredMails[index];
- if (mail) {
- updateMailContent(mail);
- }
-}
-
-function updateMailContent(mail) {
- const headerTitle = document.querySelector('.mail-content-header h2');
- const senderInfo = document.querySelectorAll('.mail-content-header div')[0];
- const dateInfo = document.querySelectorAll('.mail-content-header div')[1];
- const bodyInfo = document.querySelector('.mail-body');
-
- if (headerTitle) headerTitle.innerText = mail.title;
- if (senderInfo) senderInfo.innerHTML = `
${currentMailTab === 'outbound' ? '받는사람' : '보낸사람'} ${mail.email} (${mail.person})`;
- if (dateInfo) dateInfo.innerHTML = `
날짜 ${mail.time}`;
- if (bodyInfo) bodyInfo.innerHTML = mail.summary.replace(/\n/g, '
') + "
" + "본 내용은 샘플 데이터입니다.";
-}
-
-function switchMailTab(el, tabType) {
- document.querySelectorAll('.mail-tab').forEach(tab => tab.classList.remove('active'));
- el.classList.add('active');
-
- // 탭 이동 시 active 상태 초기화 (첫 번째 메일만 active 하거나 전부 해제)
- MAIL_SAMPLES[tabType].forEach((m, idx) => m.active = (idx === 0));
-
- renderMailList(tabType);
-}
-
-// --- 검색 기능 ---
-function searchMails() {
- const query = document.querySelector('.search-bar input[type="text"]').value.toLowerCase();
- const startDate = document.getElementById('startDate').value;
- const endDate = document.getElementById('endDate').value;
-
- const mails = MAIL_SAMPLES[currentMailTab];
- const results = mails.filter(mail => {
- const matchQuery = mail.title.toLowerCase().includes(query) ||
- mail.person.toLowerCase().includes(query) ||
- mail.summary.toLowerCase().includes(query);
-
- let matchDate = true;
- if (startDate) matchDate = matchDate && (mail.time >= startDate);
- if (endDate) matchDate = matchDate && (mail.time <= endDate);
-
- return matchQuery && matchDate;
- });
-
- renderMailList(currentMailTab, results);
-}
-
-function resetSearch() {
- document.querySelector('.search-bar input[type="text"]').value = '';
- document.getElementById('startDate').value = '';
- document.getElementById('endDate').value = '';
- document.querySelector('.search-bar select').selectedIndex = 0;
-
- renderMailList(currentMailTab);
-}
-
-// --- 모달 및 기타 유틸리티 ---
-function togglePreview(show) {
- const previewArea = document.getElementById('mailPreviewArea');
- if (show) previewArea.classList.add('active');
- else previewArea.classList.remove('active');
-}
-
-function showPreview(index, event) {
- if (event.target.closest('.btn-group') || event.target.closest('.path-display')) return;
- const file = currentFiles[index];
- const previewContainer = document.getElementById('previewContainer');
- const fullViewBtn = document.getElementById('fullViewBtn');
- document.querySelectorAll('.attachment-item').forEach(item => item.classList.remove('active'));
- event.currentTarget.classList.add('active');
- togglePreview(true);
- const isPdf = file.name.toLowerCase().endsWith('.pdf');
- const fileUrl = `/sample_files/${encodeURIComponent(file.name)}`;
- if (fullViewBtn) {
- fullViewBtn.style.display = 'block';
- fullViewBtn.onclick = () => window.open(fileUrl, 'PMFullView', 'width=1000,height=800');
- }
- if (isPdf) {
- previewContainer.innerHTML = `
`;
- } else {
- previewContainer.innerHTML = `

${file.name}
`;
- }
-}
-
+// --- 경로 선택 모달 ---
function openPathModal(index, event) {
if (event) event.stopPropagation();
editingIndex = index;
const modal = document.getElementById('pathModal');
const tabSelect = document.getElementById('tabSelect');
- if (!tabSelect) return;
+ if (!modal || !tabSelect) return;
+
tabSelect.innerHTML = Object.keys(HIERARCHY).map(tab => `
`).join('');
updateCategories();
modal.style.display = 'flex';
}
function updateCategories() {
- const tabSelect = document.getElementById('tabSelect');
+ const tab = document.getElementById('tabSelect').value;
const catSelect = document.getElementById('categorySelect');
- if (!tabSelect || !catSelect) return;
- const tab = tabSelect.value;
- const cats = Object.keys(HIERARCHY[tab]);
- catSelect.innerHTML = cats.map(cat => `
`).join('');
+ if (!catSelect) return;
+ catSelect.innerHTML = Object.keys(HIERARCHY[tab]).map(cat => `
`).join('');
updateSubs();
}
function updateSubs() {
- const tabSelect = document.getElementById('tabSelect');
- const catSelect = document.getElementById('categorySelect');
+ const tab = document.getElementById('tabSelect').value;
+ const cat = document.getElementById('categorySelect').value;
const subSelect = document.getElementById('subSelect');
- if (!tabSelect || !catSelect || !subSelect) return;
- const tab = tabSelect.value;
- const cat = catSelect.value;
- const subs = HIERARCHY[tab][cat];
- subSelect.innerHTML = subs.map(sub => `
`).join('');
+ if (!subSelect) return;
+ subSelect.innerHTML = HIERARCHY[tab][cat].map(sub => `
`).join('');
}
function applyPathSelection() {
- const tabSelect = document.getElementById('tabSelect');
- const catSelect = document.getElementById('categorySelect');
- const subSelect = document.getElementById('subSelect');
- const tab = tabSelect.value;
- const cat = catSelect.value;
- const sub = subSelect.value;
+ const tab = document.getElementById('tabSelect').value;
+ const cat = document.getElementById('categorySelect').value;
+ const sub = document.getElementById('subSelect').value;
const fullPath = `${tab} > ${cat} > ${sub}`;
+
if (!currentFiles[editingIndex].analysis) currentFiles[editingIndex].analysis = {};
currentFiles[editingIndex].analysis.suggested_path = fullPath;
currentFiles[editingIndex].analysis.isManual = true;
@@ -373,6 +373,7 @@ function closeModal() {
if (modal) modal.style.display = 'none';
}
+// --- AI 분석 및 업로드 ---
async function startAnalysis(index, event) {
if (event) event.stopPropagation();
const file = currentFiles[index];
@@ -380,20 +381,21 @@ async function startAnalysis(index, event) {
const logContent = document.getElementById(`log-content-${index}`);
const recLabel = document.getElementById(`recommend-${index}`);
if (!logArea || !logContent || !recLabel) return;
+
logArea.classList.add('active');
- logContent.innerHTML = '
>>> 3중 레이어 AI 분석 엔진 가동...
';
+ logContent.innerHTML = '
>>> AI 분석 엔진 가동 중...
';
recLabel.innerText = '분석 중...';
+
try {
const res = await fetch(`/analyze-file?filename=${encodeURIComponent(file.name)}`);
const analysis = await res.json();
const result = analysis.final_result;
- const steps = [`1. 파일 포맷 분석: ${file.name.split('.').pop().toUpperCase()} 감지`, `2. 페이지 스캔: 총 ${analysis.total_pages}페이지 분석 완료`, `3. 문맥 추론: ${result.reason}`];
- steps.forEach(step => {
- const line = document.createElement('div');
- line.className = 'log-line';
- line.innerText = " " + step;
- logContent.appendChild(line);
- });
+
+ logContent.innerHTML = `
+
1. 파일 포맷 분석: ${file.name.split('.').pop().toUpperCase()} 감지
+
2. 페이지 스캔: 총 ${analysis.total_pages}페이지 분석 완료
+
3. 문맥 추론: ${result.reason}
+ `;
currentFiles[index].analysis = { suggested_path: result.suggested_path, isManual: false };
renderFiles();
} catch (e) {
@@ -406,15 +408,13 @@ function confirmUpload(index, event) {
if (event) event.stopPropagation();
const file = currentFiles[index];
if (!file.analysis || !file.analysis.suggested_path) { alert("경로를 설정해주세요."); return; }
- if (confirm(`정해진 위치로 업로드하시겠습니까?\n\n위치: ${file.analysis.suggested_path}`)) alert("업로드가 완료되었습니다.");
+ if (confirm(`업로드하시겠습니까?\n위치: ${file.analysis.suggested_path}`)) alert("완료되었습니다.");
}
-// --- 주소록 기능 ---
+// --- 주소록 ---
let addressBookData = [
{ name: "이태훈", dept: "PM Overseas / 선임연구원", email: "th.lee@projectmaster.com", phone: "010-1234-5678" },
- { name: "Pany S.", dept: "라오스 농림부 / 국장", email: "pany.s@lao.gov.la", phone: "+856-20-1234-5678" },
- { name: "김철수", dept: "현대건설 / 현장소장", email: "cs.kim@hdec.co.kr", phone: "010-9876-5432" },
- { name: "Nguyen Van A", dept: "베트남 전력청 / 팀장", email: "nva@evn.com.vn", phone: "+84-90-1234-5678" }
+ { name: "Pany S.", dept: "라오스 농림부 / 국장", email: "pany.s@lao.gov.la", phone: "+856-20-1234-5678" }
];
let contactEditingIndex = -1;
@@ -425,33 +425,60 @@ function openAddressBook() {
function closeAddressBook() {
const modal = document.getElementById('addressBookModal');
- if (modal) { modal.style.display = 'none'; document.getElementById('addContactForm').style.display = 'none'; contactEditingIndex = -1; }
+ if (modal) { modal.style.display = 'none'; document.getElementById('addContactForm').style.display = 'none'; }
+}
+
+function toggleAddContactForm() {
+ const form = document.getElementById('addContactForm');
+ if (!form) return;
+ if (form.style.display === 'none') {
+ form.style.display = 'block';
+ } else {
+ form.style.display = 'none';
+ contactEditingIndex = -1;
+ // 폼 초기화
+ document.getElementById('newContactName').value = '';
+ document.getElementById('newContactDept').value = '';
+ document.getElementById('newContactEmail').value = '';
+ document.getElementById('newContactPhone').value = '';
+ }
+}
+
+function editContact(index) {
+ const contact = addressBookData[index];
+ if (!contact) return;
+ contactEditingIndex = index;
+
+ document.getElementById('newContactName').value = contact.name;
+ document.getElementById('newContactDept').value = contact.dept;
+ document.getElementById('newContactEmail').value = contact.email;
+ document.getElementById('newContactPhone').value = contact.phone;
+
+ document.getElementById('addContactForm').style.display = 'block';
+}
+
+function deleteContact(index) {
+ if (!addressBookData[index]) return;
+ if (confirm(`'${addressBookData[index].name}'님을 주소록에서 삭제하시겠습니까?`)) {
+ addressBookData.splice(index, 1);
+ renderAddressBook();
+ }
}
function renderAddressBook() {
const body = document.getElementById('addressBookBody');
if (!body) return;
- body.innerHTML = addressBookData.map((c, idx) => `
| ${c.name} | ${c.dept} | ${c.email} | ${c.phone} | |
`).join('');
-}
-
-function toggleAddContactForm() {
- const form = document.getElementById('addContactForm');
- if (form.style.display === 'none') form.style.display = 'block';
- else { form.style.display = 'none'; contactEditingIndex = -1; }
-}
-
-function editContact(index) {
- const contact = addressBookData[index];
- contactEditingIndex = index;
- document.getElementById('newContactName').value = contact.name;
- document.getElementById('newContactDept').value = contact.dept;
- document.getElementById('newContactEmail').value = contact.email;
- document.getElementById('newContactPhone').value = contact.phone;
- document.getElementById('addContactForm').style.display = 'block';
-}
-
-function deleteContact(index) {
- if (confirm(`'${addressBookData[index].name}'님을 주소록에서 삭제하시겠습니까?`)) { addressBookData.splice(index, 1); renderAddressBook(); }
+ body.innerHTML = addressBookData.map((c, idx) => `
+
+ | ${c.name} |
+ ${c.dept} |
+ ${c.email} |
+ ${c.phone} |
+
+
+
+ |
+
`).join('');
}
function addContact() {
@@ -459,21 +486,17 @@ function addContact() {
const dept = document.getElementById('newContactDept').value;
const email = document.getElementById('newContactEmail').value;
const phone = document.getElementById('newContactPhone').value;
- if (!name) { alert("이름을 입력해주세요."); return; }
- const newData = { name, dept, email, phone };
- if (contactEditingIndex > -1) { addressBookData[contactEditingIndex] = newData; contactEditingIndex = -1; }
- else addressBookData.push(newData);
+ if (!name) return alert("이름을 입력해주세요.");
+
+ if (contactEditingIndex > -1) addressBookData[contactEditingIndex] = { name, dept, email, phone };
+ else addressBookData.push({ name, dept, email, phone });
+
renderAddressBook();
- toggleAddContactForm();
-}
-
-function togglePreviewAuto() {
- const area = document.getElementById('mailPreviewArea');
- const icon = document.getElementById('previewToggleIcon');
- const isActive = area.classList.toggle('active');
- if (icon) icon.innerText = isActive ? '▶' : '◀';
+ document.getElementById('addContactForm').style.display = 'none';
+ contactEditingIndex = -1;
}
+// 초기화
document.addEventListener('DOMContentLoaded', () => {
loadAttachments();
renderMailList('inbound');
diff --git a/migrate_db_history.py b/migrate_db_history.py
deleted file mode 100644
index b77629e..0000000
--- a/migrate_db_history.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import pymysql
-import os
-
-def get_db():
- return pymysql.connect(
- host='localhost', user='root', password='45278434',
- database=os.getenv('DB_NAME', 'PM_proto'), charset='utf8mb4'
- )
-
-def migrate_to_timeseries():
- conn = get_db()
- try:
- with conn.cursor() as cursor:
- # 1. 기존 고유 제약 조건 제거 (project_id 중복 허용을 위함)
- try:
- cursor.execute("ALTER TABLE overseas_projects DROP INDEX project_id")
- print(">>> 기존 project_id 고유 제약 제거")
- except: pass
-
- # 2. crawl_date 컬럼 추가 (날짜별 데이터 구분을 위함)
- cursor.execute("DESCRIBE overseas_projects")
- cols = [row[0] for row in cursor.fetchall()]
- if 'crawl_date' not in cols:
- cursor.execute("ALTER TABLE overseas_projects ADD COLUMN crawl_date DATE AFTER project_id")
- print(">>> crawl_date 컬럼 추가")
-
- # 3. 기존 데이터의 crawl_date를 오늘로 채움
- cursor.execute("UPDATE overseas_projects SET crawl_date = DATE(updated_at) WHERE crawl_date IS NULL")
-
- # 4. 새로운 복합 고유 제약 추가 (ID + 날짜 조합으로 중복 방지)
- # 같은 날짜에 다시 크롤링하면 덮어쓰고, 날짜가 다르면 새로 생성됨
- try:
- cursor.execute("ALTER TABLE overseas_projects ADD UNIQUE INDEX idx_project_date (project_id, crawl_date)")
- print(">>> 복합 고유 제약(project_id + crawl_date) 추가 완료")
- except: pass
-
- conn.commit()
- print(">>> DB 시계열 마이그레이션 성공!")
- finally:
- conn.close()
-
-if __name__ == "__main__":
- migrate_to_timeseries()
diff --git a/migrate_normalized.py b/migrate_normalized.py
deleted file mode 100644
index b8cc930..0000000
--- a/migrate_normalized.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import pymysql
-import os
-
-def get_db():
- return pymysql.connect(
- host='localhost', user='root', password='45278434',
- database=os.getenv('DB_NAME', 'PM_proto'), charset='utf8mb4',
- cursorclass=pymysql.cursors.DictCursor
- )
-
-def migrate_to_normalized_tables():
- conn = get_db()
- try:
- with conn.cursor() as cursor:
- # 1. 마스터 테이블 생성 (고유 정보)
- cursor.execute("""
- CREATE TABLE IF NOT EXISTS projects_master (
- project_id VARCHAR(100) PRIMARY KEY,
- project_nm VARCHAR(255) NOT NULL,
- short_nm VARCHAR(255),
- department VARCHAR(255),
- continent VARCHAR(100),
- country VARCHAR(100),
- master VARCHAR(100),
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- """)
-
- # 2. 히스토리 테이블 생성 (일일 변동 정보)
- cursor.execute("""
- CREATE TABLE IF NOT EXISTS projects_history (
- id INT AUTO_INCREMENT PRIMARY KEY,
- project_id VARCHAR(100) NOT NULL,
- crawl_date DATE NOT NULL,
- recent_log VARCHAR(255),
- file_count INT DEFAULT 0,
- recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- UNIQUE KEY idx_proj_date (project_id, crawl_date),
- FOREIGN KEY (project_id) REFERENCES projects_master(project_id) ON DELETE CASCADE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- """)
-
- # 3. 기존 데이터 이전
- # 3-1. 마스터 정보 이전
- cursor.execute("""
- INSERT IGNORE INTO projects_master (project_id, project_nm, short_nm, department, continent, country, master)
- SELECT project_id, project_nm, short_nm, department, continent, country, master
- FROM overseas_projects
- """)
-
- # 3-2. 히스토리 정보 이전
- cursor.execute("""
- INSERT IGNORE INTO projects_history (project_id, crawl_date, recent_log, file_count)
- SELECT project_id, crawl_date, recent_log, file_count
- FROM overseas_projects
- """)
-
- # 4. 기존 단일 테이블 삭제 (성공 후 삭제)
- # cursor.execute("DROP TABLE IF EXISTS overseas_projects")
-
- conn.commit()
- print(">>> DB 정규화 마이그레이션 완료 (Master / History 분리)")
- finally:
- conn.close()
-
-if __name__ == "__main__":
- migrate_to_normalized_tables()
diff --git a/server.log b/server.log
deleted file mode 100644
index 9a46719..0000000
Binary files a/server.log and /dev/null differ
diff --git a/server.py b/server.py
index ea709f2..aa8244c 100644
--- a/server.py
+++ b/server.py
@@ -13,6 +13,7 @@ from fastapi.templating import Jinja2Templates
from analyze import analyze_file_content
from crawler_service import run_crawler_service, crawl_stop_event
+from sql_queries import InquiryQueries, DashboardQueries
# --- 환경 설정 ---
os.environ["PYTHONIOENCODING"] = "utf-8"
@@ -87,7 +88,7 @@ async def get_inquiries(pm_type: str = None, category: str = None, status: str =
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
- sql = "SELECT * FROM inquiries WHERE 1=1"
+ sql = InquiryQueries.SELECT_BASE
params = []
if pm_type:
sql += " AND pm_type = %s"
@@ -102,7 +103,7 @@ async def get_inquiries(pm_type: str = None, category: str = None, status: str =
sql += " AND (content LIKE %s OR author LIKE %s OR project_nm LIKE %s)"
params.extend([f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"])
- sql += " ORDER BY no DESC"
+ sql += f" {InquiryQueries.ORDER_BY_DESC}"
cursor.execute(sql, params)
return cursor.fetchall()
except Exception as e:
@@ -113,7 +114,7 @@ async def get_inquiry_detail(id: int):
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
- cursor.execute("SELECT * FROM inquiries WHERE id = %s", (id,))
+ cursor.execute(InquiryQueries.SELECT_BY_ID, (id,))
return cursor.fetchone()
except Exception as e:
return {"error": str(e)}
@@ -123,13 +124,8 @@ async def update_inquiry_reply(id: int, req: InquiryReplyRequest):
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
- handled_date = datetime.now().strftime("%Y. %m. %d")
- sql = """
- UPDATE inquiries
- SET reply = %s, status = %s, handler = %s, handled_date = %s
- WHERE id = %s
- """
- cursor.execute(sql, (req.reply, req.status, req.handler, handled_date, id))
+ handled_date = datetime.now().strftime("%Y.%m.%d")
+ cursor.execute(InquiryQueries.UPDATE_REPLY, (req.reply, req.status, req.handler, handled_date, id))
conn.commit()
return {"success": True}
except Exception as e:
@@ -140,12 +136,7 @@ async def delete_inquiry_reply(id: int):
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
- sql = """
- UPDATE inquiries
- SET reply = '', status = '미확인', handled_date = ''
- WHERE id = %s
- """
- cursor.execute(sql, (id,))
+ cursor.execute(InquiryQueries.DELETE_REPLY, (id,))
conn.commit()
return {"success": True}
except Exception as e:
@@ -158,7 +149,7 @@ async def get_available_dates():
try:
with get_db_connection() as conn:
with conn.cursor() as cursor:
- cursor.execute("SELECT DISTINCT crawl_date FROM projects_history ORDER BY crawl_date DESC")
+ cursor.execute(DashboardQueries.GET_AVAILABLE_DATES)
rows = cursor.fetchall()
return [row['crawl_date'].strftime("%Y.%m.%d") for row in rows if row['crawl_date']]
except Exception as e:
@@ -172,20 +163,13 @@ async def get_project_data(date: str = None):
with get_db_connection() as conn:
with conn.cursor() as cursor:
if not target_date:
- cursor.execute("SELECT MAX(crawl_date) as last_date FROM projects_history")
+ cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE)
res = cursor.fetchone()
target_date = res['last_date']
if not target_date: return {"projects": []}
- sql = """
- SELECT m.project_nm, m.short_nm, m.department, m.master,
- h.recent_log, h.file_count, m.continent, m.country
- FROM projects_master m
- LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
- ORDER BY m.project_id ASC
- """
- cursor.execute(sql, (target_date,))
+ cursor.execute(DashboardQueries.GET_PROJECT_LIST, (target_date,))
rows = cursor.fetchall()
projects = []
@@ -203,7 +187,7 @@ async def get_project_activity(date: str = None):
with get_db_connection() as conn:
with conn.cursor() as cursor:
if not date or date == "-":
- cursor.execute("SELECT MAX(crawl_date) as last_date FROM projects_history")
+ cursor.execute(DashboardQueries.GET_LAST_CRAWL_DATE)
res = cursor.fetchone()
target_date_val = res['last_date'] if res['last_date'] else datetime.now().date()
else:
@@ -212,12 +196,7 @@ async def get_project_activity(date: str = None):
target_date_dt = datetime.combine(target_date_val, datetime.min.time())
# 아코디언 리스트와 동일하게 마스터의 모든 프로젝트를 가져오되, 해당 날짜의 히스토리를 매칭
- sql = """
- SELECT m.project_id, m.project_nm, m.short_nm, h.recent_log, h.file_count
- FROM projects_master m
- LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
- """
- cursor.execute(sql, (target_date_val,))
+ cursor.execute(DashboardQueries.GET_PROJECT_LIST_FOR_ANALYSIS, (target_date_val,))
rows = cursor.fetchall()
analysis = {"summary": {"active": 0, "warning": 0, "stale": 0, "unknown": 0}, "details": []}
diff --git a/sql_queries.py b/sql_queries.py
new file mode 100644
index 0000000..99ac959
--- /dev/null
+++ b/sql_queries.py
@@ -0,0 +1,67 @@
+class InquiryQueries:
+ """문의사항(Inquiries) 페이지 관련 쿼리"""
+ # 필터링을 위한 기본 쿼리 (WHERE 1=1 포함)
+ SELECT_BASE = "SELECT * FROM inquiries WHERE 1=1"
+ ORDER_BY_DESC = "ORDER BY no DESC"
+
+ # 상세 조회
+ SELECT_BY_ID = "SELECT * FROM inquiries WHERE id = %s"
+
+ # 답변 업데이트 (handled_date 포함)
+ UPDATE_REPLY = """
+ UPDATE inquiries
+ SET reply = %s, status = %s, handler = %s, handled_date = %s
+ WHERE id = %s
+ """
+
+ # 답변 삭제 (초기화)
+ DELETE_REPLY = """
+ UPDATE inquiries
+ SET reply = '', status = '미확인', handled_date = ''
+ WHERE id = %s
+ """
+
+class DashboardQueries:
+ """대시보드(Dashboard) 및 프로젝트 현황 관련 쿼리"""
+ # 가용 날짜 목록 조회
+ GET_AVAILABLE_DATES = "SELECT DISTINCT crawl_date FROM projects_history ORDER BY crawl_date DESC"
+
+ # 최신 수집 날짜 조회
+ GET_LAST_CRAWL_DATE = "SELECT MAX(crawl_date) as last_date FROM projects_history"
+
+ # 특정 날짜 프로젝트 데이터 JOIN 조회
+ GET_PROJECT_LIST = """
+ SELECT m.project_nm, m.short_nm, m.department, m.master,
+ h.recent_log, h.file_count, m.continent, m.country
+ FROM projects_master m
+ LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
+ ORDER BY m.project_id ASC
+ """
+
+ # 활성도 분석을 위한 프로젝트 목록 조회
+ GET_PROJECT_LIST_FOR_ANALYSIS = """
+ SELECT m.project_id, m.project_nm, m.short_nm, h.recent_log, h.file_count
+ FROM projects_master m
+ LEFT JOIN projects_history h ON m.project_id = h.project_id AND h.crawl_date = %s
+ """
+
+class CrawlerQueries:
+ """크롤러(Crawler) 데이터 동기화 관련 쿼리"""
+ # 마스터 정보 UPSERT (INSERT OR UPDATE)
+ UPSERT_MASTER = """
+ INSERT INTO projects_master (project_id, project_nm, short_nm, master, continent, country)
+ VALUES (%s, %s, %s, %s, %s, %s)
+ ON DUPLICATE KEY UPDATE
+ project_nm = VALUES(project_nm), short_nm = VALUES(short_nm),
+ master = VALUES(master), continent = VALUES(continent), country = VALUES(country)
+ """
+
+ # 부서 정보 업데이트
+ UPDATE_DEPARTMENT = "UPDATE projects_master SET department = %s WHERE project_id = %s"
+
+ # 히스토리(로그/파일수) 저장
+ UPSERT_HISTORY = """
+ INSERT INTO projects_history (project_id, crawl_date, recent_log, file_count)
+ VALUES (%s, CURRENT_DATE(), %s, %s)
+ ON DUPLICATE KEY UPDATE recent_log=VALUES(recent_log), file_count=VALUES(file_count)
+ """
diff --git a/style/common.css b/style/common.css
index 3ccba6a..79ae735 100644
--- a/style/common.css
+++ b/style/common.css
@@ -125,8 +125,21 @@ button { cursor: pointer; border: none; transition: all 0.2s ease; }
.btn-danger:hover { background: #fecaca; }
/* Existing Utils - Compatibility */
-._button-small { @extend .btn; padding: 6px 14px; font-size: 12px; background: var(--primary-color); color: #fff; border-radius: 6px; }
-._button-medium { @extend .btn; padding: 10px 20px; background: var(--primary-color); color: #fff; border-radius: 6px; font-weight: 700; }
+._button-xsmall {
+ display: inline-flex; align-items: center; justify-content: center;
+ padding: 4px 10px; font-size: 11px; font-weight: 600; border-radius: 4px; border: 1px solid var(--border-color);
+ background: #fff; color: var(--text-main); cursor: pointer; transition: 0.2s;
+}
+._button-xsmall:hover { background: var(--bg-muted); border-color: var(--primary-color); color: var(--primary-color); }
+
+._button-small {
+ display: inline-flex; align-items: center; justify-content: center;
+ padding: 6px 14px; font-size: 12px; background: var(--primary-color); color: #fff; border-radius: 6px; border: none; cursor: pointer;
+}
+._button-medium {
+ display: inline-flex; align-items: center; justify-content: center;
+ padding: 10px 20px; background: var(--primary-color); color: #fff; border-radius: 6px; font-weight: 700; border: none; cursor: pointer;
+}
.sync-btn { background: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; }
/* Badges */
diff --git a/style/dashboard.css b/style/dashboard.css
index 61a6645..9d4331c 100644
--- a/style/dashboard.css
+++ b/style/dashboard.css
@@ -1,45 +1,35 @@
+/* Dashboard Constants */
:root {
- --topbar-h: 36px;
--header-h: 56px;
--activity-h: 110px;
--fixed-total-h: calc(var(--topbar-h) + var(--header-h) + var(--activity-h));
-
- --primary-color: #1E5149;
- --primary-lv-0: #f0f7f4;
- --primary-lv-1: #e1eee9;
- --border-color: #e5e7eb;
- --bg-muted: #F9FAFB;
- --text-main: #111827;
- --text-sub: #6B7280;
- --error-color: #F21D0D;
}
-/* Portal (Index) */
+/* 1. Portal (Index) */
.portal-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: calc(100vh - var(--topbar-h));
- background: var(--bg-muted);
- padding: 32px;
- margin-top: var(--topbar-h);
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
+ height: calc(100vh - var(--topbar-h)); background: var(--bg-muted); padding: var(--space-lg); margin-top: var(--topbar-h);
}
-
.portal-header { text-align: center; margin-bottom: 50px; }
.portal-header h1 { font-size: 28px; color: var(--primary-color); margin-bottom: 10px; font-weight: 800; }
.portal-header p { color: var(--text-sub); font-size: 15px; }
+
.button-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; width: 100%; max-width: 800px; }
-.portal-card { background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px; text-align: center; transition: all 0.3s ease; width: 100%; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; align-items: center; gap: 20px; cursor: pointer; text-decoration: none; }
-.portal-card:hover { transform: translateY(-8px); border-color: var(--primary-color); box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); }
+.portal-card {
+ background: #fff; border: 1px solid var(--border-color); border-radius: 12px; padding: 40px;
+ text-align: center; transition: all 0.3s ease; width: 100%; box-shadow: var(--box-shadow);
+ display: flex; flex-direction: column; align-items: center; gap: 20px;
+}
+.portal-card:hover { transform: translateY(-8px); border-color: var(--primary-color); box-shadow: var(--box-shadow-lg); }
.portal-card i { font-size: 48px; color: var(--primary-color); }
.portal-card h3 { font-size: 20px; color: var(--text-main); margin: 0; }
.portal-card p { font-size: 14px; color: var(--text-sub); margin: 0; }
-/* Dashboard Fixed Elements */
+/* 2. Dashboard Header & Activity */
header {
position: fixed; top: var(--topbar-h); left: 0; right: 0; z-index: 1001;
- background: #fff; height: var(--header-h); display: flex; justify-content: space-between; align-items: center; padding: 0 32px; border-bottom: 1px solid #f5f5f5;
+ background: #fff; height: var(--header-h); display: flex; justify-content: space-between; align-items: center;
+ padding: 0 var(--space-lg); border-bottom: 1px solid #f5f5f5;
}
.activity-dashboard-wrapper {
@@ -47,19 +37,22 @@ header {
background: #fff; height: var(--activity-h); border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 6px rgba(0,0,0,0.03);
}
-.activity-dashboard { max-width: 1200px; margin: 0 auto; height: 100%; display: flex; gap: 15px; padding: 10px 32px 20px 32px; }
-.activity-card { flex: 1; padding: 12px 15px; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent; }
-.activity-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
+.activity-dashboard { max-width: 1200px; margin: 0 auto; height: 100%; display: flex; gap: 15px; padding: 10px 32px 20px; }
+.activity-card {
+ flex: 1; padding: 12px 15px; border-radius: var(--radius-lg); cursor: pointer;
+ display: flex; flex-direction: column; justify-content: center; gap: 2px; border-left: 5px solid transparent;
+}
+.activity-card:hover { transform: translateY(-2px); box-shadow: var(--box-shadow); }
.activity-card.active { background: #e8f5e9; border-left-color: #4DB251; }
.activity-card.warning { background: #fff8e1; border-left-color: #FFBF00; }
-.activity-card.stale { background: #ffebee; border-left-color: #F21D0D; }
+.activity-card.stale { background: #ffebee; border-left-color: var(--error-color); }
.activity-card.unknown { background: #f5f5f5; border-left-color: #9e9e9e; }
.activity-card .label { font-size: 11px; font-weight: 600; opacity: 0.7; }
.activity-card .count { font-size: 20px; font-weight: 800; }
-.main-content { margin-top: var(--fixed-total-h); padding: 32px; max-width: 1400px; margin-left: auto; margin-right: auto; }
+.main-content { margin-top: var(--fixed-total-h); padding: var(--space-lg); max-width: 1400px; margin-left: auto; margin-right: auto; }
-/* 로그 콘솔 (Terminal Style) */
+/* 3. Log Console */
.log-console {
position: sticky; top: var(--fixed-total-h); z-index: 999;
background: #000; color: #0f0; font-family: 'Consolas', monospace; padding: 15px; margin-bottom: 20px;
@@ -67,120 +60,64 @@ header {
}
.log-console-header { color: #fff; border-bottom: 1px solid #333; margin-bottom: 10px; padding-bottom: 5px; font-weight: bold; }
-/* 인증 모달 */
-.activity-modal-overlay {
- position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
- background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); z-index: 3000;
- display: flex; align-items: center; justify-content: center; padding: 20px;
+/* 4. Auth Modal (Page Specific) */
+.auth-modal-content {
+ background: #fff; width: 440px; border-radius: 16px; padding: 40px; text-align: center;
+ box-shadow: var(--box-shadow-modal); display: flex; flex-direction: column; gap: 32px;
}
-.activity-modal-content { background: #fff; width: 600px; max-height: 85vh; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
-.auth-modal-content { background: #fff; width: 440px; border-radius: 16px; padding: 40px; text-align: center; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); display: flex; flex-direction: column; gap: 32px; border: 1px solid rgba(0,0,0,0.05); }
-
-.auth-header i { font-size: 32px; color: var(--primary-color); margin-bottom: 16px; }
-.auth-header h3 { font-size: 20px; font-weight: 800; color: #111; margin-bottom: 8px; }
-.auth-header p { font-size: 13px; color: var(--text-sub); }
-
-.auth-body { display: flex; flex-direction: column; gap: 20px; text-align: left; }
-.input-group { display: flex; flex-direction: column; gap: 8px; }
+.input-group { display: flex; flex-direction: column; gap: 8px; text-align: left; }
.input-group label { font-size: 12px; font-weight: 700; color: var(--text-main); }
.input-group input {
height: 48px; padding: 0 16px; border: 1px solid var(--border-color); border-radius: 8px;
- font-size: 14px; transition: 0.2s; background: #f9f9f9;
+ font-size: 14px; background: #f9f9f9; width: 100%;
}
-.input-group input:focus { border-color: var(--primary-color); background: #fff; box-shadow: 0 0 0 3px var(--primary-lv-0); outline: none; }
+.input-group input:focus { border-color: var(--primary-color); background: #fff; outline: none; }
-.error-text { color: var(--error-color); font-size: 12px; font-weight: 600; text-align: center; margin-top: -10px; }
-
-.auth-footer { display: grid; grid-template-columns: 1fr 1.5fr; gap: 12px; }
-.auth-footer button {
- height: 48px; border-radius: 8px; font-size: 14px; font-weight: 700; cursor: pointer; border: none; transition: 0.2s;
-}
-.cancel-btn { background: #f1f3f5; color: #495057; }
-.cancel-btn:hover { background: #e9ecef; }
-.login-btn { background: var(--primary-color); color: #fff; }
-.login-btn:hover { background: #153a34; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(30,81,73,0.3); }
-.modal-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
-.modal-header h3 { margin: 0; font-size: 16px; color: var(--primary-color); }
-.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--text-sub); }
-.modal-body { padding: 20px; overflow-y: auto; }
-.modal-row { cursor: pointer; border-bottom: 1px solid #f5f5f5; }
-.modal-row:hover { background: var(--primary-lv-0); }
-
-/* Accordion Layout - 정밀 정렬 개선 */
+/* 5. Accordion & Data Tables */
.accordion-list-header {
position: sticky; top: var(--fixed-total-h); background: #fff; z-index: 900;
font-size: 11px; font-weight: 700; color: var(--text-sub);
- padding: 12px 24px; /* 패딩 통일 */
- border-bottom: 2px solid var(--primary-color);
- box-shadow: 0 4px 10px rgba(0,0,0,0.05);
- display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px; align-items: center;
+ padding: 12px 24px; border-bottom: 2px solid var(--primary-color);
+ display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px;
}
.accordion-header {
display: grid; grid-template-columns: 2.5fr 1fr 1fr 0.8fr 2fr; gap: 16px;
- padding: 12px 24px; /* 패딩 통일 */
- align-items: center; cursor: pointer; border-bottom: 1px solid var(--border-color);
- transition: background 0.1s;
+ padding: 12px 24px; align-items: center; cursor: pointer; border-bottom: 1px solid var(--border-color);
}
.accordion-item:hover .accordion-header { background: var(--primary-lv-0); }
.accordion-item.active .accordion-header { background: var(--primary-lv-0); border-bottom: none; }
-.repo-title { font-weight: 700; color: var(--primary-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.repo-dept, .repo-admin { font-size: 12px; color: var(--text-main); }
+.repo-title { font-weight: 700; color: var(--primary-color); @extend .text-truncate; }
.repo-files { text-align: center; font-weight: 600; }
-.repo-log { font-size: 11px; color: var(--text-sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.repo-log { font-size: 11px; color: var(--text-sub); @extend .text-truncate; }
-.accordion-body {
- display: none;
- padding: 24px 24px 32px 24px; /* 좌우 패딩을 행 헤더와 일치시킴 */
- background: #fff; /* 일체감을 위해 흰색 배경 사용 가능 (필요시 var(--bg-muted)) */
- border-bottom: 1px solid var(--border-color);
-}
+.accordion-body { display: none; padding: 24px; background: #fff; border-bottom: 1px solid var(--border-color); }
.accordion-item.active .accordion-body { display: block; }
-/* 상세 표 정밀 정렬 */
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; }
.detail-section h4 {
font-size: 13px; margin-bottom: 12px; color: var(--text-main);
- border-left: 3px solid var(--primary-color); padding-left: 10px;
- font-weight: 700;
+ border-left: 3px solid var(--primary-color); padding-left: 10px; font-weight: 700;
}
-.data-table {
- width: 100%; border-collapse: collapse; font-size: 12px;
- table-layout: fixed; /* 컬럼 너비 고정을 위해 필수 */
-}
-.data-table th, .data-table td {
- padding: 10px 8px; border-bottom: 1px solid var(--border-color);
- text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
-}
-.data-table th { color: var(--text-sub); font-weight: 600; background: #fcfcfc; }
-
-/* 컬럼별 너비 고정 (위아래 일치 및 정리) */
-/* 참여 인원 상세 표 비율 */
-#personnel-table th:nth-child(1), #personnel-table td:nth-child(1) { width: 25%; }
-#personnel-table th:nth-child(2), #personnel-table td:nth-child(2) { width: 45%; }
-#personnel-table th:nth-child(3), #personnel-table td:nth-child(3) { width: 30%; }
-
-/* 최근 활동 표 비율 */
-#activity-table th:nth-child(1), #activity-table td:nth-child(1) { width: 20%; }
-#activity-table th:nth-child(2), #activity-table td:nth-child(2) { width: 50%; }
-#activity-table th:nth-child(3), #activity-table td:nth-child(3) { width: 30%; }
-
-/* Status Styles */
-.status-warning { background: #fffcf0; }
-.status-error { background: #fff5f4; }
-.warning-text { color: var(--error-color) !important; font-weight: 700; }
+/* Personnel & Activity Tables */
+#personnel-table th:nth-child(1) { width: 25%; }
+#personnel-table th:nth-child(2) { width: 45%; }
+#activity-table th:nth-child(1) { width: 20%; }
+#activity-table th:nth-child(2) { width: 50%; }
+/* Location Groups */
.continent-group, .country-group { margin-bottom: 15px; }
-.continent-header, .country-header { background: #fff; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-weight: 700; }
+.continent-header, .country-header {
+ background: #fff; padding: 14px 20px; border: 1px solid var(--border-color); border-radius: 8px;
+ display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-weight: 700;
+}
.continent-header { background: var(--primary-color); color: white; border: none; font-size: 15px; }
.country-header { font-size: 14px; color: var(--text-main); margin-top: 8px; }
.continent-body, .country-body { display: none; padding: 10px 0 10px 15px; }
.active>.continent-body, .active>.country-body { display: block; }
-.sync-btn { display: flex; align-items: center; gap: 8px; background-color: var(--primary-color); color: #fff; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: 0.2s; }
.admin-info { font-size: 12px; color: var(--text-sub); margin-left: 16px; padding: 6px 12px; background: #f8f9fa; border-radius: 4px; border: 1px solid var(--border-color); }
.admin-info strong { color: var(--primary-color); font-weight: 700; }
.base-date-info { font-size: 13px; color: var(--text-sub); background: #fdfdfd; padding: 6px 15px; border-radius: 6px; border: 1px solid var(--border-color); }
-.base-date-info strong { color: #333; font-weight: 700; }
diff --git a/style/inquiries.css b/style/inquiries.css
index 5b33ce3..88f6e9e 100644
--- a/style/inquiries.css
+++ b/style/inquiries.css
@@ -1,29 +1,68 @@
-
+/* 1. Layout & Board Structure */
.inquiry-board {
padding: 0 20px 32px 20px;
- max-width: 98%; /* Expanded from 1400px to use most of the screen */
- margin: 0 auto;
- margin-top: 36px; /* topbar height */
+ max-width: 98%;
+ margin: 36px auto 0;
}
-/* Sticky Header Wrapper */
.board-sticky-header {
position: sticky;
top: 36px;
background: #fff;
z-index: 1000;
- padding-top: 15px;
- padding-bottom: 10px;
+ padding: 15px 0 10px;
border-bottom: 1px solid #eee;
}
.board-header {
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: flex-end;
margin-bottom: 20px;
}
+/* 2. Stats Dashboard */
+.header-stats {
+ display: flex;
+ gap: 12px;
+}
+
+.stat-item {
+ background: #fff;
+ border: 1px solid #eee;
+ padding: 8px 16px;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 80px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.02);
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.stat-item:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.05);
+}
+
+.stat-label { font-size: 11px; font-weight: 600; color: #888; margin-bottom: 2px; }
+.stat-value { font-size: 18px; font-weight: 700; color: #333; }
+
+/* Status Border Colors */
+.stat-item.total { border-top: 3px solid #1e5149; }
+.stat-item.total .stat-value { color: #1e5149; }
+.stat-item.complete { border-top: 3px solid #2e7d32; }
+.stat-item.complete .stat-value { color: #2e7d32; }
+.stat-item.working { border-top: 3px solid #1565c0; }
+.stat-item.working .stat-value { color: #1565c0; }
+.stat-item.checking { border-top: 3px solid #ef6c00; }
+.stat-item.checking .stat-value { color: #ef6c00; }
+.stat-item.pending { border-top: 3px solid #673ab7; }
+.stat-item.pending .stat-value { color: #673ab7; }
+.stat-item.unconfirmed { border-top: 3px solid #9e9e9e; }
+.stat-item.unconfirmed .stat-value { color: #9e9e9e; }
+
+/* 3. Filters & Notice */
.notice-container {
background: #fdfdfd;
padding: 20px;
@@ -42,26 +81,11 @@
margin-top: 15px;
}
-.filter-group {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.filter-group label {
- font-size: 12px;
- font-weight: 600;
- color: #666;
-}
-
-.filter-group select,
-.filter-group input {
- padding: 8px 12px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
-}
+.filter-group { display: flex; flex-direction: column; gap: 4px; }
+.filter-group label { font-size: 12px; font-weight: 600; color: #666; }
+.filter-group select, .filter-group input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
+/* 4. Table Styles */
.inquiry-table {
width: 100%;
background: #fff;
@@ -74,7 +98,6 @@
.inquiry-table thead th {
position: sticky;
- top: 310px; /* Adjust this value based on header height or use dynamic JS */
background: #f8f9fa;
padding: 14px 16px;
text-align: left;
@@ -93,95 +116,43 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- max-width: 0; /* Necessary for text-overflow to work in table-cells with percentage or flex-like behavior */
}
-/* Specific overrides for columns that need more or less space */
+/* Table Row Hover & Active State */
+.inquiry-row:hover { background: #fcfcfc; cursor: pointer; }
+.inquiry-row.active-row { background-color: #f0f7f6 !important; }
+.inquiry-row.active-row td { font-weight: 600; color: #1e5149; border-bottom-color: transparent; }
+
+/* Status Badges */
+.status-badge { padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
+.status-complete { background: #e8f5e9; color: #2e7d32; }
+.status-working { background: #e3f2fd; color: #1565c0; }
+.status-checking { background: #fff3e0; color: #ef6c00; }
+.status-pending { background: #f5f5f5; color: #616161; }
+
+/* Table Columns Width & Truncation */
.inquiry-table td:nth-child(1) { max-width: 50px; } /* No */
-.inquiry-table td:nth-child(2) { max-width: 120px; } /* PM Type */
-.inquiry-table td:nth-child(3) { max-width: 100px; } /* Env */
-.inquiry-table td:nth-child(4) { max-width: 150px; } /* Category */
-.inquiry-table td:nth-child(5) { max-width: 200px; } /* Project */
-.inquiry-table td:nth-child(6) { max-width: 400px; } /* Content */
-.inquiry-table td:nth-child(7) { max-width: 400px; } /* Reply */
-.inquiry-table td:nth-child(8) { max-width: 100px; } /* Author */
-.inquiry-table td:nth-child(9) { max-width: 120px; } /* Date */
-.inquiry-table td:nth-child(10) { max-width: 100px; } /* Status */
+.inquiry-table td:nth-child(2) { max-width: 80px; text-align: center; } /* Image */
+.inquiry-table td:nth-child(3) { max-width: 120px; } /* PM Type */
+.inquiry-table td:nth-child(4) { max-width: 100px; } /* Env */
+.inquiry-table td:nth-child(5) { max-width: 150px; } /* Category */
+.inquiry-table td:nth-child(6) { max-width: 200px; } /* Project */
+.inquiry-table td:nth-child(7), .inquiry-table td:nth-child(8) { max-width: 400px; } /* Content & Reply */
+.inquiry-table td:nth-child(9) { max-width: 100px; } /* Author */
+.inquiry-table td:nth-child(10) { max-width: 120px; } /* Date */
+.inquiry-table td:nth-child(11) { max-width: 100px; } /* Status */
-/* Reset max-width for detail row to allow it to span full width */
-.detail-row td {
- max-width: none;
- white-space: normal;
- overflow: visible;
-}
-
-.status-badge {
- padding: 4px 10px;
- border-radius: 20px;
- font-size: 11px;
- font-weight: 700;
- display: inline-block;
-}
-
-.status-complete {
- background: #e8f5e9;
- color: #2e7d32;
-}
-
-.status-working {
- background: #e3f2fd;
- color: #1565c0;
-}
-
-.status-checking {
- background: #fff3e0;
- color: #ef6c00;
-}
-
-.status-pending {
- background: #f5f5f5;
- color: #616161;
-}
-
-.inquiry-row:hover {
- background: #fcfcfc;
- cursor: pointer;
-}
-
-/* Expanded Row Highlight */
-.inquiry-row.active-row {
- background-color: #f0f7f6 !important; /* Very light mint gray */
-}
-
-.inquiry-row.active-row td {
- font-weight: 600;
- color: #1e5149;
- border-bottom-color: transparent; /* Seamless connection with detail */
-}
-
-.content-preview {
- max-width: 500px; /* Increased from 300px */
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Accordion Detail Row Style */
-.detail-row {
- display: none;
- background: #fdfdfd;
-}
-
-.detail-row.active {
- display: table-row;
-}
+/* 5. Detail (Accordion) Styles */
+.detail-row { display: none; background: #fdfdfd; }
+.detail-row.active { display: table-row; }
+.detail-row td { max-width: none; white-space: normal; overflow: visible; }
.detail-container {
padding: 24px;
border-left: 6px solid #1e5149;
- background: #f9fafb; /* Slightly different background for grouping */
+ background: #f9fafb;
box-shadow: inset 0 4px 15px rgba(0,0,0,0.08);
- position: relative; /* For positioning the close button */
+ position: relative;
border-bottom: 2px solid #eee;
}
@@ -208,86 +179,38 @@
font-weight: 600;
color: #666;
cursor: pointer;
- display: flex;
- align-items: center;
- gap: 5px;
- transition: all 0.2s;
z-index: 10;
}
+.btn-close-accordion::after { content: "▲"; font-size: 10px; margin-left: 5px; }
-.btn-close-accordion:hover {
- background: #e0e0e0;
- color: #333;
-}
+.detail-meta-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 15px; font-size: 13px; color: #666; }
+.detail-label { font-weight: 700; color: #888; margin-right: 8px; }
-.btn-close-accordion::after {
- content: "▲";
- font-size: 10px;
-}
+.detail-q-section { background: #f8f9fa; padding: 20px; border-radius: 8px; }
+.detail-a-section { background: #f1f8f7; padding: 20px; border-radius: 8px; border-left: 5px solid #1e5149; }
-.detail-q-section {
- background: #f8f9fa;
- padding: 20px;
- border-radius: 8px;
-}
+/* 6. Image Preview & Foldable Section */
+.img-thumbnail { width: 32px; height: 32px; border-radius: 4px; object-fit: cover; border: 1px solid #ddd; cursor: pointer; transition: transform 0.2s; }
+.img-thumbnail:hover { transform: scale(1.1); }
+.no-img { font-size: 10px; color: #ccc; font-style: italic; }
-.detail-a-section {
- background: #f1f8f7;
- padding: 20px;
- border-radius: 8px;
- border-left: 5px solid #1e5149;
-}
+.detail-image-section { margin-bottom: 20px; background: #f9fafb; border-radius: 8px; border: 1px solid #e5e7eb; overflow: hidden; }
+.image-section-header { padding: 12px 16px; background: #f1f5f9; display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
+.image-section-header:hover { background: #e2e8f0; }
+.image-section-header h4 { margin: 0; color: #1e5149; display: flex; align-items: center; gap: 8px; }
+.image-section-content { padding: 20px; display: flex; justify-content: center; background: #fff; border-top: 1px solid #eee; }
+.image-section-content.collapsed { display: none; }
+.toggle-icon { font-size: 12px; color: #64748b; transition: transform 0.3s; }
+.detail-image-section.active .toggle-icon { transform: rotate(180deg); }
-.detail-meta-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 15px;
- margin-bottom: 15px;
- font-size: 13px;
- color: #666;
-}
+.preview-img { max-width: 100%; max-height: 400px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); object-fit: contain; }
-.detail-label {
- font-weight: 700;
- color: #888;
- margin-right: 8px;
-}
-
-/* Reply Form Enhancement */
+/* 7. Forms & Reply */
.reply-edit-form textarea {
- width: 100%;
- height: 120px; /* Fixed height */
- padding: 12px;
- border: 1px solid #ddd;
- border-radius: 6px;
- font-family: inherit;
- font-size: 14px;
- margin-bottom: 15px;
- resize: none; /* Disable resizing */
- background: #fff;
- transition: all 0.2s;
-}
-
-.reply-edit-form textarea:disabled,
-.reply-edit-form select:disabled,
-.reply-edit-form input:disabled {
- background: #fcfcfc;
- color: #666;
- border-color: #eee;
- cursor: default;
-}
-
-.reply-edit-form.readonly .btn-save,
-.reply-edit-form.readonly .btn-delete,
-.reply-edit-form.readonly .btn-cancel {
- display: none;
-}
-
-.reply-edit-form.editable .btn-edit {
- display: none;
-}
-
-.reply-edit-form.editable textarea {
- border-color: #1e5149;
- box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1);
+ width: 100%; height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 6px;
+ font-family: inherit; font-size: 14px; margin-bottom: 15px; resize: none; background: #fff;
}
+.reply-edit-form textarea:disabled, .reply-edit-form select:disabled, .reply-edit-form input:disabled { background: #fcfcfc; color: #666; border-color: #eee; }
+.reply-edit-form.readonly .btn-save, .reply-edit-form.readonly .btn-delete, .reply-edit-form.readonly .btn-cancel { display: none; }
+.reply-edit-form.editable .btn-edit { display: none; }
+.reply-edit-form.editable textarea { border-color: #1e5149; box-shadow: 0 0 0 2px rgba(30, 81, 73, 0.1); }
diff --git a/style/mail.css b/style/mail.css
index 6c90462..ff67104 100644
--- a/style/mail.css
+++ b/style/mail.css
@@ -1,477 +1,219 @@
-/* Mail Manager Layout */
+/* Mail Manager Layout (Vertical Split) */
.mail-wrapper {
- display: flex;
- height: calc(100vh - 36px);
- margin-top: 36px;
- background: #fff;
- overflow: hidden;
-}
-
-.mail-sidebar {
- display: none; /* 사이드바 삭제 */
+ display: flex; height: calc(100vh - var(--topbar-h));
+ margin-top: var(--topbar-h); background: #fff; overflow: hidden;
}
.mail-list-area {
- width: 400px;
- border-right: 1px solid var(--border-color);
- display: flex;
- flex-direction: column;
- height: 100%;
- background: #fff;
- position: relative;
-}
-
-/* 탭 가로 배치 복구 */
-.mail-tabs {
- display: flex;
- border-bottom: 1px solid var(--border-color);
- background: #f8f9fa;
- flex-shrink: 0;
- width: 100%;
+ width: 400px; border-right: 1px solid var(--border-color);
+ display: flex; flex-direction: column; height: 100%; background: #fff; position: relative;
}
+/* 1. Tabs & Search */
+.mail-tabs { display: flex; border-bottom: 1px solid var(--border-color); background: #f8f9fa; flex-shrink: 0; }
.mail-tab {
- flex: 1;
- padding: 12px 0;
- text-align: center;
- cursor: pointer;
- font-weight: 700;
- color: #a0aec0;
- font-size: 11px;
- transition: all 0.2s ease;
- border-bottom: 2px solid transparent;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
+ flex: 1; padding: 12px 0; text-align: center; cursor: pointer;
+ font-weight: 700; color: #a0aec0; font-size: 11px; transition: all 0.2s ease;
+ border-bottom: 2px solid transparent; display: flex; align-items: center; justify-content: center; gap: 6px;
}
+.mail-tab:hover { background: #edf2f7; color: var(--primary-color); }
+.mail-tab.active { color: var(--primary-color); border-bottom: 2px solid var(--primary-color); background: #fff; }
-.mail-tab:hover {
- background: #edf2f7;
- color: var(--primary-color);
-}
+.search-bar { padding: 16px 24px; border-bottom: 1px solid var(--border-color); background: #fff; flex-shrink: 0; }
-.mail-tab.active {
- color: var(--primary-color);
- border-bottom: 2px solid var(--primary-color);
- background: #fff;
-}
-
-/* 검색창 여백 및 간격 조정 */
-.search-bar {
- padding: 16px 24px;
- border-bottom: 1px solid var(--border-color);
- background: #fff;
- flex-shrink: 0;
- margin-bottom: 20px;
-}
-
-/* 상단 선택 액션바 */
.mail-bulk-actions {
- display: none;
- padding: 8px 16px;
- background: #f7fafc;
- border-bottom: 1px solid var(--border-color);
- align-items: center;
- justify-content: space-between;
- font-size: 12px;
+ display: none; padding: 8px 16px; background: #f7fafc;
+ border-bottom: 1px solid var(--border-color); align-items: center; justify-content: space-between; font-size: 12px;
}
+.mail-bulk-actions.active { display: flex; }
-.mail-bulk-actions.active {
- display: flex;
-}
-
-/* 메일리스트 컨테이너 */
-.mail-items-container {
- flex: 1;
- overflow-y: auto;
- padding-bottom: 60px; /* 하단 고정 버튼 공간 확보 */
-}
-
-/* 메일 아이템 스타일 */
+/* 2. Mail Items */
+.mail-items-container { flex: 1; overflow-y: auto; padding-bottom: 60px; }
.mail-item {
- padding: 16px;
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- transition: 0.2s;
- display: flex;
- align-items: flex-start;
+ padding: 16px; border-bottom: 1px solid var(--border-color); cursor: pointer;
+ display: flex; align-items: flex-start; transition: 0.2s;
}
+.mail-item:hover { background: var(--bg-muted); }
+.mail-item.active { background: var(--primary-lv-0); border-left: 4px solid var(--primary-color); }
-.mail-item:hover {
- background: var(--bg-muted);
-}
+.mail-item-checkbox { width: 16px; height: 16px; cursor: pointer; margin-right: 12px; margin-top: 2px; }
+.mail-item-content { flex: 1; min-width: 0; }
+.mail-item-info { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
+.mail-date { font-size: 11px; color: var(--text-sub); white-space: nowrap; }
-.mail-item.active {
- background: #E9EEED;
- border-left: 4px solid var(--primary-color);
-}
-
-.mail-item-checkbox {
- width: 16px;
- height: 16px;
- cursor: pointer;
- margin-right: 12px;
- margin-top: 2px;
-}
-
-.mail-item-content {
- flex: 1;
- min-width: 0;
-}
-
-.mail-item-info {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.mail-date {
- font-size: 11px;
- color: var(--text-sub);
- white-space: nowrap;
-}
-
-/* 텍스트형 삭제 버튼 스타일 */
.btn-mail-delete {
- background: #f7fafc;
- border: 1px solid var(--border-color);
- color: #718096;
- cursor: pointer;
- font-size: 10px;
- padding: 2px 8px;
- transition: all 0.2s;
- border-radius: 4px;
- font-weight: 600;
+ background: #f7fafc; border: 1px solid var(--border-color); color: #718096;
+ font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600;
}
+.btn-mail-delete:hover { color: var(--error-color); background: #fff5f5; border-color: #feb2b2; }
-.btn-mail-delete:hover {
- color: #e53e3e;
- background: #fff5f5;
- border-color: #feb2b2;
-}
-
-/* 하단 버튼 고정 */
-.address-book-footer {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- padding: 12px 16px;
- border-top: 1px solid var(--border-color);
- background: #fff;
- flex-shrink: 0;
- z-index: 5;
- display: flex;
- gap: 8px;
-}
-
-.mail-content-area {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow-y: auto;
- border-right: 1px solid var(--border-color);
-}
-
-/* Mail Preview & Toggle Handle */
-.mail-preview-area {
- width: 0;
- background: #f1f3f5;
- display: flex;
- flex-direction: column;
- transition: all 0.3s ease;
- overflow: visible;
- position: relative;
- border-left: 0px solid transparent;
-}
-
-.mail-preview-area.active {
- width: 500px;
- border-left: 1px solid var(--border-color);
-}
-
-.mail-preview-area > *:not(.preview-toggle-handle) {
- opacity: 0;
- transition: opacity 0.2s;
- pointer-events: none;
-}
-
-.mail-preview-area.active > * {
- opacity: 1;
- pointer-events: auto;
-}
-
-.preview-toggle-handle {
- position: absolute;
- left: -20px;
- top: 50%;
- transform: translateY(-50%);
- width: 20px;
- height: 60px;
- background: var(--primary-color);
- color: #fff;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- border-radius: 8px 0 0 8px;
- font-size: 10px;
- box-shadow: -2px 0 5px rgba(0,0,0,0.1);
- z-index: 10;
-}
-
-.preview-toggle-handle:hover {
- background: var(--primary-lv-8);
-}
-
-.preview-header {
- padding: 12px 16px;
- background: #fff;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.preview-header h3 {
- font-size: 14px;
- font-weight: 700;
- color: var(--primary-color);
-}
-
-.preview-content {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- display: flex;
- justify-content: center;
- align-items: flex-start;
-}
-
-.a4-container {
- width: 100%;
- background: #fff;
- box-shadow: 0 0 20px rgba(0,0,0,0.1);
- position: relative;
- aspect-ratio: 1 / 1.414; /* A4 Ratio */
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.preview-placeholder {
- color: var(--text-sub);
- font-size: 13px;
- text-align: center;
- padding: 20px;
-}
-
-.preview-image {
- max-width: 100%;
- max-height: 100%;
- object-fit: contain;
-}
-
-.mail-content-header {
- padding: var(--space-lg);
- border-bottom: 1px solid var(--border-color);
-}
-
-.mail-body {
- padding: var(--space-lg);
- line-height: 1.6;
- min-height: 200px;
-}
-
-/* Attachments & AI Analysis */
-.attachment-area {
- padding: var(--space-lg);
- border-top: 1px solid var(--border-color);
- background: var(--bg-muted);
-}
+/* 3. Content Area */
+.mail-content-area { flex: 1; display: flex; flex-direction: column; overflow-y: auto; border-right: 1px solid var(--border-color); }
+.mail-content-header { padding: var(--space-lg); border-bottom: 1px solid var(--border-color); }
+.mail-body { padding: var(--space-lg); line-height: 1.6; min-height: 200px; }
+/* 4. Attachments & AI */
+.attachment-area { padding: var(--space-lg); border-top: 1px solid var(--border-color); background: var(--bg-muted); }
.attachment-item {
- display: flex;
- align-items: center;
- gap: var(--space-md);
- background: #fff;
- padding: var(--space-sm) var(--space-md);
- border-radius: var(--radius-lg);
- border: 1px solid var(--border-color);
- margin-bottom: var(--space-sm);
- cursor: pointer;
+ display: flex; align-items: center; gap: var(--space-md); background: #fff;
+ padding: 12px 20px; border-radius: var(--radius-lg);
+ border: 1px solid var(--border-color); margin-bottom: var(--space-sm); cursor: pointer;
transition: 0.2s;
}
+.attachment-item:hover { border-color: var(--primary-color); box-shadow: var(--box-shadow); }
+.attachment-item.active { background: var(--primary-lv-0); border-color: var(--primary-color); }
-.attachment-item:hover {
- border-color: var(--primary-color);
- box-shadow: var(--box-shadow);
-}
-
-.attachment-item.active {
- background: var(--primary-lv-0);
- border-color: var(--primary-color);
-}
-
-.file-icon {
- font-size: 20px;
- flex-shrink: 0;
-}
-
-.file-details {
- flex: 1;
- overflow: hidden;
-}
-
-.file-name {
- font-size: 12px;
- font-weight: 700;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.file-size {
- font-size: 10px;
- color: var(--text-sub);
-}
+.file-details { flex: 1; min-width: 0; }
+.file-name { font-size: 13px; font-weight: 700; max-width: 450px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.file-size { font-size: 11px; color: var(--text-sub); }
.btn-group {
- display: flex;
- gap: var(--space-xs);
- flex: 1;
- justify-content: flex-end;
+ display: flex; align-items: center; gap: 12px; flex-shrink: 0; justify-content: flex-end;
}
.btn-upload {
- padding: 6px 12px;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 700;
- color: #fff;
+ padding: 6px 14px; border-radius: 6px; font-size: 11px; font-weight: 700;
+ color: #fff; border: none; cursor: pointer; transition: 0.2s; height: 32px;
}
-.btn-ai {
- background: var(--ai-gradient);
-}
+.btn-ai { background: var(--ai-gradient); }
+.btn-ai:hover { filter: brightness(1.1); transform: translateY(-1px); }
-.btn-normal {
- background: var(--primary-color);
-}
+.btn-normal { background: var(--primary-color); }
+.btn-normal:hover { background: var(--primary-hover); transform: translateY(-1px); }
.ai-recommend {
- font-size: 11px;
- padding: 2px 6px;
- border-radius: 4px;
- font-weight: 700;
- margin-left: 10px;
+ font-size: 11px; padding: 6px 12px; border-radius: 6px; font-weight: 600;
+ cursor: pointer; transition: 0.2s; display: inline-block;
+ max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.ai-recommend.smart-mode { background: #eef2ff; color: #4338ca; border: 1px solid #c7d2fe; }
+.ai-recommend.manual-mode { background: #f1f5f9; color: #475569; border: 1px dashed #cbd5e1; }
+.ai-recommend:hover { transform: scale(1.02); }
+
+/* 5. Preview Area */
+.mail-preview-area {
+ width: 0; background: #f8f9fa; display: flex; flex-direction: column;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative;
+ border-left: 0 solid transparent;
}
-.ai-recommend.smart-mode {
- background: linear-gradient(135deg, #f6f8ff 0%, #f0f4ff 100%);
- color: #4a69bd;
- border: 1px solid #d1d9ff;
+.mail-preview-area.active {
+ width: 600px;
+ border-left: 1px solid var(--border-color);
+ visibility: visible;
}
-.ai-recommend.manual-mode {
- background: var(--hover-bg);
- color: var(--text-sub);
- border: 1px dashed var(--border-color);
- font-weight: 400;
+.preview-header {
+ height: 56px; padding: 0 24px; border-bottom: 1px solid var(--border-color);
+ display: flex; align-items: center; justify-content: space-between;
+ background: #fff; flex-shrink: 0;
}
-.path-display {
- cursor: pointer;
- padding: 4px 10px;
- border-radius: 6px;
- transition: all 0.2s ease;
+.preview-header h3 { font-size: 15px; font-weight: 800; color: var(--primary-color); margin: 0; }
+
+#fullViewBtn {
+ background: var(--primary-lv-0) !important;
+ color: var(--primary-color) !important;
+ border: 1px solid var(--primary-lv-1) !important;
+ font-weight: 700 !important;
+ padding: 4px 16px !important;
+ border-radius: 4px !important;
+ font-size: 11px !important;
+ transition: 0.2s !important;
+}
+#fullViewBtn:hover { background: var(--primary-lv-1) !important; }
+
+.preview-toggle-handle {
+ position: absolute; left: -20px; top: 50%; transform: translateY(-50%);
+ width: 20px; height: 60px; background: var(--primary-color); color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ border-radius: 8px 0 0 8px; font-size: 10px; cursor: pointer;
+ box-shadow: -2px 0 5px rgba(0,0,0,0.1); z-index: 100;
+}
+.preview-toggle-handle:hover { background: var(--primary-hover); }
+
+.a4-container {
+ flex: 1; padding: 30px; overflow-y: auto; background: #e9ecef;
+ display: flex; justify-content: center;
+}
+
+.a4-container iframe, .a4-container .preview-placeholder {
+ width: 100%; height: 100%; background: #fff;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-radius: 4px;
+}
+
+.preview-placeholder {
+ display: flex; align-items: center; justify-content: center;
+ text-align: center; color: var(--text-sub); font-size: 13px; line-height: 1.6;
+}
+
+.mail-preview-area.active > * { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; }
+.mail-preview-area > *:not(.preview-toggle-handle) { opacity: 0; visibility: hidden; pointer-events: none; transition: 0.2s; }
+
+/* 6. Footer & Others */
+.address-book-footer {
+ position: absolute; bottom: 0; left: 0; width: 100%; padding: 12px 16px;
+ border-top: 1px solid var(--border-color); background: #fff; display: flex; gap: 8px; z-index: 5;
}
.file-log-area {
- display: none;
- width: 100%;
- margin-top: 10px;
- background: #1a202c;
- border-radius: 4px;
- padding: 12px;
- font-family: monospace;
- font-size: 11px;
- color: #cbd5e0;
-}
-
-.file-log-area.active {
- display: block;
-}
-
-.log-line {
- margin-bottom: 2px;
-}
-
-.log-success {
- color: #48bb78;
- font-weight: 700;
-}
-
-.log-info {
- color: #63b3ed;
-}
-
-/* Toggle Switch */
-.switch {
- position: relative;
- display: inline-block;
- width: 34px;
- height: 20px;
-}
-
-.switch input {
- opacity: 0;
- width: 0;
- height: 0;
+ display: none; width: 100%; margin-top: 10px; background: #1a202c;
+ border-radius: 4px; padding: 12px; font-family: monospace; font-size: 11px; color: #cbd5e0;
}
+.file-log-area.active { display: block; }
+.log-success { color: #48bb78; font-weight: 700; }
+.switch { position: relative; display: inline-block; width: 34px; height: 20px; }
+.switch input { opacity: 0; width: 0; height: 0; }
.slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: #ccc;
- transition: .4s;
- border-radius: 20px;
+ position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
+ background-color: #ccc; transition: .4s; border-radius: 20px;
}
-
.slider:before {
- position: absolute;
- content: "";
- height: 14px;
- width: 14px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- transition: .4s;
- border-radius: 50%;
+ position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px;
+ background-color: white; transition: .4s; border-radius: 50%;
}
+input:checked+.slider { background: var(--ai-gradient); }
+input:checked+.slider:before { transform: translateX(14px); }
-input:checked+.slider {
- background: var(--ai-gradient);
-}
-
-input:checked+.slider:before {
- transform: translateX(14px);
-}
-
-.ai-toggle-wrap {
+/* Restore Path Selector Modal Specific Styles */
+.select-group {
display: flex;
- align-items: center;
- gap: var(--space-sm);
- font-size: 12px;
- font-weight: 600;
- color: var(--text-sub);
+ flex-direction: column;
+ gap: 8px;
}
-input:checked~.ai-label {
- color: #6d3dc2;
+.select-group label {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--text-main);
+}
+
+.modal-select {
+ width: 100%;
+ height: 44px;
+ padding: 0 15px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background-color: #f9f9f9;
+ font-size: 14px;
+ color: #333;
+ outline: none;
+ transition: all 0.2s;
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L2 4h8L6 8z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 15px center;
+}
+
+.modal-select:focus {
+ border-color: var(--primary-color);
+ background-color: #fff;
+ box-shadow: 0 0 0 3px rgba(30, 81, 73, 0.1);
+}
+
+.modal-select option {
+ padding: 10px;
}
diff --git a/templates/dashboard.html b/templates/dashboard.html
index 155a087..ac3ed67 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -60,15 +60,15 @@