refactor(gateway): gateway↔system1 분리 — gateway=문짝, system1-web=독립
gateway에서 system1 프록시 제거, 대시보드+로그인+공유JS만 담당. system1-web에 /auth/, /ai-api/ 프록시 이관. tkds-web 제거(gateway 흡수). notification-bell URL tkfb→tkds, system3 로그인 URL tkds/dashboard로 변경. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
664
gateway/html/dashboard.html
Normal file
664
gateway/html/dashboard.html
Normal file
@@ -0,0 +1,664 @@
|
||||
<!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: 800px;
|
||||
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);
|
||||
}
|
||||
|
||||
.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="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>🏢</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: 's1.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: 's1.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: '공장관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard.html', pageKey: 's1.dashboard', color: '#1a56db' },
|
||||
{ id: 'report_sys', name: '신고', icon: '\uD83D\uDEA8', subdomain: 'tkreport', accessKey: 'system2', color: '#dc2626' },
|
||||
{ id: 'quality', name: '부적합관리', icon: '\uD83D\uDCCA', subdomain: 'tkqc', pageKey: 'issues_dashboard', color: '#059669' },
|
||||
{ id: 'purchase', name: '구매관리', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', pageKey: 'purchasing_schedule', color: '#d97706' },
|
||||
{ id: 'safety', name: '안전관리', icon: '\uD83D\uDD27', subdomain: 'tksafety', pageKey: 'safety_visit_management', color: '#7c3aed' },
|
||||
{ id: 'support', name: '행정지원', icon: '\uD83C\uDFE2', subdomain: 'tksupport', comingSoon: true, color: '#6b7280' },
|
||||
{ id: 'admin', name: '통합관리', icon: '\u2699\uFE0F', subdomain: 'tkuser', pageKey: 'tkuser.users', 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);
|
||||
}
|
||||
|
||||
a.appendChild(nameSpan);
|
||||
return a;
|
||||
}
|
||||
|
||||
function isCardVisible(card, allowed, systemAccess) {
|
||||
if (card.comingSoon) return true;
|
||||
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) {
|
||||
var visible = cards.filter(function(c) { return isCardVisible(c, allowed, systemAccess); });
|
||||
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) {
|
||||
['s1.work.tbm', 's1.work.report_create', 's1.inspection.checkin',
|
||||
's1.attendance.my_vacation_info', 's1.attendance.vacation_request',
|
||||
's1.dashboard'].forEach(function(k) { allowed.add(k); });
|
||||
}
|
||||
if (systemAccess.system3 !== false) allowed.add('issues_dashboard');
|
||||
}
|
||||
|
||||
// Render banners + system cards
|
||||
loadBanners(token, allowed);
|
||||
renderSection('systemSection', 'systemGrid', SYSTEM_CARDS, allowed, systemAccess, true);
|
||||
}
|
||||
|
||||
// ===== 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);
|
||||
|
||||
localStorage.setItem('sso_token', data.access_token);
|
||||
localStorage.setItem('sso_user', JSON.stringify(data.user));
|
||||
if (data.refresh_token) localStorage.setItem('sso_refresh_token', data.refresh_token);
|
||||
|
||||
var redirect = new URLSearchParams(location.search).get('redirect');
|
||||
if (redirect && isSafeRedirect(redirect)) {
|
||||
window.location.href = redirect;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = '';
|
||||
} 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)) {
|
||||
window.location.href = redirect;
|
||||
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>
|
||||
@@ -1,240 +0,0 @@
|
||||
<!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;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
// SSO 쿠키 유틸리티
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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 = '로그인 중...';
|
||||
|
||||
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 || '로그인에 실패했습니다');
|
||||
}
|
||||
|
||||
// 쿠키에 토큰 저장 (서브도메인 공유)
|
||||
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);
|
||||
}
|
||||
|
||||
// localStorage에도 저장 (같은 도메인 내 호환성)
|
||||
localStorage.setItem('sso_token', data.access_token);
|
||||
localStorage.setItem('sso_user', JSON.stringify(data.user));
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem('sso_refresh_token', data.refresh_token);
|
||||
}
|
||||
|
||||
// redirect 파라미터가 있으면 해당 URL로, 없으면 포털로
|
||||
var redirect = new URLSearchParams(location.search).get('redirect');
|
||||
if (redirect && isSafeRedirect(redirect)) {
|
||||
window.location.href = redirect;
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = '';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '로그인';
|
||||
}
|
||||
}
|
||||
|
||||
// 안전한 리다이렉트인지 확인 (같은 도메인 상대 경로 또는 *.technicalkorea.net)
|
||||
function isSafeRedirect(url) {
|
||||
if (!url) return false;
|
||||
// 상대 경로
|
||||
if (/^\/[a-zA-Z0-9]/.test(url) && !url.includes('://') && !url.includes('//')) {
|
||||
return true;
|
||||
}
|
||||
// technicalkorea.net 서브도메인 절대 URL
|
||||
try {
|
||||
var parsed = new URL(url);
|
||||
return parsed.hostname.endsWith('.technicalkorea.net') || parsed.hostname === 'technicalkorea.net';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 토큰 만료 확인
|
||||
function isTokenValid(token) {
|
||||
try {
|
||||
var payload = JSON.parse(atob(token.split('.')[1]));
|
||||
return payload.exp > Math.floor(Date.now() / 1000);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// logout=1 파라미터가 있으면 모든 인증 데이터 정리
|
||||
var isLogout = new URLSearchParams(location.search).get('logout') === '1';
|
||||
if (isLogout) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// 이미 로그인 되어있으면 포털로 (쿠키 또는 localStorage 체크 + 만료 확인)
|
||||
var existingToken = isLogout ? null : (ssoCookie.get('sso_token') || localStorage.getItem('sso_token'));
|
||||
if (existingToken && existingToken !== 'undefined' && existingToken !== 'null') {
|
||||
if (isTokenValid(existingToken)) {
|
||||
// 쿠키 재설정 (localStorage에만 있고 쿠키가 없는 경우 대비)
|
||||
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', existingToken, 7);
|
||||
if (existingUser) ssoCookie.set('sso_user', existingUser, 7);
|
||||
if (existingRefresh) ssoCookie.set('sso_refresh_token', existingRefresh, 30);
|
||||
|
||||
var redirect = new URLSearchParams(location.search).get('redirect');
|
||||
window.location.href = (redirect && isSafeRedirect(redirect)) ? redirect : '/';
|
||||
} else {
|
||||
// 만료된 토큰 정리
|
||||
ssoCookie.remove('sso_token');
|
||||
ssoCookie.remove('sso_user');
|
||||
ssoCookie.remove('sso_refresh_token');
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,254 +0,0 @@
|
||||
<!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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
padding: 16px 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: 20px; font-weight: 600; }
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
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: 900px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
flex: 1;
|
||||
}
|
||||
.welcome {
|
||||
text-align: center;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.welcome h2 { font-size: 26px; color: #1f2937; margin-bottom: 8px; }
|
||||
.welcome p { color: #6b7280; font-size: 15px; }
|
||||
.systems {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.system-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.system-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.12);
|
||||
}
|
||||
.system-card.s1 { border-top: 4px solid #1a56db; }
|
||||
.system-card.s2 { border-top: 4px solid #dc2626; }
|
||||
.system-card.s3 { border-top: 4px solid #059669; }
|
||||
.system-icon { font-size: 36px; margin-bottom: 14px; }
|
||||
.system-card h3 { font-size: 18px; color: #1f2937; margin-bottom: 6px; }
|
||||
.system-card p { color: #6b7280; font-size: 13px; line-height: 1.5; }
|
||||
.system-card .badge {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.s1 .badge { background: #dbeafe; color: #1d4ed8; }
|
||||
.s2 .badge { background: #fee2e2; color: #dc2626; }
|
||||
.s3 .badge { background: #d1fae5; color: #059669; }
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
.login-prompt {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
.login-prompt h2 { margin-bottom: 16px; color: #1f2937; }
|
||||
.btn-login {
|
||||
display: inline-block;
|
||||
background: #1a56db;
|
||||
color: white;
|
||||
padding: 12px 32px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-login:hover { background: #1e40af; }
|
||||
.no-access {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>TK 공장관리 시스템</h1>
|
||||
<div class="user-info" id="userInfo" style="display:none">
|
||||
<span id="userName"></span>
|
||||
<span id="userRole"></span>
|
||||
<button class="btn-logout" onclick="logout()">로그아웃</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 로그인 전 -->
|
||||
<div class="login-prompt" id="loginPrompt">
|
||||
<h2>시스템에 접속하려면 로그인하세요</h2>
|
||||
<a href="/login" class="btn-login">로그인</a>
|
||||
</div>
|
||||
|
||||
<!-- 로그인 후 -->
|
||||
<div id="dashboard" style="display:none">
|
||||
<div class="welcome">
|
||||
<h2 id="welcomeText">환영합니다</h2>
|
||||
<p>사용할 시스템을 선택하세요</p>
|
||||
</div>
|
||||
<div class="systems">
|
||||
<a href="/pages/dashboard.html" class="system-card s1" id="card-s1">
|
||||
<div class="system-icon">🏭</div>
|
||||
<h3>공장관리</h3>
|
||||
<p>작업보고, 근태관리, TBM, 순회점검, 장비관리 등 현장 운영 전반</p>
|
||||
<span class="badge">System 1</span>
|
||||
</a>
|
||||
<a id="card-s2-link" class="system-card s2" id="card-s2">
|
||||
<div class="system-icon">🚨</div>
|
||||
<h3>신고 시스템</h3>
|
||||
<p>안전/부적합 이슈 신고, 처리현황 추적, 부적합 자동 연동</p>
|
||||
<span class="badge">System 2</span>
|
||||
</a>
|
||||
<a id="card-s3-link" class="system-card s3" id="card-s3">
|
||||
<div class="system-icon">📋</div>
|
||||
<h3>부적합 관리</h3>
|
||||
<p>부적합 이슈 접수, 처리, 리포트 생성, 프로젝트별 현황 관리</p>
|
||||
<span class="badge">System 3</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">TK Factory Services v1.0</div>
|
||||
|
||||
<script src="/shared/nav-header.js"></script>
|
||||
<script>
|
||||
var ssoCookie = {
|
||||
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 init() {
|
||||
var token = getToken();
|
||||
var user = getUser();
|
||||
|
||||
if (token && user) {
|
||||
showDashboard(user);
|
||||
} else {
|
||||
document.getElementById('loginPrompt').style.display = '';
|
||||
document.getElementById('dashboard').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showDashboard(user) {
|
||||
document.getElementById('loginPrompt').style.display = 'none';
|
||||
document.getElementById('dashboard').style.display = '';
|
||||
document.getElementById('userInfo').style.display = 'flex';
|
||||
document.getElementById('userName').textContent = user.name || user.username;
|
||||
document.getElementById('userRole').textContent = '(' + (user.role || '') + ')';
|
||||
document.getElementById('welcomeText').textContent =
|
||||
(user.name || user.username) + '님, 환영합니다';
|
||||
|
||||
// 접근 권한에 따라 카드 비활성화
|
||||
const access = user.system_access || {};
|
||||
if (access.system1 === false) document.getElementById('card-s1').classList.add('no-access');
|
||||
if (access.system2 === false) document.getElementById('card-s2').classList.add('no-access');
|
||||
if (access.system3 === false) document.getElementById('card-s3').classList.add('no-access');
|
||||
}
|
||||
|
||||
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(){});
|
||||
location.reload();
|
||||
}
|
||||
|
||||
// 서브도메인 링크 설정
|
||||
function setupSystemLinks() {
|
||||
var hostname = window.location.hostname;
|
||||
var protocol = window.location.protocol;
|
||||
var s2Link, s3Link;
|
||||
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
s2Link = protocol + '//tkreport.technicalkorea.net';
|
||||
s3Link = protocol + '//tkqc.technicalkorea.net';
|
||||
} else {
|
||||
// 개발 환경: 포트 기반
|
||||
s2Link = protocol + '//' + hostname + ':30180';
|
||||
s3Link = protocol + '//' + hostname + ':30280';
|
||||
}
|
||||
|
||||
document.getElementById('card-s2-link').href = s2Link;
|
||||
document.getElementById('card-s3-link').href = s3Link;
|
||||
}
|
||||
setupSystemLinks();
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,29 +2,32 @@ server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 50M;
|
||||
|
||||
# ===== Gateway 자체 페이지 (포털, 로그인) =====
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# 로그인 페이지
|
||||
location = /login {
|
||||
# 대시보드 (로그인 포함)
|
||||
location = /dashboard {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
try_files /login.html =404;
|
||||
try_files /dashboard.html =404;
|
||||
}
|
||||
|
||||
# 대시보드 → tkds로 리다이렉트 (북마크 깨짐 방지)
|
||||
location = /dashboard {
|
||||
return 301 $scheme://tkds.technicalkorea.net/dashboard;
|
||||
location = / {
|
||||
return 302 /dashboard$is_args$args;
|
||||
}
|
||||
|
||||
# 공유 JS/CSS (nav-header 등)
|
||||
# 레거시 /login → /dashboard 리다이렉트
|
||||
location = /login {
|
||||
return 302 /dashboard$is_args$args;
|
||||
}
|
||||
|
||||
# 공유 JS (notification-bell.js, nav-header.js)
|
||||
location /shared/ {
|
||||
alias /usr/share/nginx/html/shared/;
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# ===== SSO Auth API =====
|
||||
# SSO Auth 프록시
|
||||
location /auth/ {
|
||||
proxy_pass http://sso-auth:3000/api/auth/;
|
||||
proxy_set_header Host $host;
|
||||
@@ -33,7 +36,7 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ===== System 1 API 프록시 =====
|
||||
# System1 API 프록시 (대시보드 배너용: 알림/TBM/휴가 카운트)
|
||||
location /api/ {
|
||||
proxy_pass http://system1-api:3005/api/;
|
||||
proxy_http_version 1.1;
|
||||
@@ -43,50 +46,9 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ===== System 1 업로드 파일 =====
|
||||
location /uploads/ {
|
||||
proxy_pass http://system1-api:3005/uploads/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# ===== System 1 FastAPI Bridge =====
|
||||
location /fastapi/ {
|
||||
proxy_pass http://system1-fastapi:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ===== AI Service API (맥미니 home-service-proxy 경유) =====
|
||||
location /ai-api/ {
|
||||
resolver 8.8.8.8 valid=300s ipv6=off;
|
||||
set $ai_upstream https://ai.hyungi.net;
|
||||
rewrite ^/ai-api/(.*) /api/ai/$1 break;
|
||||
proxy_pass $ai_upstream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host ai.hyungi.net;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_ssl_server_name on;
|
||||
proxy_read_timeout 180s;
|
||||
proxy_send_timeout 180s;
|
||||
}
|
||||
|
||||
# ===== System 1 Web (나머지 모든 경로) =====
|
||||
location / {
|
||||
proxy_pass http://system1-web:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ===== Health Check =====
|
||||
# Health check
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 '{"status":"ok","service":"gateway"}';
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user