523 lines
19 KiB
HTML
523 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>고장 신고</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
<style>
|
|
body { background: var(--gray1); }
|
|
.report-wrap { max-width: 480px; margin: 0 auto; padding: 20px 16px 40px; }
|
|
|
|
/* ── 충전기 정보 헤더 ── */
|
|
.charger-info {
|
|
background: var(--navy);
|
|
color: white;
|
|
border-radius: 10px;
|
|
padding: 16px 18px;
|
|
margin-bottom: 14px;
|
|
}
|
|
.charger-info h2 { font-size: 16px; margin-bottom: 8px; color: var(--accent); }
|
|
.charger-info .row {
|
|
display: flex; justify-content: space-between;
|
|
font-size: 12px; color: rgba(255,255,255,.75); margin-top: 4px;
|
|
}
|
|
|
|
/* ── 현황 섹션 ── */
|
|
.status-section {
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin-bottom: 14px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,.08);
|
|
}
|
|
.status-header {
|
|
background: #1A2B4A;
|
|
padding: 12px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.status-header-left { display: flex; align-items: center; gap: 8px; }
|
|
.status-header h3 { font-size: 14px; font-weight: 700; color: white; }
|
|
.status-badge-count {
|
|
background: var(--orange);
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
.status-toggle-icon { font-size: 12px; color: rgba(255,255,255,.5); transition: transform .2s; }
|
|
.status-toggle-icon.open { transform: rotate(180deg); }
|
|
.status-body { background: white; }
|
|
|
|
/* ── 개별 신고 현황 카드 ── */
|
|
.report-status-card {
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--gray2);
|
|
position: relative;
|
|
}
|
|
.report-status-card:last-child { border-bottom: none; }
|
|
.rsc-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
}
|
|
.rsc-num { font-size: 12px; font-weight: 700; color: var(--navy2); }
|
|
.rsc-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 3px 10px;
|
|
border-radius: 20px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
}
|
|
.rsc-badge.pending_approval { background: #FFF3CD; color: #856404; }
|
|
.rsc-badge.pending { background: #DBEAFE; color: #1565C0; }
|
|
.rsc-badge.in_progress { background: #FEF3C7; color: #B45309; }
|
|
.rsc-badge.waiting { background: #FFE4E4; color: #C0392B; }
|
|
.rsc-badge.revisit { background: #EDE9FE; color: #5B21B6; }
|
|
|
|
.rsc-issues {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.rsc-issue-tag {
|
|
background: var(--gray1);
|
|
color: var(--text2);
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--gray2);
|
|
}
|
|
.rsc-meta {
|
|
font-size: 11px;
|
|
color: var(--gray4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
}
|
|
.rsc-mechanic {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 12px;
|
|
color: var(--green);
|
|
font-weight: 600;
|
|
margin-top: 6px;
|
|
padding: 6px 10px;
|
|
background: #E8F8F2;
|
|
border-radius: 6px;
|
|
}
|
|
.rsc-progress-bar {
|
|
height: 4px;
|
|
background: var(--gray2);
|
|
border-radius: 2px;
|
|
margin-top: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.rsc-progress-fill {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
background: linear-gradient(90deg, var(--accent), var(--green));
|
|
transition: width .4s;
|
|
}
|
|
|
|
/* ── 접기/펼치기 버튼 ── */
|
|
.collapse-body { overflow: hidden; transition: max-height .3s ease; }
|
|
.collapse-body.collapsed { max-height: 0 !important; }
|
|
|
|
/* ── 신규 신고 폼 ── */
|
|
.section {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 18px;
|
|
margin-bottom: 14px;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,.06);
|
|
}
|
|
.section h3 {
|
|
font-size: 14px; font-weight: 700; color: var(--navy);
|
|
border-left: 3px solid var(--accent); padding-left: 9px; margin-bottom: 12px;
|
|
}
|
|
.issue-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
.issue-btn {
|
|
padding: 10px; border: 1px solid var(--gray3); border-radius: 7px;
|
|
background: white; cursor: pointer; font-size: 13px; text-align: center;
|
|
transition: all .15s;
|
|
}
|
|
.issue-btn.sel { background: #E3EDFF; border-color: var(--accent); font-weight: 700; color: var(--blue); }
|
|
|
|
.upload-area {
|
|
border: 2px dashed var(--gray3); border-radius: 8px; padding: 14px;
|
|
text-align: center; cursor: pointer; color: var(--gray4); font-size: 13px;
|
|
transition: border-color .15s; margin-bottom: 6px; display: block;
|
|
}
|
|
.upload-area:hover { border-color: var(--accent); }
|
|
.photo-preview { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
.photo-preview img {
|
|
width: 80px; height: 80px; object-fit: cover;
|
|
border-radius: 6px; border: 1px solid var(--gray3);
|
|
}
|
|
.photo-info { font-size: 11px; margin-top: 4px; min-height: 16px; }
|
|
|
|
/* ── 결과 화면 ── */
|
|
#resultBox {
|
|
background: var(--navy); color: white;
|
|
border-radius: 10px; padding: 24px; text-align: center; display: none;
|
|
}
|
|
#resultBox h2 { color: var(--green); font-size: 20px; margin-bottom: 10px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="report-wrap">
|
|
|
|
<!-- 충전기 정보 -->
|
|
<div id="chargerInfo" class="charger-info">
|
|
<h2>⚡ 충전기 정보 로딩 중...</h2>
|
|
</div>
|
|
|
|
<!-- ★ 현재 접수 현황 섹션 -->
|
|
<div class="status-section" id="statusSection" style="display:none">
|
|
<div class="status-header" onclick="toggleStatus()">
|
|
<div class="status-header-left">
|
|
<span>🔔</span>
|
|
<h3>현재 접수된 신고 현황</h3>
|
|
<span class="status-badge-count" id="statusCount">0</span>
|
|
</div>
|
|
<span class="status-toggle-icon open" id="statusToggleIcon">▼</span>
|
|
</div>
|
|
<div class="collapse-body" id="statusBody">
|
|
<div class="status-body" id="statusList"></div>
|
|
<div style="padding:12px 16px;background:#F4F7FB;border-top:1px solid var(--gray2);">
|
|
<p style="font-size:12px;color:var(--gray4);line-height:1.7">
|
|
📌 동일 고장이 이미 접수되어 처리 중인 경우 추가 신고는 필요 없습니다.<br>
|
|
신고가 미처리 상태이거나 다른 문제가 있다면 아래에서 새로 신고해 주세요.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 신고 없음 안내 -->
|
|
<div id="noReportNotice" style="display:none;background:#E8F8F2;border:1px solid var(--green);border-radius:10px;padding:12px 16px;margin-bottom:14px;font-size:13px;color:#00531A;">
|
|
✅ 이 충전기에 현재 접수된 신고가 없습니다. 고장이 확인되면 아래에서 신고해 주세요.
|
|
</div>
|
|
|
|
<!-- 신고 폼 -->
|
|
<div id="mainForm">
|
|
|
|
<div class="section">
|
|
<h3>📍 신고 위치</h3>
|
|
<div id="gpsStatus" class="alert alert-info">위치 정보 수집 중...</div>
|
|
<input type="hidden" id="gpsLat">
|
|
<input type="hidden" id="gpsLng">
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>🔴 문제 유형 <span style="color:var(--red);font-size:11px">* 1개 이상 선택</span></h3>
|
|
<div class="issue-grid" id="issueGrid"></div>
|
|
<div id="errorCodeWrap" style="margin-top:10px;display:none;">
|
|
<input type="text" id="errorCode" placeholder="에러 코드 입력 (예: E001)">
|
|
</div>
|
|
<div id="etcWrap" style="margin-top:10px;display:none;">
|
|
<input type="text" id="etcText" placeholder="기타 문제 내용 입력">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>🕐 문제 발생 시각</h3>
|
|
<input type="datetime-local" id="occurredAt">
|
|
<div style="font-size:11px;color:var(--gray4);margin-top:4px">언제부터 문제가 발생했나요? (선택)</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>📷 사진 첨부</h3>
|
|
<div class="form-group">
|
|
<label style="font-size:13px;font-weight:600;color:var(--navy2);margin-bottom:6px;display:block">
|
|
충전기 사진 <span style="color:var(--red)">*필수</span>
|
|
<span style="font-size:11px;color:var(--gray4);font-weight:400">(여러 장 선택 가능)</span>
|
|
</label>
|
|
<label class="upload-area" for="chargerPhoto">
|
|
📷 탭하여 촬영하거나 앨범에서 선택<br>
|
|
<span style="font-size:11px">여러 장 동시 선택 가능</span>
|
|
</label>
|
|
<input type="file" id="chargerPhoto" accept="image/*" multiple style="display:none">
|
|
<div class="photo-preview" id="chargerPreview"></div>
|
|
<div class="photo-info" id="chargerInfo2" style="color:var(--gray4)"></div>
|
|
</div>
|
|
<div class="form-group" style="margin-top:14px">
|
|
<label style="font-size:13px;font-weight:600;color:var(--navy2);margin-bottom:6px;display:block">
|
|
차량 사진 <span style="font-size:11px;color:var(--gray4);font-weight:400">(선택 · 여러 장 가능)</span>
|
|
</label>
|
|
<label class="upload-area" for="carPhoto">📷 탭하여 촬영하거나 앨범에서 선택</label>
|
|
<input type="file" id="carPhoto" accept="image/*" multiple style="display:none">
|
|
<div class="photo-preview" id="carPreview"></div>
|
|
<div class="photo-info" id="carInfo" style="color:var(--gray4)"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>📝 상세 설명 (선택)</h3>
|
|
<textarea id="detail" placeholder="문제 상황을 자세히 설명해 주세요." rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>📞 연락처 (선택)</h3>
|
|
<input type="tel" id="contact" placeholder="010-0000-0000">
|
|
<div style="margin-top:10px;display:flex;align-items:flex-start;gap:8px;">
|
|
<input type="checkbox" id="consent" style="width:auto;margin-top:2px;accent-color:var(--accent)">
|
|
<label for="consent" style="font-size:12px;color:var(--gray4);cursor:pointer">
|
|
개인정보(연락처)를 AS 처리 목적으로 수집·이용하는 것에 동의합니다.
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="formErr" class="alert alert-danger" style="display:none"></div>
|
|
<button class="btn btn-primary btn-lg" id="submitBtn">신고 접수하기</button>
|
|
</div>
|
|
|
|
<div id="resultBox">
|
|
<h2>✅ 신고 접수 완료</h2>
|
|
<p id="resultMsg"></p>
|
|
<p style="margin-top:12px;font-size:13px;color:rgba(255,255,255,.6)">빠른 시간 내에 처리하겠습니다.</p>
|
|
<button onclick="location.reload()"
|
|
style="margin-top:16px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.3);color:white;padding:8px 20px;border-radius:8px;font-size:13px;cursor:pointer;">
|
|
🔄 현황 다시 보기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/imageCompress.js"></script>
|
|
<script>
|
|
const ISSUES = [
|
|
{key:'충전불가', label:'⚡ 충전 불가'},
|
|
{key:'화면오류', label:'🖥 화면 오류'},
|
|
{key:'케이블불량',label:'🔌 케이블 불량'},
|
|
{key:'결제오류', label:'💳 결제 오류'},
|
|
{key:'외관손상', label:'🔨 외관 손상'},
|
|
{key:'에러발생', label:'⚠️ 에러 발생'},
|
|
{key:'기타', label:'📋 기타'},
|
|
];
|
|
|
|
const STATUS_ICON = {
|
|
pending_approval: '🕐',
|
|
pending: '📋',
|
|
in_progress: '🔧',
|
|
waiting: '⏳',
|
|
revisit: '🔄',
|
|
};
|
|
|
|
const PROGRESS_PCT = {
|
|
pending_approval: 10,
|
|
pending: 25,
|
|
in_progress: 65,
|
|
waiting: 50,
|
|
revisit: 80,
|
|
};
|
|
|
|
const selected = new Set();
|
|
const chargerId = location.pathname.split('/').pop();
|
|
let isStatusOpen = true;
|
|
|
|
// ── 충전기 정보 로드 ──
|
|
async function loadCharger() {
|
|
try {
|
|
const c = await fetch('/api/chargers/' + chargerId).then(r => r.json());
|
|
document.getElementById('chargerInfo').innerHTML = `
|
|
<h2>⚡ ${c.name}</h2>
|
|
<div class="row"><span>충전소</span><span>${c.station_name}</span></div>
|
|
<div class="row"><span>종류</span><span>${c.charger_type || '-'}</span></div>
|
|
<div class="row"><span>CPO</span><span>${c.cpo_name || '-'}</span></div>
|
|
<div class="row"><span>설치일</span><span>${c.installed_at || '-'}</span></div>
|
|
`;
|
|
} catch {
|
|
document.getElementById('chargerInfo').innerHTML =
|
|
'<h2 style="color:#ff8888">충전기 정보를 불러올 수 없습니다.</h2>';
|
|
}
|
|
}
|
|
|
|
// ── 현재 접수 현황 로드 ──
|
|
async function loadStatus() {
|
|
try {
|
|
const reports = await fetch('/api/reports/public/' + chargerId).then(r => r.json());
|
|
|
|
if (!reports.length) {
|
|
document.getElementById('noReportNotice').style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('statusSection').style.display = 'block';
|
|
document.getElementById('statusCount').textContent = reports.length + '건';
|
|
|
|
const list = document.getElementById('statusList');
|
|
list.innerHTML = reports.map(r => {
|
|
const pct = PROGRESS_PCT[r.status] || 20;
|
|
const icon = STATUS_ICON[r.status] || '📋';
|
|
const dt = r.reported_at
|
|
? new Date(r.reported_at).toLocaleString('ko-KR',
|
|
{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'})
|
|
: '';
|
|
|
|
const mechHtml = r.mechanic_name
|
|
? `<div class="rsc-mechanic">🔧 ${r.mechanic_name} 정비사가 처리 중입니다</div>`
|
|
: '';
|
|
|
|
const detailHtml = r.issue_detail
|
|
? `<div style="font-size:12px;color:var(--text2);margin-top:4px;padding:6px 8px;background:var(--gray1);border-radius:5px;">${r.issue_detail}</div>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="report-status-card">
|
|
<div class="rsc-top">
|
|
<span class="rsc-num">#${r.id} · ${dt}</span>
|
|
<span class="rsc-badge ${r.status}">${icon} ${r.status_label}</span>
|
|
</div>
|
|
<div class="rsc-issues">
|
|
${(r.issue_types || []).map(t =>
|
|
`<span class="rsc-issue-tag">${t}</span>`).join('')}
|
|
</div>
|
|
${detailHtml}
|
|
${r.photo_count > 0
|
|
? `<div style="font-size:11px;color:var(--gray4);margin-top:4px;">📷 사진 ${r.photo_count}장 첨부됨</div>`
|
|
: ''}
|
|
${mechHtml}
|
|
<div class="rsc-progress-bar">
|
|
<div class="rsc-progress-fill" style="width:${pct}%"></div>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--gray4);margin-top:4px;">
|
|
<span>접수</span><span>처리중</span><span>완료</span>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
} catch(e) {
|
|
console.warn('현황 로드 실패:', e);
|
|
}
|
|
}
|
|
|
|
// ── 현황 접기/펼치기 ──
|
|
function toggleStatus() {
|
|
isStatusOpen = !isStatusOpen;
|
|
const body = document.getElementById('statusBody');
|
|
const icon = document.getElementById('statusToggleIcon');
|
|
if (isStatusOpen) {
|
|
body.style.maxHeight = body.scrollHeight + 'px';
|
|
icon.classList.add('open');
|
|
} else {
|
|
body.style.maxHeight = '0';
|
|
icon.classList.remove('open');
|
|
}
|
|
}
|
|
|
|
// 초기 펼침 높이 설정
|
|
function initCollapseHeight() {
|
|
const body = document.getElementById('statusBody');
|
|
if (body) body.style.maxHeight = body.scrollHeight + 'px';
|
|
}
|
|
|
|
// ── GPS ──
|
|
navigator.geolocation?.getCurrentPosition(
|
|
pos => {
|
|
document.getElementById('gpsLat').value = pos.coords.latitude;
|
|
document.getElementById('gpsLng').value = pos.coords.longitude;
|
|
document.getElementById('gpsStatus').textContent =
|
|
`📍 위치 수집 완료 (${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)})`;
|
|
document.getElementById('gpsStatus').className = 'alert alert-success';
|
|
},
|
|
() => {
|
|
document.getElementById('gpsStatus').textContent = '위치 정보를 가져올 수 없습니다. (수동 신고로 진행)';
|
|
document.getElementById('gpsStatus').className = 'alert alert-warn';
|
|
}
|
|
);
|
|
|
|
// ── 문제 유형 버튼 ──
|
|
const grid = document.getElementById('issueGrid');
|
|
ISSUES.forEach(issue => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'issue-btn';
|
|
btn.textContent = issue.label;
|
|
btn.type = 'button';
|
|
btn.onclick = () => {
|
|
if (selected.has(issue.key)) { selected.delete(issue.key); btn.classList.remove('sel'); }
|
|
else { selected.add(issue.key); btn.classList.add('sel'); }
|
|
document.getElementById('errorCodeWrap').style.display =
|
|
selected.has('에러발생') ? 'block' : 'none';
|
|
document.getElementById('etcWrap').style.display =
|
|
selected.has('기타') ? 'block' : 'none';
|
|
};
|
|
grid.appendChild(btn);
|
|
});
|
|
|
|
// ── 이미지 압축 + 다중 선택 ──
|
|
ImageCompressor.setupPreview('chargerPhoto', 'chargerPreview', 'chargerInfo2');
|
|
ImageCompressor.setupPreview('carPhoto', 'carPreview', 'carInfo');
|
|
|
|
// ── 신고 제출 ──
|
|
document.getElementById('submitBtn').addEventListener('click', async () => {
|
|
const issues = [...selected];
|
|
if (!issues.length) { showErr('문제 유형을 1개 이상 선택해 주세요.'); return; }
|
|
|
|
const chargerFiles = document.getElementById('chargerPhoto').files;
|
|
if (!chargerFiles.length) { showErr('충전기 사진을 1장 이상 첨부해 주세요.'); return; }
|
|
|
|
const contact = document.getElementById('contact').value.trim();
|
|
const consent = document.getElementById('consent').checked;
|
|
if (contact && !consent) {
|
|
showErr('연락처를 입력한 경우 개인정보 수집 동의가 필요합니다.'); return;
|
|
}
|
|
|
|
document.getElementById('submitBtn').disabled = true;
|
|
document.getElementById('submitBtn').textContent = '접수 중...';
|
|
|
|
const fd = new FormData();
|
|
fd.append('charger_id', chargerId);
|
|
fd.append('issue_types', JSON.stringify(issues));
|
|
fd.append('issue_detail', document.getElementById('detail').value);
|
|
fd.append('error_code', document.getElementById('errorCode').value);
|
|
fd.append('occurred_at', document.getElementById('occurredAt').value || '');
|
|
fd.append('contact', contact);
|
|
fd.append('consent', consent);
|
|
fd.append('gps_lat', document.getElementById('gpsLat').value || '');
|
|
fd.append('gps_lng', document.getElementById('gpsLng').value || '');
|
|
Array.from(chargerFiles).forEach(f => fd.append('photos', f));
|
|
Array.from(document.getElementById('carPhoto').files).forEach(f => fd.append('photos', f));
|
|
|
|
try {
|
|
const res = await fetch('/api/reports', { method: 'POST', body: fd });
|
|
if (!res.ok) { const e = await res.json(); throw new Error(e.detail); }
|
|
const data = await res.json();
|
|
document.getElementById('mainForm').style.display = 'none';
|
|
document.getElementById('resultBox').style.display = 'block';
|
|
document.getElementById('resultMsg').textContent = `접수번호: #${data.id}`;
|
|
// 현황 새로고침
|
|
document.getElementById('statusSection').style.display = 'none';
|
|
document.getElementById('noReportNotice').style.display = 'none';
|
|
await loadStatus();
|
|
initCollapseHeight();
|
|
} catch(e) {
|
|
showErr(e.message);
|
|
document.getElementById('submitBtn').disabled = false;
|
|
document.getElementById('submitBtn').textContent = '신고 접수하기';
|
|
}
|
|
});
|
|
|
|
function showErr(msg) {
|
|
const el = document.getElementById('formErr');
|
|
el.textContent = msg; el.style.display = 'block';
|
|
el.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
// ── 초기 로드 ──
|
|
(async () => {
|
|
await Promise.all([loadCharger(), loadStatus()]);
|
|
initCollapseHeight();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|