o2o-castad-frontend/test/templates/result.html

718 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>크롤링 결과</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #fff;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 40px 20px;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 10px;
color: #fff;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 30px;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.loading {
text-align: center;
padding: 80px;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.result-container {
display: none;
}
.result-container.active {
display: block;
}
.business-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 32px;
margin-bottom: 24px;
}
.business-name {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.business-category {
font-size: 1.1rem;
color: #a0aec0;
margin-bottom: 16px;
}
.business-address {
display: flex;
align-items: center;
gap: 8px;
color: #718096;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 24px;
}
.info-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
}
.info-card h3 {
font-size: 1.1rem;
color: #667eea;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag {
padding: 8px 16px;
background: rgba(102, 126, 234, 0.2);
border-radius: 20px;
font-size: 0.9rem;
color: #a78bfa;
}
.facility-tag {
background: rgba(72, 187, 120, 0.2);
color: #68d391;
}
.images-section {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
}
.images-section h3 {
font-size: 1.1rem;
color: #667eea;
margin-bottom: 16px;
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.image-item {
aspect-ratio: 4/3;
border-radius: 12px;
overflow: hidden;
background: rgba(0, 0, 0, 0.3);
}
.image-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.image-item:hover img {
transform: scale(1.05);
}
.report-section {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
}
.report-section h3 {
font-size: 1.1rem;
color: #667eea;
margin-bottom: 16px;
}
.report-content {
line-height: 1.8;
color: #e2e8f0;
}
.report-content h4 {
color: #a78bfa;
margin: 20px 0 10px;
font-size: 1rem;
}
.json-section {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
}
.json-section h3 {
font-size: 1.1rem;
color: #667eea;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.copy-btn {
padding: 8px 16px;
background: rgba(102, 126, 234, 0.3);
border: none;
border-radius: 8px;
color: #fff;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.3s ease;
}
.copy-btn:hover {
background: rgba(102, 126, 234, 0.5);
}
.json-content {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 20px;
overflow-x: auto;
font-family: 'Fira Code', 'Monaco', monospace;
font-size: 0.85rem;
line-height: 1.6;
color: #a0aec0;
max-height: 400px;
overflow-y: auto;
}
.log-section {
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
}
.log-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.log-title {
font-size: 0.9rem;
color: #a0aec0;
display: flex;
align-items: center;
gap: 8px;
}
.log-toggle {
background: none;
border: none;
color: #667eea;
cursor: pointer;
font-size: 0.85rem;
}
.log-content {
background: rgba(0, 0, 0, 0.4);
border-radius: 8px;
padding: 12px;
font-family: 'Fira Code', monospace;
font-size: 0.8rem;
color: #68d391;
max-height: 150px;
overflow-y: auto;
}
.log-line {
margin-bottom: 4px;
padding: 2px 0;
}
.log-line.info { color: #68d391; }
.log-line.warn { color: #fbd38d; }
.log-line.error { color: #fc8181; }
.detail-info-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
}
.detail-info-card h3 {
font-size: 1.1rem;
color: #667eea;
margin-bottom: 16px;
}
.detail-row {
display: flex;
padding: 10px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
width: 140px;
color: #a0aec0;
flex-shrink: 0;
}
.detail-value {
color: #e2e8f0;
flex: 1;
}
.detail-value a {
color: #667eea;
text-decoration: none;
}
.detail-value a:hover {
text-decoration: underline;
}
.tab-container {
margin-bottom: 24px;
}
.tab-buttons {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.tab-btn {
padding: 10px 20px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 8px;
color: #a0aec0;
cursor: pointer;
transition: all 0.3s ease;
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.error-container {
text-align: center;
padding: 60px;
display: none;
}
.error-container.active {
display: block;
}
.error-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.error-message {
font-size: 1.2rem;
color: #fca5a5;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<button class="back-btn" onclick="goBack()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"/>
</svg>
검색으로 돌아가기
</button>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>상세 정보를 불러오는 중...</p>
</div>
<div class="error-container" id="errorContainer">
<div class="error-icon">⚠️</div>
<p class="error-message" id="errorMessage"></p>
<button class="back-btn" onclick="goBack()">다시 검색하기</button>
</div>
<div class="result-container" id="resultContainer">
<!-- 로그 섹션 -->
<div class="log-section" id="logSection" style="display: none;">
<div class="log-header">
<span class="log-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
API 호출 로그
</span>
<button class="log-toggle" onclick="toggleLog()">접기/펼치기</button>
</div>
<div class="log-content" id="logContent"></div>
</div>
<!-- Business Header -->
<div class="business-header">
<h1 class="business-name" id="businessName"></h1>
<p class="business-category" id="businessCategory"></p>
<p class="business-address">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
<span id="businessAddress"></span>
</p>
</div>
<!-- 원본 상세 정보 -->
<div class="detail-info-card">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle; margin-right: 8px;">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
원본 상세 정보
</h3>
<div id="rawDetailInfo"></div>
</div>
<!-- Info Grid -->
<div class="info-grid">
<div class="info-card">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
<line x1="7" y1="7" x2="7.01" y2="7"/>
</svg>
추천 타겟 키워드
</h3>
<div class="tag-list" id="tagList"></div>
</div>
<div class="info-card">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<line x1="9" y1="9" x2="15" y2="9"/>
<line x1="9" y1="15" x2="15" y2="15"/>
</svg>
시설 및 서비스
</h3>
<div class="tag-list" id="facilityList"></div>
</div>
</div>
<!-- Images Section -->
<div class="images-section" id="imagesSection">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle; margin-right: 8px;">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21,15 16,10 5,21"/>
</svg>
수집된 이미지 (<span id="imageCount">0</span>장)
</h3>
<div class="images-grid" id="imagesGrid"></div>
</div>
<!-- Report Section -->
<div class="report-section">
<h3>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle; margin-right: 8px;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
마케팅 분석 리포트
</h3>
<div class="report-content" id="reportContent"></div>
</div>
<!-- JSON Section -->
<div class="json-section">
<h3>
CrawlingResponse JSON
<button class="copy-btn" onclick="copyJson()">복사</button>
</h3>
<pre class="json-content" id="jsonContent"></pre>
</div>
</div>
</div>
<script>
let crawlingData = null;
let rawDetailData = null;
let logVisible = true;
function goBack() {
window.location.href = '/';
}
function toggleLog() {
const logContent = document.getElementById('logContent');
logVisible = !logVisible;
logContent.style.display = logVisible ? 'block' : 'none';
}
function displayLogs(logs) {
const logSection = document.getElementById('logSection');
const logContent = document.getElementById('logContent');
if (!logs || logs.length === 0) {
logSection.style.display = 'none';
return;
}
let html = '';
logs.forEach(log => {
let className = 'info';
if (log.includes('error') || log.includes('Error') || log.includes('실패')) {
className = 'error';
} else if (log.includes('시도') || log.includes('찾지 못함')) {
className = 'warn';
}
html += `<div class="log-line ${className}">${log}</div>`;
});
logContent.innerHTML = html;
logSection.style.display = 'block';
}
function displayRawDetail(detail) {
const container = document.getElementById('rawDetailInfo');
if (!detail) {
container.innerHTML = '<p style="color: #718096;">상세 정보 없음</p>';
return;
}
const rows = [
{ label: 'place_id', value: detail.place_id },
{ label: '업체명', value: detail.name },
{ label: '카테고리', value: detail.category },
{ label: '지번 주소', value: detail.address },
{ label: '도로명 주소', value: detail.road_address },
{ label: '전화번호', value: detail.phone },
{ label: '설명', value: detail.description },
{ label: '영업시간', value: detail.business_hours },
{ label: '홈페이지', value: detail.homepage, isLink: true },
{ label: '키워드', value: detail.keywords ? detail.keywords.join(', ') : null },
{ label: '시설', value: detail.facilities ? detail.facilities.join(', ') : null },
{ label: '이미지 수', value: detail.images ? detail.images.length + '개' : '0개' },
];
let html = '';
rows.forEach(row => {
const value = row.value || '-';
let displayValue = value;
if (row.isLink && row.value) {
displayValue = `<a href="${row.value}" target="_blank">${row.value}</a>`;
}
html += `
<div class="detail-row">
<span class="detail-label">${row.label}</span>
<span class="detail-value">${displayValue}</span>
</div>
`;
});
container.innerHTML = html;
}
async function loadDetail() {
const urlParams = new URLSearchParams(window.location.search);
const placeId = urlParams.get('place_id');
const title = decodeURIComponent(urlParams.get('title') || '');
if (!placeId) {
showError('place_id가 없습니다.');
return;
}
try {
const response = await fetch('/api/detail', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ place_id: placeId }),
});
const data = await response.json();
// 로그 표시
displayLogs(data.logs);
if (data.error) {
showError(data.error);
return;
}
crawlingData = data.crawling_response;
rawDetailData = data.detail;
// 원본 상세 정보 표시
displayRawDetail(data.detail);
// 크롤링 응답 표시
displayResult(data.crawling_response);
} catch (error) {
showError('상세 정보를 불러오는 중 오류가 발생했습니다: ' + error.message);
}
}
function showError(message) {
document.getElementById('loading').style.display = 'none';
document.getElementById('errorMessage').textContent = message;
document.getElementById('errorContainer').classList.add('active');
}
function displayResult(data) {
document.getElementById('loading').style.display = 'none';
document.getElementById('resultContainer').classList.add('active');
// Business Info
document.getElementById('businessName').textContent = data.processed_info.customer_name;
document.getElementById('businessCategory').textContent = data.marketing_analysis.facilities.join(' · ') || '정보 없음';
document.getElementById('businessAddress').textContent = data.processed_info.detail_region_info;
// Tags
const tagList = document.getElementById('tagList');
tagList.innerHTML = data.marketing_analysis.tags.map(tag =>
`<span class="tag">${tag}</span>`
).join('');
// Facilities
const facilityList = document.getElementById('facilityList');
facilityList.innerHTML = data.marketing_analysis.facilities.map(facility =>
`<span class="tag facility-tag">${facility}</span>`
).join('');
// Images
const imageCount = data.image_count || 0;
document.getElementById('imageCount').textContent = imageCount;
if (imageCount > 0) {
const imagesGrid = document.getElementById('imagesGrid');
imagesGrid.innerHTML = data.image_list.map(url =>
`<div class="image-item">
<img src="${url}" alt="업체 이미지" loading="lazy" onerror="this.src='https://via.placeholder.com/400x300?text=Image+Not+Found'">
</div>`
).join('');
} else {
document.getElementById('imagesSection').style.display = 'none';
}
// Report
const reportContent = document.getElementById('reportContent');
const report = data.marketing_analysis.report;
reportContent.innerHTML = report
.replace(/## (.*)/g, '<h4>$1</h4>')
.replace(/\n/g, '<br>');
// JSON
document.getElementById('jsonContent').textContent = JSON.stringify(data, null, 2);
}
function copyJson() {
if (crawlingData) {
navigator.clipboard.writeText(JSON.stringify(crawlingData, null, 2))
.then(() => {
alert('JSON이 클립보드에 복사되었습니다.');
})
.catch(err => {
console.error('복사 실패:', err);
});
}
}
// 페이지 로드 시 상세 정보 불러오기
window.onload = loadDetail;
</script>
</body>
</html>