- 이미지 압축: 삼성/네이버 브라우저 호환, URL.createObjectURL 방식으로 메모리 절감, 대용량 PNG/HEIC 처리, blob 유효성 검증, 순차 압축으로 모바일 OOM 방지 - HEIC/HEIF 지원: pillow-heif 서버사이드 변환, Pillow 12.2.0 업그레이드 - 조치 페이지: '조치 완료 저장' 단일 버튼으로 단순화 - 재조치 흐름: 관리자 재조치 요청 시 이전 조치 이력을 번호 카드로 순차 표시 - 신고 순번: 전체 기준 ROW_NUMBER(oldest=1) 순번 표시, 삭제 gap 제거 - 모바일 탭바: position:fixed 적용으로 nav 하단 흰 여백 제거 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
239 lines
11 KiB
HTML
239 lines
11 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="form-group" style="margin-bottom:12px">
|
|
<label>계정 유형 <span style="color:var(--red)">*</span></label>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:4px">
|
|
<label id="roleCardMechanic" onclick="selectRole('mechanic')" style="border:2px solid var(--accent);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;background:#E3EDFF;">
|
|
<div style="font-size:18px;margin-bottom:2px">🔧</div>
|
|
<div style="font-size:13px;font-weight:700;color:var(--navy)">정비사</div>
|
|
<div style="font-size:11px;color:var(--gray4)">조치 입력·처리</div>
|
|
</label>
|
|
<label id="roleCardObserver" onclick="selectRole('observer')" style="border:2px solid var(--gray3);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;">
|
|
<div style="font-size:18px;margin-bottom:2px">👁</div>
|
|
<div style="font-size:13px;font-weight:700;color:var(--navy)">옵저버</div>
|
|
<div style="font-size:11px;color:var(--gray4)">현황 조회만 가능</div>
|
|
</label>
|
|
</div>
|
|
<input type="hidden" id="regRole" value="mechanic">
|
|
</div>
|
|
<div id="regNotice" 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);
|
|
const redirect = sessionStorage.getItem('ev_redirect');
|
|
sessionStorage.removeItem('ev_redirect');
|
|
if (redirect && redirect !== '/pages/login.html') {
|
|
location.href = redirect;
|
|
} else if (data.role === 'admin') location.href = '/pages/admin/dashboard.html';
|
|
else if (data.role === 'mechanic') location.href = '/pages/mechanic/dashboard.html';
|
|
else if (data.role === 'observer') location.href = '/pages/observer/dashboard.html';
|
|
else location.href = '/pages/manufacturer/dashboard.html';
|
|
} catch(e) {
|
|
document.getElementById('err').textContent = e.message;
|
|
document.getElementById('loginBtn').disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── 계정 유형 선택 ──
|
|
function selectRole(role) {
|
|
document.getElementById('regRole').value = role;
|
|
const mc = document.getElementById('roleCardMechanic');
|
|
const oc = document.getElementById('roleCardObserver');
|
|
if (role === 'mechanic') {
|
|
mc.style.cssText = 'border:2px solid var(--accent);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;background:#E3EDFF;';
|
|
oc.style.cssText = 'border:2px solid var(--gray3);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;';
|
|
document.getElementById('regNotice').innerHTML = '📌 정비사 계정으로 가입됩니다.<br>가입 후 <strong>관리자 승인</strong>이 완료되어야 로그인 가능합니다.';
|
|
} else {
|
|
oc.style.cssText = 'border:2px solid var(--accent);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;background:#E3EDFF;';
|
|
mc.style.cssText = 'border:2px solid var(--gray3);border-radius:8px;padding:10px 12px;cursor:pointer;text-align:center;transition:all .15s;';
|
|
document.getElementById('regNotice').innerHTML = '👁 현황 조회 전용 계정입니다.<br>신고 등록·조치 등 쓰기 기능은 사용할 수 없습니다.<br>가입 후 <strong>관리자 승인</strong>이 완료되어야 로그인 가능합니다.';
|
|
}
|
|
}
|
|
|
|
// ── 제조사 목록 로드 (비인증) ──
|
|
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);
|
|
fd.append('role', document.getElementById('regRole').value);
|
|
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>
|