Files
tk-factory-services/gateway/html/dashboard.html
Hyungi Ahn 0de9d5bb48 feat(sso): 인앱 브라우저 SSO 토큰 릴레이 — 카톡 WebView 쿠키 미공유 해결
카카오톡 인앱 WebView는 서브도메인 간 쿠키를 공유하지 않아
tkds에서 로그인 후 tkfb로 리다이렉트 시 인증이 풀리는 문제.

- sso-relay.js: URL hash의 _sso= 토큰을 로컬 쿠키+localStorage로 설정
- gateway dashboard: 로그인 후 redirect URL에 #_sso=<token> 추가
- 전 서비스 HTML: core JS 직전에 sso-relay.js 로드 (81개 파일)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:44:02 +09:00

871 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TK 대시보드</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
}
/* ===== Login Form ===== */
.login-wrapper {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-box {
background: white;
border-radius: 12px;
padding: 40px;
width: 100%;
max-width: 400px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.login-box h1 {
text-align: center;
color: #1a56db;
font-size: 22px;
margin-bottom: 6px;
}
.login-box .sub {
text-align: center;
color: #6b7280;
font-size: 13px;
margin-bottom: 28px;
}
.form-group { margin-bottom: 16px; }
.form-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.form-group input {
width: 100%;
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.form-group input:focus {
border-color: #1a56db;
box-shadow: 0 0 0 3px rgba(26,86,219,0.1);
}
.btn-submit {
width: 100%;
padding: 12px;
background: #1a56db;
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
margin-top: 8px;
}
.btn-submit:hover { background: #1e40af; }
.btn-submit:disabled { background: #93c5fd; cursor: not-allowed; }
.error-msg {
color: #dc2626;
font-size: 13px;
text-align: center;
margin-top: 12px;
display: none;
}
/* ===== Dashboard ===== */
.header {
background: #1a56db;
color: white;
padding: 14px 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header h1 { font-size: 18px; font-weight: 600; }
.user-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.user-info span { opacity: 0.9; }
.btn-logout {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 6px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.btn-logout:hover { background: rgba(255,255,255,0.3); }
.container {
max-width: 1080px;
margin: 0 auto;
padding: 24px 16px 40px;
}
.section { margin-bottom: 28px; }
.section-title {
font-size: 15px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
/* Card grid */
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (min-width: 640px) {
.card-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1024px) {
.card-grid { grid-template-columns: repeat(4, 1fr); }
}
.card {
background: white;
border-radius: 10px;
padding: 14px;
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
gap: 10px;
min-height: 56px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: transform 0.15s, box-shadow 0.15s;
cursor: pointer;
border: 1px solid #e5e7eb;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.card:active { transform: translateY(0); }
.card-icon { font-size: 22px; flex-shrink: 0; }
.card-name { font-size: 13px; font-weight: 500; color: #1f2937; line-height: 1.3; }
/* System cards */
.system-grid {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
@media (min-width: 640px) {
.system-grid { grid-template-columns: repeat(3, 1fr); }
}
.system-card {
background: white;
border-radius: 10px;
padding: 18px 16px;
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: transform 0.15s, box-shadow 0.15s;
cursor: pointer;
border-left: 4px solid;
}
.system-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.system-card .card-icon { font-size: 26px; }
.system-card .card-name { font-size: 14px; font-weight: 600; }
/* Banner */
.banner-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.banner-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: white;
border-radius: 10px;
border-left: 4px solid;
text-decoration: none;
color: inherit;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: transform 0.15s, box-shadow 0.15s;
}
.banner-item:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.banner-icon { font-size: 20px; flex-shrink: 0; }
.banner-text { font-size: 14px; font-weight: 500; color: #1f2937; flex: 1; }
.banner-arrow { font-size: 18px; color: #9ca3af; }
/* Coming soon */
.badge-soon {
display: inline-block;
background: #f3f4f6;
color: #6b7280;
font-size: 10px;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
margin-left: 4px;
vertical-align: middle;
}
.card.coming-soon {
opacity: 0.55;
cursor: default;
}
.card.coming-soon:hover {
transform: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
/* ===== Welcome Section ===== */
.welcome-section {
padding: 24px;
margin-bottom: 24px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-radius: 12px;
}
.welcome-greeting {
font-size: 20px;
font-weight: 600;
color: #1e3a5f;
}
.welcome-meta {
font-size: 14px;
color: #6b7280;
margin-top: 6px;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
/* ===== Stats Cards ===== */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (min-width: 640px) {
.stats-grid { grid-template-columns: repeat(3, 1fr); }
}
.stat-card {
background: white;
border-radius: 10px;
padding: 18px 14px;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
border: 1px solid #e5e7eb;
}
.stat-icon { font-size: 22px; margin-bottom: 4px; }
.stat-label { font-size: 12px; color: #6b7280; font-weight: 500; }
.stat-value { font-size: 28px; font-weight: 700; margin-top: 4px; }
/* ===== Card desc ===== */
.card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.card-desc { font-size: 11px; color: #9ca3af; font-weight: 400; line-height: 1.3; }
.footer {
text-align: center;
padding: 16px;
color: #9ca3af;
font-size: 11px;
}
</style>
</head>
<body>
<!-- Login Form -->
<div id="loginView" class="login-wrapper" style="display:none">
<div class="login-box">
<h1>TK 공장관리 시스템</h1>
<p class="sub">통합 로그인</p>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label for="username">사용자명</label>
<input type="text" id="username" name="username" required autofocus autocomplete="username">
</div>
<div class="form-group">
<label for="password">비밀번호</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="btn-submit" id="submitBtn">로그인</button>
<p class="error-msg" id="errorMsg"></p>
</form>
</div>
</div>
<!-- Dashboard -->
<div id="dashboardView" style="display:none">
<div class="header">
<h1>TK 대시보드</h1>
<div class="user-info">
<span id="userName"></span>
<button class="btn-logout" onclick="logout()">로그아웃</button>
</div>
</div>
<div class="container">
<div class="welcome-section" id="welcomeSection" style="display:none">
<div class="welcome-greeting" id="welcomeGreeting"></div>
<div class="welcome-meta">
<span id="welcomeDate"></span>
<span id="welcomeWeather"></span>
</div>
</div>
<div class="section" id="statsSection" style="display:none">
<div class="stats-grid" id="statsGrid"></div>
</div>
<div class="section" id="bannerSection" style="display:none">
<div class="banner-list" id="bannerList"></div>
</div>
<div class="section" id="systemSection" style="display:none">
<div class="section-title"><span>&#127970;</span> 시스템</div>
<div class="system-grid" id="systemGrid"></div>
</div>
</div>
<div class="footer">TK Factory Services v1.0</div>
</div>
<script>
// ===== SSO Cookie Utility =====
var ssoCookie = {
set: function(name, value, days) {
var cookie = name + '=' + encodeURIComponent(value) + '; path=/';
if (days) cookie += '; max-age=' + (days * 86400);
if (window.location.hostname.includes('technicalkorea.net')) {
cookie += '; domain=.technicalkorea.net; secure; samesite=lax';
}
document.cookie = cookie;
},
get: function(name) {
var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
},
remove: function(name) {
var cookie = name + '=; path=/; max-age=0';
if (window.location.hostname.includes('technicalkorea.net')) {
cookie += '; domain=.technicalkorea.net; secure; samesite=lax';
}
document.cookie = cookie;
}
};
function getToken() {
return ssoCookie.get('sso_token') || localStorage.getItem('sso_token');
}
function getUser() {
var raw = ssoCookie.get('sso_user') || localStorage.getItem('sso_user');
try { return JSON.parse(raw); } catch(e) { return null; }
}
function isTokenValid(token) {
try {
var payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp > Math.floor(Date.now() / 1000);
} catch (e) { return false; }
}
function isSafeRedirect(url) {
if (!url) return false;
if (/^\/[a-zA-Z0-9]/.test(url) && !url.includes('://') && !url.includes('//')) return true;
try {
var parsed = new URL(url);
return parsed.hostname.endsWith('.technicalkorea.net') || parsed.hostname === 'technicalkorea.net';
} catch (e) { return false; }
}
// ===== Subdomain URLs =====
function getSubdomainUrl(name) {
var hostname = window.location.hostname;
var protocol = window.location.protocol;
if (hostname.includes('technicalkorea.net')) {
return protocol + '//' + name + '.technicalkorea.net';
}
var ports = { tkfb: 30080, tkreport: 30180, tkqc: 30280, tkuser: 30380, tkpurchase: 30480, tksafety: 30580, tksupport: 30680 };
return protocol + '//' + hostname + ':' + (ports[name] || 30000);
}
// ===== Banner Definitions =====
var BANNERS = [
{
id: 'notifications',
icon: '\uD83D\uDD14',
api: '/api/notifications/unread/count',
parse: function(data) {
var count = data.data && data.data.count;
return count > 0 ? { text: '\uBBF8\uD655\uC778 \uC54C\uB9BC ' + count + '\uAC74' } : null;
},
subdomain: 'tkfb',
path: '/pages/profile/notifications.html',
color: '#1a56db'
},
{
id: 'tbm',
icon: '\uD83D\uDCCB',
api: '/api/tbm/sessions/incomplete-reports',
parse: function(data) {
var items = data.data || data;
var count = Array.isArray(items) ? items.length : 0;
return count > 0 ? { text: '\uBBF8\uC81C\uCD9C TBM \uBCF4\uACE0 ' + count + '\uAC74' } : null;
},
subdomain: 'tkfb',
path: '/pages/work/tbm.html',
color: '#d97706',
requirePageKey: 'work.tbm'
},
{
id: 'vacation',
icon: '\uD83D\uDCC5',
api: '/api/vacation-requests/pending',
parse: function(data) {
var items = data.data || data;
var count = Array.isArray(items) ? items.length : 0;
return count > 0 ? { text: '\uD734\uAC00 \uC2B9\uC778 \uB300\uAE30 ' + count + '\uAC74' } : null;
},
subdomain: 'tkfb',
path: '/pages/attendance/vacation-management.html',
color: '#7c3aed',
requirePageKey: 'attendance.vacation_management'
}
];
// ===== Banner Loading =====
async function loadBanners(token, allowed) {
var container = document.getElementById('bannerList');
container.innerHTML = '';
var visible = BANNERS.filter(function(b) {
return !b.requirePageKey || allowed.has(b.requirePageKey);
});
var results = await Promise.allSettled(
visible.map(function(b) {
return fetch(b.api, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(data) { return data ? { banner: b, result: b.parse(data) } : null; });
})
);
var anyVisible = false;
results.forEach(function(r) {
if (r.status !== 'fulfilled' || !r.value || !r.value.result) return;
var b = r.value.banner;
var result = r.value.result;
var a = document.createElement('a');
a.className = 'banner-item';
a.style.borderLeftColor = b.color;
a.href = getSubdomainUrl(b.subdomain) + (b.path || '');
a.innerHTML = '<span class="banner-icon">' + b.icon + '</span>'
+ '<span class="banner-text">' + result.text + '</span>'
+ '<span class="banner-arrow">\u203A</span>';
container.appendChild(a);
anyVisible = true;
});
if (anyVisible) {
document.getElementById('bannerSection').style.display = '';
}
}
// ===== Card Definitions =====
var SYSTEM_CARDS = [
{ id: 'factory', name: '공장관리', desc: '작업장 현황, TBM, 설비관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard.html', pageKey: 'dashboard', color: '#1a56db' },
{ id: 'report_sys', name: '신고', desc: '사건·사고 신고 접수', icon: '\uD83D\uDEA8', subdomain: 'tkreport', accessKey: 'system2', color: '#dc2626' },
{ id: 'quality', name: '부적합관리', desc: '부적합 이슈 추적·처리', icon: '\uD83D\uDCCA', subdomain: 'tkqc', accessKey: 'system3', color: '#059669' },
{ id: 'purchase', name: '구매관리', desc: '자재 구매, 일용직 관리', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', color: '#d97706' },
{ id: 'safety', name: '안전관리', desc: '안전 점검, 방문 관리', icon: '\uD83E\uDDBA', subdomain: 'tksafety', color: '#7c3aed' },
{ id: 'support', name: '행정지원', desc: '전사 행정 업무 지원', icon: '\uD83C\uDFE2', subdomain: 'tksupport', color: '#0284c7' },
{ id: 'admin', name: '통합관리', desc: '사용자·권한 관리', icon: '\u2699\uFE0F', subdomain: 'tkuser', minRole: 'admin', color: '#0891b2' }
];
// ===== Rendering =====
function resolveHref(card) {
if (card.href) return card.href;
if (card.subdomain) {
var base = getSubdomainUrl(card.subdomain);
return card.path ? base + card.path : base;
}
return '#';
}
function createCardElement(card, isSystem) {
var a = document.createElement('a');
a.className = isSystem ? 'system-card' : 'card';
if (isSystem && card.color) {
a.style.borderLeftColor = card.color;
}
if (card.comingSoon) {
a.className += ' coming-soon';
a.href = 'javascript:void(0)';
a.onclick = function(e) { e.preventDefault(); };
} else {
a.href = resolveHref(card);
}
var iconSpan = document.createElement('span');
iconSpan.className = 'card-icon';
iconSpan.textContent = card.icon;
a.appendChild(iconSpan);
var nameSpan = document.createElement('span');
nameSpan.className = 'card-name';
nameSpan.textContent = card.name;
if (card.comingSoon) {
var badge = document.createElement('span');
badge.className = 'badge-soon';
badge.textContent = '\uC900\uBE44\uC911';
nameSpan.appendChild(document.createTextNode(' '));
nameSpan.appendChild(badge);
}
if (card.desc && isSystem) {
var textDiv = document.createElement('div');
textDiv.className = 'card-text';
textDiv.appendChild(nameSpan);
var descSpan = document.createElement('span');
descSpan.className = 'card-desc';
descSpan.textContent = card.desc;
textDiv.appendChild(descSpan);
a.appendChild(textDiv);
} else {
a.appendChild(nameSpan);
}
return a;
}
function isCardVisible(card, allowed, systemAccess, userRole) {
if (card.comingSoon) return true;
if (card.minRole) {
var roleOrder = ['user','leader','support_team','admin','system'];
var userIdx = roleOrder.indexOf(userRole);
var minIdx = roleOrder.indexOf(card.minRole);
if (userIdx < minIdx) return false;
}
if (card.pageKey && !allowed.has(card.pageKey)) return false;
if (card.accessKey && systemAccess[card.accessKey] === false) return false;
return true;
}
function renderSection(sectionId, gridId, cards, allowed, systemAccess, isSystem, userRole) {
var visible = cards.filter(function(c) { return isCardVisible(c, allowed, systemAccess, userRole); });
if (visible.length === 0) return;
var grid = document.getElementById(gridId);
grid.innerHTML = '';
visible.forEach(function(card) {
grid.appendChild(createCardElement(card, isSystem));
});
document.getElementById(sectionId).style.display = '';
}
// ===== Dashboard =====
async function showDashboard(user, token) {
document.getElementById('loginView').style.display = 'none';
document.getElementById('dashboardView').style.display = '';
document.getElementById('userName').textContent = (user.name || user.username);
var systemAccess = user.system_access || {};
var allowed = new Set();
// Fetch page access
try {
var userId = user.user_id || user.id;
var res = await fetch('/api/users/' + userId + '/page-access', {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (data.success && data.data && data.data.pageAccess) {
data.data.pageAccess.forEach(function(p) {
if (p.can_access) allowed.add(p.page_key);
});
}
} catch (e) {
// Fallback: show cards based on system_access
console.warn('page-access API error:', e);
if (systemAccess.system1 !== false) {
['work.tbm', 'work.report_create', 'inspection.checkin',
'attendance.my_vacation_info', 'attendance.vacation_request',
'dashboard'].forEach(function(k) { allowed.add(k); });
}
}
// A: Welcome section
showWelcome(user, token);
// B: Today stats
loadTodayStats(token, allowed);
// Render banners + system cards
var userRole = user.role || 'user';
loadBanners(token, allowed);
renderSection('systemSection', 'systemGrid', SYSTEM_CARDS, allowed, systemAccess, true, userRole);
}
// ===== A: Welcome =====
function getGreeting() {
var h = new Date().getHours();
if (h >= 6 && h < 12) return '좋은 아침입니다';
if (h >= 12 && h < 18) return '좋은 오후입니다';
return '좋은 저녁입니다';
}
function getWeatherIcon(sky, precip) {
var p = Number(precip);
if (p === 1 || p === 2 || p === 4 || p === 5 || p === 6) return '\uD83C\uDF27\uFE0F';
if (p === 3 || p === 7) return '\u2744\uFE0F';
if (sky === 'overcast') return '\u2601\uFE0F';
if (sky === 'cloudy') return '\u26C5';
return '\u2600\uFE0F';
}
function getSkyLabel(sky) {
if (sky === 'clear') return '맑음';
if (sky === 'cloudy') return '구름많음';
if (sky === 'overcast') return '흐림';
return '';
}
function showWelcome(user, token) {
var section = document.getElementById('welcomeSection');
var name = user.name || user.username;
document.getElementById('welcomeGreeting').textContent = getGreeting() + ', ' + name + '님';
var now = new Date();
var days = ['일','월','화','수','목','금','토'];
var dateStr = now.getFullYear() + '년 ' + (now.getMonth()+1) + '월 ' + now.getDate() + '일 (' + days[now.getDay()] + ')';
document.getElementById('welcomeDate').textContent = dateStr;
section.style.display = '';
// Weather (async, hide on failure)
var weatherEl = document.getElementById('welcomeWeather');
weatherEl.textContent = '';
fetch('/api/tbm/weather/current', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.ok ? r.json() : Promise.reject(); })
.then(function(json) {
var d = json.data;
if (!d) return;
var icon = getWeatherIcon(d.skyCondition, d.precipitationType);
var label = getSkyLabel(d.skyCondition);
var temp = d.temperature != null ? Math.round(d.temperature) + '\u00B0C' : '';
weatherEl.textContent = '| ' + icon + ' ' + label + ' ' + temp;
})
.catch(function() { weatherEl.textContent = ''; });
}
// ===== B: Today Stats =====
function loadTodayStats(token, allowed) {
var today = new Date().toISOString().slice(0, 10);
var cards = [];
var fetches = [];
// Attendance
if (allowed.has('attendance.daily') || allowed.has('attendance.vacation_management') || allowed.has('dashboard')) {
var idx = cards.length;
cards.push({ icon: '\uD83D\uDC77', label: '출근', value: '\u2013', color: '#1a56db', visible: true });
fetches.push(
fetch('/api/attendance/daily-status?date=' + today, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.ok ? r.json() : Promise.reject(); })
.then(function(json) {
var data = json.data;
if (Array.isArray(data)) {
cards[idx].value = data.filter(function(d) { return d.status !== 'vacation'; }).length + '명';
}
})
.catch(function() { cards[idx].visible = false; })
);
}
// Work reports
if (allowed.has('dashboard')) {
var idx2 = cards.length;
cards.push({ icon: '\uD83D\uDD27', label: '작업', value: '\u2013', color: '#059669', visible: true });
fetches.push(
fetch('/api/work-analysis/dashboard?start=' + today + '&end=' + today, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.ok ? r.json() : Promise.reject(); })
.then(function(json) {
var stats = json.data && json.data.stats;
if (stats) {
cards[idx2].value = (stats.totalReports || 0) + '건';
}
})
.catch(function() { cards[idx2].visible = false; })
);
}
// Issues
var idx3 = cards.length;
cards.push({ icon: '\u26A0\uFE0F', label: '이슈', value: '\u2013', color: '#dc2626', visible: true });
fetches.push(
fetch('/api/work-issues/stats/summary', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.ok ? r.json() : Promise.reject(); })
.then(function(json) {
var data = json.data;
if (data && data.openCount != null) {
cards[idx3].value = data.openCount + '건';
} else if (data && data.total != null) {
cards[idx3].value = data.total + '건';
}
})
.catch(function() { cards[idx3].visible = false; })
);
if (fetches.length === 0) return;
Promise.allSettled(fetches).then(function() {
var visible = cards.filter(function(c) { return c.visible; });
if (visible.length === 0) return;
var grid = document.getElementById('statsGrid');
grid.innerHTML = '';
visible.forEach(function(c) {
var div = document.createElement('div');
div.className = 'stat-card';
div.innerHTML = '<div class="stat-icon">' + c.icon + '</div>'
+ '<div class="stat-label">' + c.label + '</div>'
+ '<div class="stat-value" style="color:' + c.color + '">' + c.value + '</div>';
grid.appendChild(div);
});
document.getElementById('statsSection').style.display = '';
});
}
// ===== Login =====
async function handleLogin(e) {
e.preventDefault();
var btn = document.getElementById('submitBtn');
var errEl = document.getElementById('errorMsg');
errEl.style.display = 'none';
btn.disabled = true;
btn.textContent = '\uB85C\uADF8\uC778 \uC911...';
try {
var res = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
})
});
var data = await res.json();
if (!res.ok || !data.success) throw new Error(data.error || '\uB85C\uADF8\uC778\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4');
ssoCookie.set('sso_token', data.access_token, 7);
ssoCookie.set('sso_user', JSON.stringify(data.user), 7);
if (data.refresh_token) ssoCookie.set('sso_refresh_token', data.refresh_token, 30);
var redirect = new URLSearchParams(location.search).get('redirect');
if (redirect && isSafeRedirect(redirect)) {
var sep = redirect.indexOf('#') === -1 ? '#' : '&';
window.location.href = redirect + sep + '_sso=' + encodeURIComponent(data.access_token);
} else {
window.location.href = '/dashboard';
}
} catch (err) {
errEl.textContent = err.message;
errEl.style.display = 'block';
} finally {
btn.disabled = false;
btn.textContent = '\uB85C\uADF8\uC778';
}
}
// ===== Logout =====
function logout() {
ssoCookie.remove('sso_token');
ssoCookie.remove('sso_user');
ssoCookie.remove('sso_refresh_token');
['sso_token','sso_user','sso_refresh_token','token','user','access_token',
'currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) {
localStorage.removeItem(k);
});
fetch('/auth/logout', { method: 'POST' }).catch(function(){});
window.location.href = '/dashboard?logout=1';
}
// ===== Auth cleanup helpers =====
function clearAllAuth() {
ssoCookie.remove('sso_token');
ssoCookie.remove('sso_user');
ssoCookie.remove('sso_refresh_token');
['sso_token','sso_user','sso_refresh_token','token','user','access_token',
'currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) {
localStorage.removeItem(k);
});
}
// ===== Entry Point =====
(function init() {
var params = new URLSearchParams(location.search);
var isLogout = params.get('logout') === '1';
if (isLogout) clearAllAuth();
var token = isLogout ? null : getToken();
if (token && token !== 'undefined' && token !== 'null') {
if (isTokenValid(token)) {
var user = getUser();
if (user) {
// Partner redirect
if (user.partner_company_id) {
window.location.href = getSubdomainUrl('tkfb') + '/pages/partner/partner-portal.html';
return;
}
// Already logged in + redirect param
var redirect = params.get('redirect');
if (redirect && isSafeRedirect(redirect)) {
var sep = redirect.indexOf('#') === -1 ? '#' : '&';
window.location.href = redirect + sep + '_sso=' + encodeURIComponent(token);
return;
}
// Sync cookies
var existingUser = ssoCookie.get('sso_user') || localStorage.getItem('sso_user');
var existingRefresh = ssoCookie.get('sso_refresh_token') || localStorage.getItem('sso_refresh_token');
ssoCookie.set('sso_token', token, 7);
if (existingUser) ssoCookie.set('sso_user', existingUser, 7);
if (existingRefresh) ssoCookie.set('sso_refresh_token', existingRefresh, 30);
showDashboard(user, token);
return;
}
}
// Token invalid or no user data
clearAllAuth();
}
// Show login
document.getElementById('loginView').style.display = 'flex';
document.getElementById('dashboardView').style.display = 'none';
})();
</script>
</body>
</html>