Spaces:
Running
Running
# 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 | |
""" |