p763nx9tf / export_utils.py
ssboost's picture
Upload 11 files
1271db4 verified
"""
๊ฒฐ๊ณผ ์ถœ๋ ฅ ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ๋ชจ์Œ - ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ œ๊ฑฐ
- HTML ํ…Œ์ด๋ธ” ์ƒ์„ฑ
- ์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ
"""
import pandas as pd
import tempfile
import os
import threading
import time
import logging
# ๋กœ๊น… ์„ค์ •
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ๋ฆฌ์ŠคํŠธ
_temp_files = []
def create_table_without_checkboxes(df):
"""DataFrame์„ HTML ํ…Œ์ด๋ธ”๋กœ ๋ณ€ํ™˜ - ํ‚ค์›Œ๋“œ ํด๋ฆญ ์‹œ ๋„ค์ด๋ฒ„ ์‡ผํ•‘ ์ด๋™ ๊ธฐ๋Šฅ ์ถ”๊ฐ€"""
if df.empty:
return "<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>"
# === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์—ด ์ œ๊ฑฐ ===
df_display = df.copy()
# "์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)" ๋˜๋Š” "๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ", "์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ" ์—ด์ด ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
columns_to_remove = ["์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)", "๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ", "์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ"]
for col in columns_to_remove:
if col in df_display.columns:
df_display = df_display.drop(columns=[col])
logger.info(f"ํ…Œ์ด๋ธ”์—์„œ '{col}' ์—ด ์ œ๊ฑฐ๋จ")
# HTML ํ…Œ์ด๋ธ” ์Šคํƒ€์ผ ์ •์˜ - Z-INDEX ์ˆ˜์ •
html = '''
<style>
.table-container {
position: relative;
width: 100%;
margin: 0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.header-wrap {
position: sticky;
top: 0;
z-index: 100; /* z-index ์ฆ๊ฐ€ */
background-color: #009879;
}
.styled-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin: 0;
padding: 0;
font-size: 14px;
}
.styled-table th,
.styled-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #dddddd;
overflow: hidden;
text-overflow: ellipsis;
}
/* ๊ธด ํ…์ŠคํŠธ๊ฐ€ ์…€์—์„œ ์ค„๋ฐ”๊ฟˆ๋˜๋„๋ก ์ˆ˜์ • */
.styled-table td.col-rank {
white-space: normal;
word-break: break-word;
line-height: 1.3;
}
/* ๊ทธ ์™ธ ์—ด์€ ํ•œ ์ค„๋กœ ํ‘œ์‹œ */
.styled-table td.col-seq,
.styled-table td.col-keyword,
.styled-table td.col-pc,
.styled-table td.col-mobile,
.styled-table td.col-total,
.styled-table td.col-range,
.styled-table td.col-count {
white-space: nowrap;
}
.styled-table th {
background-color: #009879;
color: white;
font-weight: bold;
position: sticky;
top: 0;
white-space: nowrap;
z-index: 50; /* ํ—ค๋” z-index ์ฆ๊ฐ€ */
}
.styled-table tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
.styled-table tbody tr:hover {
background-color: #f0f0f0;
}
.styled-table tbody tr:last-of-type {
border-bottom: 2px solid #009879;
}
/* ๋ฐ์ดํ„ฐ ์…€ z-index ์„ค์ • */
.styled-table tbody td {
position: relative;
z-index: 1; /* ๋ฐ์ดํ„ฐ ์…€์€ ๋‚ฎ์€ z-index */
}
.data-container {
max-height: 600px;
overflow-y: auto;
position: relative; /* position ์ถ”๊ฐ€ */
}
/* ์Šคํฌ๋กค๋ฐ” ์Šคํƒ€์ผ */
.data-container::-webkit-scrollbar {
width: 10px;
}
.data-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.data-container::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
}
.data-container::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* ํ‚ค์›Œ๋“œ ๋งํฌ ์Šคํƒ€์ผ - ์ƒˆ๋กœ ์ถ”๊ฐ€ */
.keyword-link {
color: #2c5aa0;
text-decoration: none;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
padding: 2px 4px;
border-radius: 3px;
position: relative;
z-index: 5; /* ๋งํฌ z-index ์„ค์ • */
}
.keyword-link:hover {
color: #ffffff;
background-color: #2c5aa0;
text-decoration: none;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(44, 90, 160, 0.3);
}
.keyword-link:active {
transform: translateY(0px);
}
/* ํ‚ค์›Œ๋“œ ์…€ ํŠน๋ณ„ ์Šคํƒ€์ผ */
.col-keyword {
position: relative;
}
.keyword-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 6px 10px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000; /* ํˆดํŒ์€ ๊ฐ€์žฅ ๋†’์€ z-index */
pointer-events: none;
margin-bottom: 5px;
}
.keyword-tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 4px solid transparent;
border-top-color: #333;
}
.keyword-link:hover .keyword-tooltip {
opacity: 1;
visibility: visible;
}
/* === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์—ด ๋„ˆ๋น„ ์ •์˜ - ์นดํ…Œ๊ณ ๋ฆฌ ์—ด ์ œ๊ฑฐ ํ›„ ์กฐ์ • === */
.col-seq { width: 8%; }
.col-keyword { width: 25%; }
.col-pc { width: 12%; }
.col-mobile { width: 12%; }
.col-total { width: 12%; }
.col-range { width: 12%; }
.col-rank { width: 15%; }
.col-count { width: 10%; }
.truncated-text {
position: relative;
cursor: pointer;
z-index: 2; /* ํ…์ŠคํŠธ z-index ์„ค์ • */
}
.truncated-text:hover::after {
content: attr(data-full-text);
position: absolute;
left: 0;
top: 100%;
z-index: 99;
min-width: 200px;
max-width: 400px;
padding: 8px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
white-space: normal;
}
/* ํ‚ค์›Œ๋“œ ํƒœ๊ทธ ์Šคํƒ€์ผ */
.keyword-tag-container {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.keyword-tag {
display: inline-block;
background-color: #009879;
color: white;
padding: 5px 10px;
margin: 5px;
border-radius: 15px;
font-size: 12px;
}
.category-tag {
display: inline-block;
background-color: #2c7fb8;
color: white;
padding: 5px 10px;
margin: 5px;
border-radius: 15px;
font-size: 12px;
}
/* ๋ถ„์„ ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ” ์Šคํƒ€์ผ */
.analysis-result {
margin-top: 30px;
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
background-color: #f9f9f9;
}
.result-header {
font-weight: bold;
margin-bottom: 10px;
color: #009879;
}
.match-item {
margin: 5px 0;
padding: 5px;
border-bottom: 1px solid #eee;
}
.match-keyword {
font-weight: bold;
color: #2c7fb8;
}
.match-count {
display: inline-block;
background-color: #009879;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
margin-left: 10px;
}
</style>
'''
# === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์—ด ์ด๋ฆ„๊ณผ ํด๋ž˜์Šค ๋งคํ•‘ - ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์ œ๊ฑฐ ===
col_mapping = {
"์ˆœ๋ฒˆ": "col-seq",
"์กฐํ•ฉ ํ‚ค์›Œ๋“œ": "col-keyword",
"PC๊ฒ€์ƒ‰๋Ÿ‰": "col-pc",
"๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰": "col-mobile",
"์ด๊ฒ€์ƒ‰๋Ÿ‰": "col-total",
"๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„": "col-range",
"ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„": "col-rank",
"ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜": "col-count"
# ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ๋งคํ•‘ ์ œ๊ฑฐ๋จ
}
# ํ…Œ์ด๋ธ” ์ปจํ…Œ์ด๋„ˆ ์‹œ์ž‘
html += '<div class="table-container">'
# ๋‹จ์ผ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝ (ํ—ค๋”๋Š” position: sticky๋กœ ๊ณ ์ •)
html += '<div class="data-container">'
html += '<table class="styled-table">'
# colgroup์œผ๋กœ ์—ด ๋„ˆ๋น„ ์ •์˜
html += '<colgroup>'
html += f'<col class="{col_mapping["์ˆœ๋ฒˆ"]}">'
for col in df_display.columns:
col_class = col_mapping.get(col, "")
html += f'<col class="{col_class}">'
html += '</colgroup>'
# ํ…Œ์ด๋ธ” ํ—ค๋”
html += '<thead>'
html += '<tr>'
html += f'<th class="{col_mapping["์ˆœ๋ฒˆ"]}">์ˆœ๋ฒˆ</th>'
for col in df_display.columns:
col_class = col_mapping.get(col, "")
html += f'<th class="{col_class}">{col}</th>'
html += '</tr>'
html += '</thead>'
# ํ…Œ์ด๋ธ” ๋ณธ๋ฌธ
html += '<tbody>'
for idx, row in df_display.iterrows():
html += '<tr>'
# ์ˆœ๋ฒˆ ํ‘œ์‹œ - 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์ˆœ์ฐจ์  ๋ฒˆํ˜ธ
html += f'<td class="{col_mapping["์ˆœ๋ฒˆ"]}">{idx + 1}</td>'
# ๋ฐ์ดํ„ฐ ์…€ ์ถ”๊ฐ€
for col in df_display.columns:
col_class = col_mapping.get(col, "")
value = str(row[col])
if col == "ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„":
# ๊ธด ํ…์ŠคํŠธ์˜ ์…€์€ ๊ทธ๋Œ€๋กœ ํ‘œ์‹œ (์ค„๋ฐ”๊ฟˆ ํ—ˆ์šฉ)
html += f'<td class="{col_class}">{value}</td>'
elif len(value) > 30:
# ๋‹ค๋ฅธ ๊ธด ํ…์ŠคํŠธ๋Š” hover๋กœ ์ „์ฒด ํ‘œ์‹œ
html += f'<td class="{col_class}"><div class="truncated-text" data-full-text="{value}">{value[:30]}...</div></td>'
else:
# ์ผ๋ฐ˜ ํ…์ŠคํŠธ
html += f'<td class="{col_class}">{value}</td>'
html += '</tr>'
html += '</tbody>'
html += '</table>'
html += '</div>' # data-container ๋‹ซ๊ธฐ
html += '</div>' # table-container ๋‹ซ๊ธฐ
return html
def cleanup_temp_files(delay=300):
"""์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ ํ•จ์ˆ˜"""
global _temp_files
def cleanup():
time.sleep(delay) # ์ง€์ •๋œ ์‹œ๊ฐ„ ๋Œ€๊ธฐ
temp_files_to_remove = _temp_files.copy()
_temp_files = []
for file_path in temp_files_to_remove:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
except Exception as e:
logger.error(f"ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
# ์ƒˆ ์Šค๋ ˆ๋“œ ์‹œ์ž‘
threading.Thread(target=cleanup, daemon=True).start()
def download_keywords(df, auto_cleanup=True, cleanup_delay=300):
"""ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์—‘์…€ ํŒŒ์ผ๋กœ ๋‹ค์šด๋กœ๋“œ - ์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ ์ œ๊ฑฐ"""
global _temp_files
if df is None or df.empty:
return None
# ์ž„์‹œ ํŒŒ์ผ๋กœ ์ €์žฅ
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx')
temp_file.close()
filename = temp_file.name
# ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€
_temp_files.append(filename)
# === ์ˆ˜์ •๋œ ๋ถ€๋ถ„: ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์—ด ์ œ๊ฑฐ ===
df_export = df.copy()
# ์นดํ…Œ๊ณ ๋ฆฌ ๊ด€๋ จ ์—ด๋“ค ์ œ๊ฑฐ
columns_to_remove = ["์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ(์ƒ์œ„100์œ„)", "๊ด€๋ จ ์นดํ…Œ๊ณ ๋ฆฌ", "์นดํ…Œ๊ณ ๋ฆฌ ํ•ญ๋ชฉ"]
for col in columns_to_remove:
if col in df_export.columns:
df_export = df_export.drop(columns=[col])
logger.info(f"์—‘์…€ ๋‚ด๋ณด๋‚ด๊ธฐ์—์„œ '{col}' ์—ด ์ œ๊ฑฐ๋จ")
# ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์—‘์…€ ํŒŒ์ผ๋กœ ์ €์žฅ
with pd.ExcelWriter(filename, engine='xlsxwriter') as writer:
# ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ์‹œํŠธ
df_export.to_excel(writer, sheet_name='ํ‚ค์›Œ๋“œ ๋ชฉ๋ก', index=False)
# ์—ด ๋„ˆ๋น„ ์กฐ์ • - ์นดํ…Œ๊ณ ๋ฆฌ ์—ด ์ œ๊ฑฐ ํ›„ ์กฐ์ •
worksheet = writer.sheets['ํ‚ค์›Œ๋“œ ๋ชฉ๋ก']
worksheet.set_column('A:A', 20) # ์กฐํ•ฉ ํ‚ค์›Œ๋“œ ์—ด
worksheet.set_column('B:B', 12) # PC๊ฒ€์ƒ‰๋Ÿ‰ ์—ด
worksheet.set_column('C:C', 12) # ๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰ ์—ด
worksheet.set_column('D:D', 12) # ์ด๊ฒ€์ƒ‰๋Ÿ‰ ์—ด
worksheet.set_column('E:E', 12) # ๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„ ์—ด
worksheet.set_column('F:F', 20) # ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„ ์—ด
worksheet.set_column('G:G', 12) # ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜ ์—ด
# ์นดํ…Œ๊ณ ๋ฆฌ ์—ด๋“ค ์ œ๊ฑฐ๋กœ H, I ์—ด ์„ค์ • ์ œ๊ฑฐ๋จ
# ํ—ค๋” ํ˜•์‹ ์„ค์ •
header_format = writer.book.add_format({
'bold': True,
'bg_color': '#009879',
'color': 'white',
'border': 1
})
# ํ—ค๋”์— ํ˜•์‹ ์ ์šฉ
for col_num, value in enumerate(df_export.columns.values):
worksheet.write(0, col_num, value, header_format)
logger.info(f"์—‘์…€ ํŒŒ์ผ ์ƒ์„ฑ: {filename}")
# ํŒŒ์ผ ์ž๋™ ์ •๋ฆฌ ์˜ต์…˜
if auto_cleanup:
# ๋ณ„๋„ ์ •๋ฆฌ ์ž‘์—… ์š”์ฒญ ์—†์ด ์ถ”์  ๋ชฉ๋ก์— ์ถ”๊ฐ€๋งŒ ํ•˜์—ฌ ์ผ๊ด„ ์ฒ˜๋ฆฌ
pass
return filename
def register_cleanup_handlers():
"""์•ฑ ์ข…๋ฃŒ ์‹œ ์ •๋ฆฌ๋ฅผ ์œ„ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก"""
import atexit
def cleanup_all_temp_files():
global _temp_files
for file_path in _temp_files:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"์ข…๋ฃŒ ์‹œ ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
except Exception as e:
logger.error(f"ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
_temp_files = []
# ์•ฑ ์ข…๋ฃŒ ์‹œ ์‹คํ–‰๋  ํ•จ์ˆ˜ ๋“ฑ๋ก
atexit.register(cleanup_all_temp_files)