ev-cook2 / app.py
seawolf2357's picture
Update app.py
1828d51 verified
# app.py - Hugging Face Spaces ์ „์šฉ (์ˆ˜์ •๋œ ๋ฒ„์ „)
import gradio as gr
import sqlite3
import json
import uuid
from datetime import datetime
import pandas as pd
import os
import threading
from collections import deque
# ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ €์žฅ์†Œ (SQLite ๋Œ€์‹  ์‚ฌ์šฉ)
tracking_data = deque(maxlen=10000) # ์ตœ๋Œ€ 10,000๊ฐœ ์ €์žฅ
visitors = {}
stats = {
"total_events": 0,
"unique_devices": set(),
"event_types": {},
"last_update": None
}
# API ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ ํ•จ์ˆ˜
def process_tracking_data(data_json):
"""์ถ”์  ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ"""
try:
data = json.loads(data_json) if isinstance(data_json, str) else data_json
# ๋””๋ฐ”์ด์Šค ์ •๋ณด ์—…๋ฐ์ดํŠธ
device_id = data.get('deviceId', 'unknown')
if device_id not in visitors:
visitors[device_id] = {
'first_seen': datetime.now().isoformat(),
'last_seen': datetime.now().isoformat(),
'info': data
}
else:
visitors[device_id]['last_seen'] = datetime.now().isoformat()
# ์ถ”์  ๋ฐ์ดํ„ฐ ์ €์žฅ
data['serverTimestamp'] = datetime.now().isoformat()
tracking_data.append(data)
# ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
stats["total_events"] += 1
stats["unique_devices"].add(device_id)
event_type = data.get("eventType", "pageview")
stats["event_types"][event_type] = stats["event_types"].get(event_type, 0) + 1
stats["last_update"] = datetime.now().isoformat()
return {
"status": "success",
"message": "Data tracked successfully",
"trackingId": stats["total_events"]
}
except Exception as e:
return {"status": "error", "message": str(e)}
# ์ถ”์  ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ
def generate_tracking_script(site_id, server_url):
"""์™ธ๋ถ€ ์‚ฌ์ดํŠธ์— ์‚ฝ์ž…ํ•  ์ถ”์  ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ"""
script = f"""
<!-- Visitor Tracking Script -->
<script>
(function() {{
// ์ถ”์  ์„œ๋ฒ„ URL
const TRACKING_SERVER = '{server_url}';
const SITE_ID = '{site_id}';
// ๋””๋ฐ”์ด์Šค ID ์ƒ์„ฑ/์กฐํšŒ
function getDeviceId() {{
let deviceId = localStorage.getItem('_tracker_device_id');
if (!deviceId) {{
// ํ•˜๋“œ์›จ์–ด ๊ธฐ๋ฐ˜ ID ์ƒ์„ฑ
const components = [
screen.width + 'x' + screen.height,
navigator.hardwareConcurrency || 0,
navigator.deviceMemory || 0,
navigator.platform,
navigator.language,
new Date().getTimezoneOffset()
];
// ๊ฐ„๋‹จํ•œ ํ•ด์‹œ ํ•จ์ˆ˜
let hash = 0;
const str = components.join('|');
for (let i = 0; i < str.length; i++) {{
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}}
deviceId = 'DEV_' + Math.abs(hash).toString(36);
localStorage.setItem('_tracker_device_id', deviceId);
}}
return deviceId;
}}
// WebGL ์ •๋ณด ์ˆ˜์ง‘
function getWebGLInfo() {{
try {{
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return 'Not supported';
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {{
return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
}}
return 'Unknown';
}} catch(e) {{
return 'Error';
}}
}}
// Gradio ์—”๋“œํฌ์ธํŠธ๋กœ ๋ฐ์ดํ„ฐ ์ „์†ก (์ˆ˜์ •๋จ)
async function collectAndSend(eventType = 'pageview', eventData = {{}}) {{
const data = {{
siteId: SITE_ID,
deviceId: getDeviceId(),
eventType: eventType,
eventData: eventData,
pageUrl: window.location.href,
pageTitle: document.title,
referrer: document.referrer,
userAgent: navigator.userAgent,
screenResolution: screen.width + 'x' + screen.height,
platform: navigator.platform,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
cpuCores: navigator.hardwareConcurrency || 0,
deviceMemory: navigator.deviceMemory || 0,
gpuInfo: getWebGLInfo(),
timestamp: new Date().toISOString()
}};
try {{
// Gradio API ์—”๋“œํฌ์ธํŠธ ์‚ฌ์šฉ
const response = await fetch(TRACKING_SERVER + '/run/predict', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify({{
data: [JSON.stringify(data)],
fn_index: 0 // process_tracking API ํ•จ์ˆ˜์˜ ์ธ๋ฑ์Šค
}})
}});
if (response.ok) {{
console.log('[Tracker] Event tracked:', eventType);
}} else {{
console.error('[Tracker] Server error:', response.status);
}}
}} catch(e) {{
console.error('[Tracker] Network error:', e);
}}
}}
// ์ „์—ญ ์ถ”์  ํ•จ์ˆ˜
window._tracker = {{
trackEvent: function(eventName, eventData) {{
collectAndSend('custom', {{ name: eventName, data: eventData }});
}}
}};
// ํŽ˜์ด์ง€ ๋กœ๋“œ์‹œ ์ถ”์ 
if (document.readyState === 'loading') {{
document.addEventListener('DOMContentLoaded', () => collectAndSend());
}} else {{
collectAndSend();
}}
// SPA ์ง€์› - URL ๋ณ€๊ฒฝ ๊ฐ์ง€
let lastUrl = location.href;
new MutationObserver(() => {{
const url = location.href;
if (url !== lastUrl) {{
lastUrl = url;
collectAndSend('navigation', {{ from: lastUrl, to: url }});
}}
}}).observe(document, {{subtree: true, childList: true}});
}})();
</script>
<!-- End Visitor Tracking Script -->
"""
return script.strip()
# Gradio ์ธํ„ฐํŽ˜์ด์Šค ํ•จ์ˆ˜๋“ค
def create_site_id(server_url):
"""์ƒˆ๋กœ์šด ์ถ”์  ์‚ฌ์ดํŠธ ID ์ƒ์„ฑ"""
if not server_url:
server_url = "https://your-space-name.hf.space"
site_id = str(uuid.uuid4())[:8]
return site_id, generate_tracking_script(site_id, server_url)
def view_statistics():
"""ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ"""
unique_devices = len(stats["unique_devices"])
total_events = stats["total_events"]
# ์˜ค๋Š˜ ๋ฐฉ๋ฌธ์ž ๊ณ„์‚ฐ
today = datetime.now().date()
today_devices = set()
for data in tracking_data:
try:
timestamp = datetime.fromisoformat(data.get('timestamp', '').replace('Z', '+00:00'))
if timestamp.date() == today:
today_devices.add(data.get('deviceId'))
except:
pass
summary = f"""
### ๐Ÿ“Š ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„
- **์ด ๋ฐฉ๋ฌธ์ž**: {unique_devices}๋ช…
- **์˜ค๋Š˜ ๋ฐฉ๋ฌธ์ž**: {len(today_devices)}๋ช…
- **์ด ์ด๋ฒคํŠธ**: {total_events}ํšŒ
- **๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ**: {stats.get('last_update', 'N/A')}
### ๐Ÿ“ˆ ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ํ†ต๊ณ„
"""
for event_type, count in stats["event_types"].items():
summary += f"\n- **{event_type}**: {count}ํšŒ"
summary += "\n\n### ๐Ÿ“ ์ตœ๊ทผ ์ด๋ฒคํŠธ (์ตœ๋Œ€ 10๊ฐœ)"
recent_events = list(tracking_data)[-10:]
for event in reversed(recent_events):
summary += f"\n- {event.get('timestamp', 'N/A')} | {event.get('eventType', 'N/A')} | {event.get('deviceId', 'N/A')} | {event.get('pageUrl', 'N/A')[:50]}..."
return summary
def get_recent_data():
"""์ตœ๊ทผ ๋ฐ์ดํ„ฐ๋ฅผ DataFrame์œผ๋กœ ๋ฐ˜ํ™˜"""
if not tracking_data:
return pd.DataFrame()
recent = list(tracking_data)[-100:] # ์ตœ๊ทผ 100๊ฐœ
df_data = []
for event in recent:
df_data.append({
'Timestamp': event.get('timestamp', ''),
'Device ID': event.get('deviceId', ''),
'Event Type': event.get('eventType', ''),
'Page URL': event.get('pageUrl', ''),
'Platform': event.get('platform', ''),
'Language': event.get('language', '')
})
return pd.DataFrame(df_data)
def test_tracking():
"""๋กœ์ปฌ ์ถ”์  ํ…Œ์ŠคํŠธ"""
test_data = {
"siteId": "test",
"deviceId": f"TEST_{str(uuid.uuid4())[:8]}",
"eventType": "test",
"pageUrl": "https://test.example.com",
"timestamp": datetime.now().isoformat()
}
result = process_tracking_data(test_data)
return f"ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ:\n{json.dumps(result, indent=2)}\n\nํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ:\n{json.dumps(test_data, indent=2)}"
# CSS ์Šคํƒ€์ผ
custom_css = """
.gradio-container {
font-family: 'Arial', sans-serif;
}
.tracking-script {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
}
"""
# Gradio UI
with gr.Blocks(title="๋ฐฉ๋ฌธ์ž ์ถ”์  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ", css=custom_css, theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# ๐Ÿ” ๋ฐฉ๋ฌธ์ž ์ถ”์  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
### ์™ธ๋ถ€ ์›น์‚ฌ์ดํŠธ์— ์ถ”์  ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜์—ฌ ๋ฐฉ๋ฌธ์ž๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.
> โš ๏ธ **์ฃผ์˜**: ์ด ๋ฒ„์ „์€ ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.
""")
# API ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ˆจ๊ฒจ์ง„ ์ปดํฌ๋„ŒํŠธ
api_input = gr.Textbox(visible=False)
api_output = gr.JSON(visible=False)
# API ์—”๋“œํฌ์ธํŠธ (fn_index=0)
api_input.change(
fn=process_tracking_data,
inputs=api_input,
outputs=api_output,
api_name="process_tracking"
)
with gr.Tab("๐Ÿ”ง ์ถ”์  ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ"):
gr.Markdown("### ์ƒˆ๋กœ์šด ์ถ”์  ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ")
with gr.Row():
with gr.Column():
server_url_input = gr.Textbox(
label="์ถ”์  ์„œ๋ฒ„ URL",
placeholder="https://your-space-name.hf.space",
info="์ด Hugging Face Space์˜ URL์„ ์ž…๋ ฅํ•˜์„ธ์š”"
)
gr.Markdown("""
โ„น๏ธ **์„œ๋ฒ„ URL ์ฐพ๊ธฐ**:
1. ์ด Space์˜ URL์„ ํ™•์ธํ•˜์„ธ์š”
2. ํ˜•์‹: `https://username-spacename.hf.space`
3. ์˜ˆ์‹œ: `https://seawolf2357-ev-cook2.hf.space`
""")
create_btn = gr.Button("๐Ÿ”ง ์ƒˆ ์ถ”์  ์ฝ”๋“œ ์ƒ์„ฑ", variant="primary")
with gr.Row():
site_id_output = gr.Textbox(label="์‚ฌ์ดํŠธ ID", interactive=False)
script_output = gr.Code(
label="HTML์— ์‚ฝ์ž…ํ•  ์ถ”์  ์Šคํฌ๋ฆฝํŠธ",
language="html",
interactive=True,
elem_classes="tracking-script"
)
gr.Markdown("""
### ๐Ÿ“Œ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•:
1. ์œ„์—์„œ ์ด Space์˜ URL์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค
2. '์ƒˆ ์ถ”์  ์ฝ”๋“œ ์ƒ์„ฑ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค
3. ์ƒ์„ฑ๋œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค
4. ์ถ”์ ํ•˜๋ ค๋Š” ์›น์‚ฌ์ดํŠธ์˜ `</body>` ํƒœ๊ทธ ๋ฐ”๋กœ ์•ž์— ๋ถ™์—ฌ๋„ฃ์Šต๋‹ˆ๋‹ค
### ๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•:
```javascript
// ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”(F12)์—์„œ ์‹คํ–‰
window._tracker.trackEvent('test', {data: 'test'});
```
""")
create_btn.click(
fn=create_site_id,
inputs=[server_url_input],
outputs=[site_id_output, script_output]
)
with gr.Tab("๐Ÿ“Š ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ"):
gr.Markdown("### ๐Ÿ“Š ์‹ค์‹œ๊ฐ„ ๋ฐฉ๋ฌธ์ž ํ†ต๊ณ„")
with gr.Row():
refresh_btn = gr.Button("๐Ÿ”„ ํ†ต๊ณ„ ์ƒˆ๋กœ๊ณ ์นจ", variant="secondary")
test_btn = gr.Button("๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ", variant="secondary")
stats_output = gr.Markdown()
test_output = gr.Textbox(label="ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ", visible=False)
# ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ํ†ต๊ณ„ ํ‘œ์‹œ
demo.load(fn=view_statistics, outputs=stats_output)
refresh_btn.click(fn=view_statistics, outputs=stats_output)
def test_and_refresh():
test_result = test_tracking()
stats = view_statistics()
return stats, gr.update(visible=True, value=test_result)
test_btn.click(
fn=test_and_refresh,
outputs=[stats_output, test_output]
)
with gr.Tab("๐Ÿ“ ๋ฐ์ดํ„ฐ ๋ทฐ์–ด"):
gr.Markdown("### ๐Ÿ“ ์ตœ๊ทผ ์ถ”์  ๋ฐ์ดํ„ฐ")
data_refresh_btn = gr.Button("๐Ÿ”„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ")
data_output = gr.Dataframe(
headers=["Timestamp", "Device ID", "Event Type", "Page URL", "Platform", "Language"],
label="์ตœ๊ทผ ์ด๋ฒคํŠธ (์ตœ๋Œ€ 100๊ฐœ)"
)
demo.load(fn=get_recent_data, outputs=data_output)
data_refresh_btn.click(fn=get_recent_data, outputs=data_output)
gr.Markdown("""
### ๐Ÿ“ฅ ๋ฐ์ดํ„ฐ ๋‚ด๋ณด๋‚ด๊ธฐ
์œ„์˜ ํ‘œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์„ ํƒํ•˜๊ณ  ๋ณต์‚ฌํ•˜๊ฑฐ๋‚˜, CSV๋กœ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
""")
with gr.Tab("โ“ ๋„์›€๋ง"):
gr.Markdown("""
### ๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘ ๊ฐ€์ด๋“œ
1. **์ด Space์˜ URL ํ™•์ธ**
- ๋ธŒ๋ผ์šฐ์ € ์ฃผ์†Œ์ฐฝ์—์„œ URL ๋ณต์‚ฌ
- ์˜ˆ: `https://username-spacename.hf.space`
2. **์ถ”์  ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ**
- "์ถ”์  ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ" ํƒญ์œผ๋กœ ์ด๋™
- URL ์ž…๋ ฅ ํ›„ ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ
3. **์›น์‚ฌ์ดํŠธ์— ์„ค์น˜**
```html
<!-- ์—ฌ๊ธฐ์— ์ƒ์„ฑ๋œ ์Šคํฌ๋ฆฝํŠธ ๋ถ™์—ฌ๋„ฃ๊ธฐ -->
</body>
</html>
```
4. **์ถ”์  ํ™•์ธ**
- "ํ†ต๊ณ„ ๋Œ€์‹œ๋ณด๋“œ" ํƒญ์—์„œ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง
### ๐Ÿ”ง ๋ฌธ์ œ ํ•ด๊ฒฐ
**Q: ์ถ”์ ์ด ์ž‘๋™ํ•˜์ง€ ์•Š์•„์š”**
- A: ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”(F12)์—์„œ ์—๋Ÿฌ ํ™•์ธ
- A: HTTPS ์‚ฌ์ดํŠธ์ธ์ง€ ํ™•์ธ
- A: ์„œ๋ฒ„ URL์ด ์ •ํ™•ํ•œ์ง€ ํ™•์ธ
**Q: CORS ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด์š”**
- A: ์ด Space์˜ ์ •ํ™•ํ•œ URL ์‚ฌ์šฉ
- A: HTTP๊ฐ€ ์•„๋‹Œ HTTPS ์‚ฌ์šฉ
### ๐Ÿ“ž ์ง€์›
๋ฌธ์ œ๊ฐ€ ์ง€์†๋˜๋ฉด Space ํ† ๋ก  ํƒญ์— ๋ฌธ์˜ํ•˜์„ธ์š”.
""")
# ์•ฑ ์‹คํ–‰
if __name__ == "__main__":
demo.launch()
# requirements.txt ๋‚ด์šฉ:
"""
gradio==4.44.1
pandas==2.2.3
"""