|
""" |
|
๊ฒฐ๊ณผ ์ถ๋ ฅ ๊ด๋ จ ์ ํธ๋ฆฌํฐ ํจ์ ๋ชจ์ - ์นดํ
๊ณ ๋ฆฌ ํญ๋ชฉ ์ ๊ฑฐ |
|
- 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() |
|
|
|
|
|
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 = ''' |
|
<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">' |
|
|
|
|
|
html += '<div class="data-container">' |
|
html += '<table class="styled-table">' |
|
|
|
|
|
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>' |
|
|
|
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: |
|
|
|
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>' |
|
html += '</div>' |
|
|
|
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) |
|
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) |
|
|
|
|
|
|
|
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) |