기능 추가 및 버그 수정 — 처리시간 지표, 대시보드 차트, UI 개선
## 처리시간 지표 - 업무시간 기준(09-18 평일) / 공휴일 제외 24h / 달력 기준 3가지 모드 선택 - 공휴일 DB 관리 (holidays 테이블, 수동 등록·삭제·일괄 추가) - 2026년 공휴일 등록 지원 - 설정 페이지에서 라디오 버튼으로 모드 선택 ## 대시보드 차트 - 월별 평균 처리시간 막대 차트 추가 - 월별 신고 접수 건수 누적 막대 차트 추가 - 월별 → 일별 드릴다운 (막대 클릭 시 해당 월의 일별 차트로 전환) - 일별 막대 클릭 시 처리 완료/신고 접수 상세 내역 모달 - 충전기별 누적 고장 건수 Top 10 수평 막대 차트 추가 ## 신고 목록 - # 컬럼을 DB PK 대신 현재 목록 순서(1, 2, 3…)로 표시 - 엑셀 export 접수번호도 순차번호로 변경 ## 모바일 네비게이션 버그 수정 - 모바일에서 가로 오버플로우 시 nav가 body 넓이로 늘어나 햄버거 버튼이 화면 밖으로 밀리는 문제 수정 - nav를 position:fixed + body padding-top:54px 로 변경 (전체 페이지 적용) - 충전기 관리·신고 목록 페이지 지도 컨테이너에 isolation:isolate 적용 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -221,7 +221,8 @@ body { background: var(--gray1); }
|
||||
<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)">
|
||||
<!-- populated as dropdown or text input depending on chargerErrors -->
|
||||
<div id="errorCodeInner"></div>
|
||||
</div>
|
||||
<div id="etcWrap" style="margin-top:10px;display:none;">
|
||||
<input type="text" id="etcText" placeholder="기타 문제 내용 입력">
|
||||
@@ -260,6 +261,24 @@ body { background: var(--gray1); }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>📡 신고 범위</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<label style="display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer">
|
||||
<input type="radio" name="scope" value="single" checked style="width:auto;accent-color:var(--accent)">
|
||||
<div><strong>이 충전기만</strong><div style="font-size:11px;color:var(--gray4)">현재 스캔한 충전기에만 신고</div></div>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer">
|
||||
<input type="radio" name="scope" value="station" style="width:auto;accent-color:var(--accent)">
|
||||
<div><strong>충전소 전체</strong><div style="font-size:11px;color:var(--gray4)">같은 충전소의 모든 충전기에 신고</div></div>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer">
|
||||
<input type="radio" name="scope" value="type" style="width:auto;accent-color:var(--accent)">
|
||||
<div><strong>동일 모델 전체</strong><div style="font-size:11px;color:var(--gray4)">같은 충전기 모델 전체에 신고</div></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>📝 상세 설명 (선택)</h3>
|
||||
<textarea id="detail" placeholder="문제 상황을 자세히 설명해 주세요." rows="3"></textarea>
|
||||
@@ -294,7 +313,7 @@ body { background: var(--gray1); }
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/imageCompress.js"></script>
|
||||
<script>
|
||||
const ISSUES = [
|
||||
let ISSUES = [
|
||||
{key:'충전불가', label:'⚡ 충전 불가'},
|
||||
{key:'화면오류', label:'🖥 화면 오류'},
|
||||
{key:'케이블불량',label:'🔌 케이블 불량'},
|
||||
@@ -303,6 +322,7 @@ const ISSUES = [
|
||||
{key:'에러발생', label:'⚠️ 에러 발생'},
|
||||
{key:'기타', label:'📋 기타'},
|
||||
];
|
||||
let chargerErrors = [];
|
||||
|
||||
const STATUS_ICON = {
|
||||
pending_approval: '🕐',
|
||||
@@ -327,7 +347,11 @@ let isStatusOpen = true;
|
||||
// ── 충전기 정보 로드 ──
|
||||
async function loadCharger() {
|
||||
try {
|
||||
const c = await fetch('/api/chargers/' + chargerId).then(r => r.json());
|
||||
const [c, errs] = await Promise.all([
|
||||
fetch('/api/chargers/' + chargerId).then(r => r.json()),
|
||||
fetch('/api/chargers/' + chargerId + '/errors').then(r => r.json()).catch(() => []),
|
||||
]);
|
||||
chargerErrors = errs;
|
||||
document.getElementById('chargerInfo').innerHTML = `
|
||||
<h2>⚡ ${c.name}</h2>
|
||||
<div class="row"><span>충전소</span><span>${c.station_name}</span></div>
|
||||
@@ -435,23 +459,67 @@ navigator.geolocation?.getCurrentPosition(
|
||||
}
|
||||
);
|
||||
|
||||
// ── 에러코드 UI 갱신 ──
|
||||
function updateErrorCodeUI() {
|
||||
const wrap = document.getElementById('errorCodeWrap');
|
||||
const inner = document.getElementById('errorCodeInner');
|
||||
if (!selected.has('에러발생')) { wrap.style.display = 'none'; return; }
|
||||
wrap.style.display = 'block';
|
||||
if (chargerErrors.length > 0) {
|
||||
inner.innerHTML = `
|
||||
<select id="errorCode" style="width:100%">
|
||||
<option value="">-- 에러코드 선택 --</option>
|
||||
${chargerErrors.map(e =>
|
||||
`<option value="${e.error_code}">${e.error_code} — ${e.error_name}${e.range_condition ? ' ('+e.range_condition+')' : ''}</option>`
|
||||
).join('')}
|
||||
<option value="__other__">기타 (직접 입력)</option>
|
||||
</select>
|
||||
<input type="text" id="errorCodeCustom" placeholder="에러코드 직접 입력" style="margin-top:6px;display:none">`;
|
||||
document.getElementById('errorCode').onchange = function() {
|
||||
document.getElementById('errorCodeCustom').style.display =
|
||||
this.value === '__other__' ? 'block' : 'none';
|
||||
};
|
||||
} else {
|
||||
inner.innerHTML = `<input type="text" id="errorCode" placeholder="에러 코드 입력 (예: E001)">`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 에러코드 값 가져오기 ──
|
||||
function getErrorCodeValue() {
|
||||
const sel = document.getElementById('errorCode');
|
||||
if (!sel) return '';
|
||||
if (sel.tagName === 'SELECT') {
|
||||
if (sel.value === '__other__') return document.getElementById('errorCodeCustom')?.value || '';
|
||||
return sel.value;
|
||||
}
|
||||
return sel.value;
|
||||
}
|
||||
|
||||
// ── 문제 유형 버튼 ──
|
||||
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);
|
||||
});
|
||||
function renderIssueButtons(issues) {
|
||||
const grid = document.getElementById('issueGrid');
|
||||
grid.innerHTML = '';
|
||||
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'); }
|
||||
updateErrorCodeUI();
|
||||
document.getElementById('etcWrap').style.display =
|
||||
selected.has('기타') ? 'block' : 'none';
|
||||
};
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/api/settings/issue-types')
|
||||
.then(r => r.json())
|
||||
.then(data => { if (Array.isArray(data) && data.length) { ISSUES = data; } })
|
||||
.catch(() => {})
|
||||
.finally(() => renderIssueButtons(ISSUES));
|
||||
|
||||
// ── 이미지 압축 + 다중 선택 ──
|
||||
ImageCompressor.setupPreview('chargerPhoto', 'chargerPreview', 'chargerInfo2');
|
||||
@@ -474,11 +542,14 @@ document.getElementById('submitBtn').addEventListener('click', async () => {
|
||||
document.getElementById('submitBtn').disabled = true;
|
||||
document.getElementById('submitBtn').textContent = '접수 중...';
|
||||
|
||||
const scope = document.querySelector('input[name="scope"]:checked')?.value || 'single';
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('charger_id', chargerId);
|
||||
fd.append('scope', scope);
|
||||
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('error_code', getErrorCodeValue());
|
||||
fd.append('occurred_at', document.getElementById('occurredAt').value || '');
|
||||
fd.append('contact', contact);
|
||||
fd.append('consent', consent);
|
||||
@@ -488,12 +559,13 @@ document.getElementById('submitBtn').addEventListener('click', async () => {
|
||||
Array.from(document.getElementById('carPhoto').files).forEach(f => fd.append('photos', f));
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/reports', { method: 'POST', body: fd });
|
||||
const res = await fetch('/api/reports/batch', { 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}`;
|
||||
const label = data.count > 1 ? `접수번호: #${data.primary_id} 외 ${data.count-1}건` : `접수번호: #${data.primary_id}`;
|
||||
document.getElementById('resultMsg').textContent = label;
|
||||
// 현황 새로고침
|
||||
document.getElementById('statusSection').style.display = 'none';
|
||||
document.getElementById('noReportNotice').style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user