Files
ev-charger-as/frontend/static/pages/login.html
byun 2e8751ea6c 기능 추가 및 버그 수정 — 처리시간 지표, 대시보드 차트, 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>
2026-05-31 06:52:56 +09:00

201 lines
8.3 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>로그인 — EV AS 관리</title>
<link rel="stylesheet" href="/css/style.css">
<style>
body { display:flex; align-items:center; justify-content:center; min-height:100vh; background:var(--navy); }
.login-box {
background:white; border-radius:14px; padding:40px 36px;
width:100%; max-width:380px; box-shadow:0 8px 32px rgba(0,0,0,.3);
}
.login-logo { text-align:center; margin-bottom:28px; }
.login-logo h1 { font-size:22px; font-weight:900; color:var(--navy); }
.login-logo p { font-size:12px; color:var(--gray4); margin-top:4px; }
.login-box .form-group { margin-bottom:14px; }
#err, #regErr { font-size:13px; text-align:center; min-height:18px; margin-bottom:8px; }
#err { color:var(--red); }
#regErr { color:var(--red); }
.tab-row {
display:flex; gap:0; border-bottom:2px solid var(--gray2); margin-bottom:24px;
}
.tab-row button {
flex:1; background:none; border:none; padding:9px 0; font-size:14px; font-weight:600;
color:var(--gray4); border-bottom:3px solid transparent; margin-bottom:-2px; cursor:pointer;
transition:color .15s, border-color .15s;
}
.tab-row button.active { color:var(--navy); border-bottom-color:var(--accent); }
.pane { display:none; }
.pane.active { display:block; }
.reg-notice {
background:#EFF6FF; border:1px solid #BFDBFE; border-radius:8px;
padding:10px 14px; font-size:12px; color:#1E40AF; margin-bottom:16px; line-height:1.6;
}
</style>
</head>
<body>
<div class="login-box">
<div class="login-logo">
<h1>⚡ EV AS 관리</h1>
<p>cs.byunc.com</p>
</div>
<div class="tab-row">
<button id="tabLogin" class="active" onclick="switchTab('login')">로그인</button>
<button id="tabRegister" onclick="switchTab('register')">회원가입</button>
</div>
<!-- 로그인 -->
<div class="pane active" id="paneLogin">
<div class="form-group">
<label>아이디</label>
<input type="text" id="username" placeholder="아이디 입력" autofocus>
</div>
<div class="form-group">
<label>비밀번호</label>
<input type="password" id="password" placeholder="비밀번호 입력">
</div>
<div id="err"></div>
<button class="btn btn-primary btn-lg" id="loginBtn">로그인</button>
</div>
<!-- 회원가입 -->
<div class="pane" id="paneRegister">
<div class="reg-notice">
📌 정비사 계정으로 가입됩니다.<br>
가입 후 <strong>관리자 승인</strong>이 완료되어야 로그인 가능합니다.
</div>
<div class="form-group">
<label>이름 <span style="color:var(--red)">*</span></label>
<input type="text" id="regName" placeholder="실명 입력">
</div>
<div class="form-group">
<label>아이디 <span style="color:var(--red)">*</span></label>
<input type="text" id="regUsername" placeholder="영문·숫자 조합">
</div>
<div class="form-group">
<label>비밀번호 <span style="color:var(--red)">*</span></label>
<input type="password" id="regPassword" placeholder="8자 이상 권장">
</div>
<div class="form-group">
<label>비밀번호 확인 <span style="color:var(--red)">*</span></label>
<input type="password" id="regPassword2" placeholder="비밀번호 재입력">
</div>
<div class="form-group">
<label>전화번호 <span style="font-size:11px;color:var(--gray4);font-weight:400">(선택)</span></label>
<input type="tel" id="regPhone" placeholder="예) 010-1234-5678">
</div>
<div class="form-group">
<label>회사명 <span style="font-size:11px;color:var(--gray4);font-weight:400">(선택)</span></label>
<select id="regCompany">
<option value="">-- 소속 제조사 없음 --</option>
</select>
</div>
<div id="regErr"></div>
<button class="btn btn-primary btn-lg" id="regBtn">가입 신청</button>
<div id="regOk" class="alert alert-success" style="display:none;margin-top:14px;text-align:center">
✅ 가입 신청이 완료되었습니다.<br>
<span style="font-size:12px">관리자 승인 후 로그인 가능합니다.</span>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/auth.js"></script>
<script>
function switchTab(name) {
document.getElementById('tabLogin').classList.toggle('active', name==='login');
document.getElementById('tabRegister').classList.toggle('active', name==='register');
document.getElementById('paneLogin').classList.toggle('active', name==='login');
document.getElementById('paneRegister').classList.toggle('active', name==='register');
document.getElementById('err').textContent = '';
document.getElementById('regErr').textContent = '';
}
// ── 로그인 ──
async function doLogin() {
const u = document.getElementById('username').value.trim();
const p = document.getElementById('password').value;
if (!u || !p) { document.getElementById('err').textContent = '아이디와 비밀번호를 입력하세요.'; return; }
document.getElementById('loginBtn').disabled = true;
document.getElementById('err').textContent = '';
try {
const fd = new FormData();
fd.append('username', u); fd.append('password', p);
const res = await fetch('/api/auth/login', { method:'POST', body: fd });
if (!res.ok) { const e = await res.json(); throw new Error(e.detail); }
const data = await res.json();
Auth.save(data.access_token, data.role, data.name, data.user_id);
if (data.role === 'admin') location.href = '/pages/admin/dashboard.html';
else if (data.role === 'mechanic') location.href = '/pages/mechanic/dashboard.html';
else location.href = '/pages/manufacturer/dashboard.html';
} catch(e) {
document.getElementById('err').textContent = e.message;
document.getElementById('loginBtn').disabled = false;
}
}
// ── 제조사 목록 로드 (비인증) ──
async function loadCompanies() {
try {
const list = await fetch('/api/manufacturers/public').then(r => r.json());
const sel = document.getElementById('regCompany');
list.forEach(m => {
const opt = document.createElement('option');
opt.value = m.name; opt.textContent = m.name;
sel.appendChild(opt);
});
} catch {}
}
loadCompanies();
// ── 회원가입 ──
async function doRegister() {
const name = document.getElementById('regName').value.trim();
const uname = document.getElementById('regUsername').value.trim();
const pw = document.getElementById('regPassword').value;
const pw2 = document.getElementById('regPassword2').value;
const phone = document.getElementById('regPhone').value.trim();
const company = document.getElementById('regCompany').value;
const errEl = document.getElementById('regErr');
errEl.textContent = '';
if (!name) { errEl.textContent = '이름을 입력하세요.'; return; }
if (!uname) { errEl.textContent = '아이디를 입력하세요.'; return; }
if (!pw) { errEl.textContent = '비밀번호를 입력하세요.'; return; }
if (pw !== pw2) { errEl.textContent = '비밀번호가 일치하지 않습니다.'; return; }
document.getElementById('regBtn').disabled = true;
try {
const fd = new FormData();
fd.append('username', uname);
fd.append('password', pw);
fd.append('name', name);
fd.append('phone', phone);
fd.append('company', company);
const res = await fetch('/api/auth/register', { method:'POST', body: fd });
if (!res.ok) { const e = await res.json(); throw new Error(e.detail); }
document.getElementById('regOk').style.display = 'block';
document.getElementById('regBtn').style.display = 'none';
['regName','regUsername','regPassword','regPassword2','regPhone'].forEach(id =>
document.getElementById(id).value = '');
document.getElementById('regCompany').value = '';
} catch(e) {
errEl.textContent = e.message;
document.getElementById('regBtn').disabled = false;
}
}
document.getElementById('loginBtn').addEventListener('click', doLogin);
document.getElementById('password').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
document.getElementById('regBtn').addEventListener('click', doRegister);
document.getElementById('regPassword2').addEventListener('keydown', e => { if(e.key==='Enter') doRegister(); });
</script>
</body>
</html>