2481 lines
114 KiB
HTML
2481 lines
114 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>PM_ver4 통합 관리자 어플리케이션 (Admin Panel) UI 제안</title>
|
||
<!-- Google Fonts - Inter & Noto Sans KR -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||
|
||
<style>
|
||
:root {
|
||
--bg-color: #f4f7f6;
|
||
--sidebar-bg: #142e29; /* Deep Teal-Green */
|
||
--sidebar-active: #1e5149; /* Primary Forest Green (pm_ver4 lv5) */
|
||
--card-bg: #ffffff;
|
||
--border: #d2dcdb; /* Light Green Gray (pm_ver4 lv1) */
|
||
--text-main: #141e1d; /* Very Dark Forest (pm_ver4 lv9) */
|
||
--text-muted: #4b746d; /* Muted Green (pm_ver4 lv4) */
|
||
--text-light: #a5b9b6; /* Soft Gray-Green (pm_ver4 lv2) */
|
||
--primary: #1e5149; /* Primary (pm_ver4 lv5) */
|
||
--primary-hover: #193833; /* Darker Primary (pm_ver4 lv7) */
|
||
--primary-soft: #e9eeed; /* Soft Teal-Gray (pm_ver4 lv0) */
|
||
--success: #4db251; /* pm_ver4 Green */
|
||
--success-soft: #eef8ee; /* pm_ver4 Light Green */
|
||
--warning: #ff9800; /* pm_ver4 Orange */
|
||
--warning-soft: #fff5e6; /* pm_ver4 Light Orange */
|
||
--radius-lg: 6px;
|
||
--radius-md: 4px;
|
||
--radius-sm: 2px;
|
||
--shadow: 0 4px 6px -1px rgb(20 30 29 / 0.08), 0 2px 4px -2px rgb(20 30 29 / 0.08);
|
||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
body {
|
||
font-family: 'Pretendard Variable', 'Pretendard', 'Inter', 'Noto Sans KR', sans-serif;
|
||
background-color: var(--bg-color);
|
||
color: var(--text-main);
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 1. Sidebar LNB */
|
||
.sidebar {
|
||
width: 260px;
|
||
background-color: var(--sidebar-bg);
|
||
color: #ffffff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar-brand {
|
||
padding: 24px 20px;
|
||
font-size: 1.15rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.5px;
|
||
border-bottom: 1px solid #1b443d; /* pm_ver4 lv6 */
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
background: linear-gradient(135deg, #1e5149 0%, #142e29 100%);
|
||
}
|
||
|
||
.sidebar-brand span.logo-highlight {
|
||
color: #4db251; /* pm_ver4 green accent */
|
||
}
|
||
|
||
.sidebar-menu {
|
||
flex: 1;
|
||
padding: 20px 12px;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 25px;
|
||
}
|
||
|
||
.menu-category {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.menu-category-title {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
color: #64748b;
|
||
text-transform: uppercase;
|
||
padding-left: 8px;
|
||
margin-bottom: 4px;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.menu-item {
|
||
padding: 10px 14px;
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
color: #cbd5e1;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
transition: var(--transition);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.menu-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.menu-item.active {
|
||
background-color: var(--primary);
|
||
color: #ffffff;
|
||
font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(30, 81, 73, 0.25);
|
||
}
|
||
|
||
/* 2. Main Content Area */
|
||
.main-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background-color: var(--bg-color);
|
||
}
|
||
|
||
.main-header {
|
||
height: 70px;
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 30px;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.02);
|
||
}
|
||
|
||
.main-header .page-title h2 {
|
||
margin: 0;
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.user-profile {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.user-profile .avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
background-color: var(--primary-soft);
|
||
color: var(--primary);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-weight: 700;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.user-profile .info {
|
||
font-size: 0.85rem;
|
||
text-align: right;
|
||
}
|
||
|
||
.user-profile .info .name {
|
||
font-weight: 600;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.user-profile .info .role {
|
||
font-size: 0.75rem;
|
||
color: var(--text-light);
|
||
}
|
||
|
||
.content-area {
|
||
flex: 1;
|
||
padding: 30px;
|
||
overflow-y: auto;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
animation: tabFadeIn 0.3s ease-out;
|
||
flex-direction: column;
|
||
gap: 25px;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: flex;
|
||
}
|
||
|
||
@keyframes tabFadeIn {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* 3. Cards, Forms, Tables Widgets */
|
||
.card {
|
||
background-color: var(--card-bg);
|
||
border-radius: var(--radius-lg);
|
||
border: 1px solid var(--border);
|
||
box-shadow: var(--shadow);
|
||
padding: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--border);
|
||
padding-bottom: 14px;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 1.05rem;
|
||
font-weight: 700;
|
||
margin: 0;
|
||
color: var(--text-main);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.grid-2col {
|
||
display: grid;
|
||
grid-template-columns: 1.25fr 1fr;
|
||
gap: 24px;
|
||
}
|
||
|
||
.grid-2col-even {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1.25fr;
|
||
gap: 24px;
|
||
}
|
||
|
||
.grid-3col {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 20px;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.grid-2col, .grid-2col-even, .grid-3col {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* KPI Card */
|
||
.kpi-card {
|
||
background-color: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.kpi-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: var(--radius-md);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.kpi-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 0.8rem;
|
||
color: var(--text-muted);
|
||
margin-bottom: 4px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 1.4rem;
|
||
font-weight: 700;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
/* Forms styling */
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.form-group label {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.text-input, .select-input, .date-input {
|
||
width: 100%;
|
||
padding: 10px 14px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border);
|
||
font-size: 0.9rem;
|
||
background-color: #f8fafc;
|
||
box-sizing: border-box;
|
||
outline: none;
|
||
transition: var(--transition);
|
||
font-family: inherit;
|
||
}
|
||
|
||
.text-input:focus, .select-input:focus, .date-input:focus {
|
||
border-color: var(--primary);
|
||
background-color: #ffffff;
|
||
box-shadow: 0 0 0 3px var(--primary-soft);
|
||
}
|
||
|
||
/* Buttons styling */
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border-radius: var(--radius-md);
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
border: none;
|
||
transition: var(--transition);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
outline: none;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: var(--primary);
|
||
color: #ffffff;
|
||
box-shadow: 0 2px 6px rgba(30, 81, 73, 0.15);
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: var(--primary-hover);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background-color: #ffffff;
|
||
color: var(--text-muted);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background-color: #f1f5f9;
|
||
}
|
||
|
||
.btn-danger {
|
||
background-color: #ef4444;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background-color: #dc2626;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 6px 12px;
|
||
font-size: 0.8rem;
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
/* Tables styling */
|
||
.table-wrapper {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-md);
|
||
overflow: hidden;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
table.admin-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.875rem;
|
||
text-align: left;
|
||
}
|
||
|
||
table.admin-table th {
|
||
background-color: #f8fafc;
|
||
color: var(--text-muted);
|
||
font-weight: 600;
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
table.admin-table td {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
color: var(--text-muted);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
table.admin-table tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
table.admin-table tr.clickable {
|
||
cursor: pointer;
|
||
}
|
||
|
||
table.admin-table tr.clickable:hover td {
|
||
background-color: var(--primary-soft);
|
||
}
|
||
|
||
table.admin-table tr.selected td {
|
||
background-color: var(--primary-soft) !important;
|
||
color: var(--primary-hover);
|
||
}
|
||
|
||
/* Badges */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.badge.active { background-color: var(--success-soft); color: var(--success); }
|
||
.badge.inactive { background-color: #f1f5f9; color: var(--text-light); }
|
||
.badge.danger { background-color: #fee2e2; color: #ef4444; }
|
||
.badge.warning { background-color: var(--warning-soft); color: var(--warning); }
|
||
|
||
.progress-bar-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.progress-bar-bg {
|
||
flex: 1;
|
||
height: 8px;
|
||
background-color: #e2e8f0;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar-fill {
|
||
height: 100%;
|
||
background-color: var(--primary);
|
||
border-radius: 4px;
|
||
transition: width 0.4s ease;
|
||
}
|
||
|
||
.action-btns {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.form-title-wrap {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 4. Modal CSS */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(20, 30, 29, 0.6);
|
||
backdrop-filter: blur(4px);
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
animation: modalFadeIn 0.2s ease-out;
|
||
}
|
||
|
||
@keyframes modalFadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
.modal-card {
|
||
width: 100%;
|
||
max-width: 500px;
|
||
background-color: #ffffff;
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||
border: 1px solid var(--border);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
animation: modalSlideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
@keyframes modalSlideUp {
|
||
from { transform: translateY(30px); opacity: 0; }
|
||
to { transform: translateY(0); opacity: 1; }
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid var(--border);
|
||
background-color: #f8fafc;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.25rem;
|
||
cursor: pointer;
|
||
color: var(--text-light);
|
||
}
|
||
|
||
.modal-close:hover {
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 16px 20px;
|
||
border-top: 1px solid var(--border);
|
||
background-color: #f8fafc;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* Checkbox list style inside modal */
|
||
.check-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.check-list-item:hover {
|
||
background-color: #f8fafc;
|
||
}
|
||
|
||
.check-list-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.check-list-item input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
accent-color: var(--primary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.check-list-item .user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.check-list-item .user-info-name {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.check-list-item .user-info-desc {
|
||
font-size: 0.75rem;
|
||
color: var(--text-light);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- 1. Sidebar LNB -->
|
||
<div class="sidebar">
|
||
<div class="sidebar-brand">
|
||
📁 PM_ver4 <span class="logo-highlight">Admin</span>
|
||
</div>
|
||
|
||
<div class="sidebar-menu">
|
||
<div class="menu-category">
|
||
<div class="menu-category-title">Dashboards</div>
|
||
<a class="menu-item active" id="menu-dashboard" onclick="switchTab('dashboard')">📊 종합 용량 및 접속자</a>
|
||
</div>
|
||
|
||
<div class="menu-category">
|
||
<div class="menu-category-title">프로젝트 관리</div>
|
||
<a class="menu-item" id="menu-project-mgmt" onclick="switchTab('project-mgmt')">🏗️ 프로젝트 관리</a>
|
||
<a class="menu-item" id="menu-banner-notice" onclick="switchTab('banner-notice')">📢 실시간 배너 공지</a>
|
||
</div>
|
||
|
||
<div class="menu-category">
|
||
<div class="menu-category-title">사용자 및 권한</div>
|
||
<a class="menu-item" id="menu-user-mgmt" onclick="switchTab('user-mgmt')">👥 사용자 관리</a>
|
||
</div>
|
||
|
||
<div class="menu-category">
|
||
<div class="menu-category-title">시스템 감사 및 환경</div>
|
||
<a class="menu-item" id="menu-audit-logs" onclick="switchTab('audit-logs')">🔎 감사 로그 조회</a>
|
||
<a class="menu-item" id="menu-delete-policy" onclick="switchTab('delete-policy')">⚙️ 보관 및 삭제 정책 설정</a>
|
||
<a class="menu-item" id="menu-code-mgmt" onclick="switchTab('code-mgmt')">🔑 공통 코드 관리</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2. Main Wrapper -->
|
||
<div class="main-wrapper">
|
||
<!-- Header -->
|
||
<div class="main-header">
|
||
<div class="page-title">
|
||
<h2 id="headerTitle">📊 종합 용량 및 접속자 현황</h2>
|
||
</div>
|
||
|
||
<div class="user-profile">
|
||
<div class="info">
|
||
<div class="name">관리자님</div>
|
||
<div class="role">Super Administrator</div>
|
||
</div>
|
||
<div class="avatar">AD</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Content Area -->
|
||
<div class="content-area">
|
||
|
||
<!-- ================= TAB: DASHBOARD ================= -->
|
||
<div id="tab-dashboard" class="tab-content active">
|
||
<div class="grid-3col">
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon" style="background-color: var(--primary-soft); color: var(--primary);">💾</div>
|
||
<div class="kpi-info">
|
||
<span class="kpi-label">전체 사용 용량</span>
|
||
<span class="kpi-value" id="kpi-storage">9.70 GB / 20 GB</span>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon" style="background-color: #ecfdf5; color: var(--success);">📡</div>
|
||
<div class="kpi-info">
|
||
<span class="kpi-label">실시간 접속자</span>
|
||
<span class="kpi-value">3 명</span>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon" style="background-color: #fef3c7; color: var(--warning);">⚡</div>
|
||
<div class="kpi-info">
|
||
<span class="kpi-label">대기 중인 압축작업</span>
|
||
<span class="kpi-value">0 건</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-2col">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">💾 현장(프로젝트)별 스토리지 사용 현황</h3>
|
||
</div>
|
||
<div id="dashboard-progress-bars" style="display: flex; flex-direction: column; gap: 16px;"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">📡 실시간 소켓 접속 현황</h3>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>사용자 ID</th>
|
||
<th>접속 IP</th>
|
||
<th>현재 조회 경로</th>
|
||
<th>작업</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>1</td>
|
||
<td><strong>test_user</strong></td>
|
||
<td>127.0.0.1</td>
|
||
<td>/PM_TEST_01/01_설계도서</td>
|
||
<td><button class="btn btn-danger btn-sm" onclick="alert('test_user 소켓 연결 강제 종료(Kick)')">강제퇴장</button></td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
<td><strong>admin</strong></td>
|
||
<td>192.168.1.100</td>
|
||
<td>/admin/queues</td>
|
||
<td>-</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================= TAB: PROJECT CRUD ================= -->
|
||
<div id="tab-project-mgmt" class="tab-content">
|
||
<div class="grid-2col">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">🏗️ 프로젝트 목록 (Read)</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="openProjectModal('create')">➕ 신규 프로젝트 등록</button>
|
||
</div>
|
||
<div class="table-wrapper" style="max-height: 550px; overflow-y: auto;">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>ID</th>
|
||
<th>현장명</th>
|
||
<th>카테고리</th>
|
||
<th>용량 제한</th>
|
||
<th>상태</th>
|
||
<th>관리</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="project-list-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Side: Assigned User List (Merged Permission Management) -->
|
||
<div class="card" style="gap: 20px;">
|
||
<div class="card-header" style="flex-wrap: wrap; gap: 10px;">
|
||
<div>
|
||
<h3 class="card-title" id="assigned-project-title">🌉 프로젝트 선택 대기 중</h3>
|
||
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-muted);" id="assigned-project-desc">좌측에서 프로젝트를 선택해 주세요.</p>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" id="btn-show-assign-modal" onclick="openAssignModal()" disabled>➕ 사용자 권한 배정</button>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 style="margin: 0 0 10px 0; font-size: 0.95rem; color: var(--text-main); display: flex; align-items: center; gap: 6px;">
|
||
🔑 참여 권한 사용자 목록 <span class="badge active" id="assigned-user-count" style="font-size: 0.75rem;">0명</span>
|
||
</h4>
|
||
<div class="table-wrapper" style="max-height: 400px; overflow-y: auto;">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>사용자 ID</th>
|
||
<th>이름</th>
|
||
<th>부서/직급</th>
|
||
<th>권한 등급</th>
|
||
<th>작업</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="assigned-user-list-body">
|
||
<tr>
|
||
<td colspan="6" style="text-align: center; color: var(--text-light); padding: 40px 0;">프로젝트를 선택하시면 배정된 사용자 목록이 여기에 표시됩니다.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================= TAB: BANNER NOTICE ================= -->
|
||
<div id="tab-banner-notice" class="tab-content">
|
||
<!-- Banner Input Form -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">📢 실시간 배너 공지사항 등록</h3>
|
||
</div>
|
||
<form class="config-form" style="gap: 20px;" onsubmit="handleBannerSubmit(event)">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="banner-project-select">공지 대상 프로젝트</label>
|
||
<select class="select-input" id="banner-project-select"></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="banner-reg-date">등록일</label>
|
||
<input type="date" class="date-input" id="banner-reg-date" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="banner-start-date">공지 시작일</label>
|
||
<input type="date" class="date-input" id="banner-start-date" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="banner-end-date">공지 종료일</label>
|
||
<input type="date" class="date-input" id="banner-end-date" required>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="banner-text">배너 공지 내용</label>
|
||
<input type="text" class="text-input" id="banner-text" required placeholder="자막 형태로 흐르게 띄워질 공지 문구 내용입니다.">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn btn-secondary" type="reset" onclick="renderBannerTab()">초기화</button>
|
||
<button class="btn btn-primary" type="submit">실시간 공지 배포 (송출)</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Banner History List (1. 요구사항 반영) -->
|
||
<div class="card" style="margin-top: 10px;">
|
||
<div class="card-header">
|
||
<h3 class="card-title">📋 배너 공지 송출 및 예약 이력 목록 (History List)</h3>
|
||
</div>
|
||
<!-- 검색조건 추가 -->
|
||
<div class="form-row" style="background-color: var(--primary-soft); padding: 16px; border-radius: var(--radius-md); gap: 16px; align-items: flex-end;">
|
||
<div class="form-group">
|
||
<label for="search-banner-status">송출 상태</label>
|
||
<select class="select-input" id="search-banner-status" style="background-color: #ffffff;">
|
||
<option value="all">전체 상태</option>
|
||
<option value="active">송출중</option>
|
||
<option value="scheduled">예약됨</option>
|
||
<option value="expired">기간 만료</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="search-banner-from">등록일 시작 (From)</label>
|
||
<input type="date" class="date-input" id="search-banner-from" style="background-color: #ffffff;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="search-banner-to">등록일 종료 (To)</label>
|
||
<input type="date" class="date-input" id="search-banner-to" style="background-color: #ffffff;">
|
||
</div>
|
||
<button class="btn btn-primary" onclick="searchBannerHistory()" style="height: 42px;">🔍 검색</button>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>등록일</th>
|
||
<th>대상 프로젝트</th>
|
||
<th>공지 사항 내용</th>
|
||
<th>공지 시작일</th>
|
||
<th>공지 종료일</th>
|
||
<th>송출 상태</th>
|
||
<th>작업</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="banner-history-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================= TAB: USER CRUD ================= -->
|
||
<div id="tab-user-mgmt" class="tab-content">
|
||
<div class="grid-2col">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">👥 사용자 계정 목록 (Read)</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="openUserModal('create')">➕ 신규 사용자 등록</button>
|
||
</div>
|
||
<div class="table-wrapper" style="max-height: 550px; overflow-y: auto;">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>아이디</th>
|
||
<th>이름</th>
|
||
<th>소속/직급</th>
|
||
<th>그룹</th>
|
||
<th>상태</th>
|
||
<th>관리</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="user-list-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Side: Selected User's Permission Projects List -->
|
||
<div class="card" style="gap: 20px;">
|
||
<div class="card-header">
|
||
<div>
|
||
<h3 class="card-title" id="user-permission-title">🔑 권한부여 프로젝트 목록</h3>
|
||
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-muted);" id="user-permission-desc">사용자 목록에서 사용자를 선택해 주세요.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="table-wrapper" style="max-height: 480px; overflow-y: auto;">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>프로젝트 ID</th>
|
||
<th>프로젝트명</th>
|
||
<th>권한 등급</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="user-permission-list-body">
|
||
<tr>
|
||
<td colspan="4" style="text-align: center; color: var(--text-light); padding: 40px 0;">사용자를 선택하시면 권한이 부여된 프로젝트 목록이 표시됩니다.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================= TAB: AUDIT LOGS ================= -->
|
||
<div id="tab-audit-logs" class="tab-content">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">🔎 시스템 민감 파일 감사 로그 조회 (tb_log)</h3>
|
||
</div>
|
||
<div class="form-row">
|
||
<input type="text" class="text-input" placeholder="유저 ID 검색...">
|
||
<select class="select-input">
|
||
<option value="all">모든 조작 액션</option>
|
||
<option value="delete">파일 삭제 (Delete)</option>
|
||
<option value="move">파일 이동 (Move)</option>
|
||
<option value="download">압축 다운로드 (Zip)</option>
|
||
</select>
|
||
<button class="btn btn-secondary" onclick="alert('필터 검색 완료')">감사 로그 필터링</button>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>일시</th>
|
||
<th>프로젝트</th>
|
||
<th>사용자 ID</th>
|
||
<th>IP</th>
|
||
<th>조작 액션</th>
|
||
<th>조작 대상 경로</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>1</td>
|
||
<td>2026-06-11 11:45:00</td>
|
||
<td>PM_TEST_01</td>
|
||
<td>test_user</td>
|
||
<td>127.0.0.1</td>
|
||
<td><span class="badge active">다운로드</span></td>
|
||
<td>/01_설계도서/교량일반도.dwg</td>
|
||
</tr>
|
||
<tr>
|
||
<td>2</td>
|
||
<td>2026-06-11 09:12:15</td>
|
||
<td>PM_TEST_01</td>
|
||
<td>admin</td>
|
||
<td>192.168.1.100</td>
|
||
<td><span class="badge danger">파일 삭제</span></td>
|
||
<td>/01_설계도서/구조계산서_old.pdf</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================= TAB: DELETE POLICY ================= -->
|
||
<div id="tab-delete-policy" class="tab-content">
|
||
<div class="grid-2col">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">⚙️ 시스템 공통 자동 삭제 정책 구성</h3>
|
||
</div>
|
||
<div class="config-form" style="gap: 20px;">
|
||
<div class="form-group">
|
||
<label>정책 활성화 토글</label>
|
||
<select class="select-input" id="policyActiveToggle" onchange="updatePolicySummary()">
|
||
<option value="true">활성화 (Active)</option>
|
||
<option value="false">일시 중지 (Inactive)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>최소 유지 파일 개수 기준</label>
|
||
<input type="number" class="text-input" id="fileThresholdInput" value="3" oninput="updatePolicySummary()">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>자동 삭제 제한 기간 (일)</label>
|
||
<input type="number" class="text-input" id="daysThresholdInput" value="15" oninput="updatePolicySummary()">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn btn-secondary" onclick="resetPolicyForm()">기본값 초기화</button>
|
||
<button class="btn btn-primary" onclick="savePolicyConfig()">변경 설정 저장</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||
<div class="card" style="background-color: var(--primary-soft); border: 1px dashed var(--primary); padding: 24px;">
|
||
<h4 style="margin: 0; color: var(--primary); font-size: 1rem;">📌 보존 정책 실시간 요약 (전체 공통)</h4>
|
||
<p style="font-size: 0.9rem; line-height: 1.6; color: var(--text-main); margin: 10px 0 0 0;" id="policySummaryText"></p>
|
||
</div>
|
||
<div class="card" style="background-color: var(--warning-soft); border: 1px solid #fde68a; padding: 20px;">
|
||
<h4 style="margin: 0; color: #78350f; font-size: 0.95rem;">⚠️ 주의사항</h4>
|
||
<p style="font-size: 0.8rem; line-height: 1.5; color: #78350f; margin: 8px 0 0 0;">
|
||
해당 설정을 저장하면 기존 프론트엔드 코드(pageRenderer.js)의 하드코딩 기준값을 대체하여, 데이터베이스의 별도 글로벌 정책 테이블(<code>tb_system_policy</code>) 필드에 동적으로 저장되며 전체 사용자 및 프로젝트 화면에 즉각 실시간 적용됩니다.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top: 10px;">
|
||
<div class="card-header">
|
||
<h3 class="card-title">🕒 정기 스케줄러 자동 삭제 처리 이력 (Auto-Clean Logs)</h3>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>자동 처리 일자</th>
|
||
<th>프로젝트 ID</th>
|
||
<th>삭제 처리 폴더 경로</th>
|
||
<th>적용 기준 (파일 수 / 보존일)</th>
|
||
<th>처리 결과</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="auto-delete-history-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ================= TAB: CODE MGMT ================= -->
|
||
<div id="tab-code-mgmt" class="tab-content" style="gap: 20px;">
|
||
<!-- Top: Code Master (대분류) -->
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h3 class="card-title">🗂️ 대분류 코드 마스터 (code_master)</h3>
|
||
<button class="btn btn-primary btn-sm" onclick="openCodeMasterModal('create')">➕ 대분류 등록</button>
|
||
</div>
|
||
<div class="table-wrapper" style="max-height: 300px; overflow-y: auto;">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>대분류 코드</th>
|
||
<th>대분류 코드명</th>
|
||
<th style="width: 70px;">사용</th>
|
||
<th>관리</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="code-master-list-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom: Code Detail (소분류) -->
|
||
<div class="card" style="gap: 20px;">
|
||
<div class="card-header" style="flex-wrap: wrap; gap: 10px;">
|
||
<div>
|
||
<h3 class="card-title" id="code-detail-title">📑 세부 코드 목록 (code_detail)</h3>
|
||
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-muted);" id="code-detail-desc">상단에서 대분류 코드를 선택해 주세요.</p>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" id="btn-show-code-detail-modal" onclick="openCodeDetailModal('create')" disabled>➕ 세부코드 등록</button>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="table-wrapper" style="max-height: 300px; overflow-y: auto;">
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 50px;">NO</th>
|
||
<th>소분류 코드</th>
|
||
<th>조합 코드 (base_code)</th>
|
||
<th>코드 명칭</th>
|
||
<th style="width: 60px;">정렬</th>
|
||
<th style="width: 70px;">사용</th>
|
||
<th>관리</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="code-detail-list-body">
|
||
<tr>
|
||
<td colspan="7" style="text-align: center; color: var(--text-light); padding: 40px 0;">상단에서 대분류 코드를 선택하시면 세부 코드 목록이 표시됩니다.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 프로젝트 등록/수정 모달 Overlay -->
|
||
<div class="modal-overlay" id="projectModalOverlay">
|
||
<div class="modal-card" style="max-width: 550px;">
|
||
<div class="modal-header">
|
||
<h3 id="project-modal-title">➕ 신규 프로젝트 등록</h3>
|
||
<button class="modal-close" onclick="closeProjectModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form class="config-form" id="project-form" style="display: flex; flex-direction: column; gap: 16px;" onsubmit="handleProjectSubmit(event)">
|
||
<div class="form-group">
|
||
<label for="form-project-id">프로젝트 ID (수정불가)</label>
|
||
<input type="text" class="text-input" id="form-project-id" required placeholder="예: PM_TEST_03">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-project-nm">프로젝트 명</label>
|
||
<input type="text" class="text-input" id="form-project-nm" required placeholder="예: 부산 외곽 터널 건설 공구">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-project-short">단축명 (Short Name)</label>
|
||
<input type="text" class="text-input" id="form-project-short" placeholder="예: 부산터널">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-project-category">카테고리</label>
|
||
<select class="select-input" id="form-project-category">
|
||
<option value="tdc" selected>TDC (tdc)</option>
|
||
<option value="gpd">GPD (gpd)</option>
|
||
<option value="bimproject">BIM 프로젝트 (bimproject)</option>
|
||
<option value="overseas">해외 프로젝트 (overseas)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="form-project-storage">스토리지 제한 (GB)</label>
|
||
<input type="number" class="text-input" id="form-project-storage" value="10" min="1" max="1000">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-project-active">운영 상태</label>
|
||
<select class="select-input" id="form-project-active">
|
||
<option value="true" selected>활성화 (Active)</option>
|
||
<option value="false">일시잠금 (Inactive)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px;">
|
||
<button class="btn btn-secondary" type="button" onclick="closeProjectModal()">취소</button>
|
||
<button class="btn btn-primary" type="submit" id="project-submit-btn">등록 하기</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 사용자 등록/수정 모달 Overlay -->
|
||
<div class="modal-overlay" id="userModalOverlay">
|
||
<div class="modal-card" style="max-width: 550px;">
|
||
<div class="modal-header">
|
||
<h3 id="user-modal-title">➕ 신규 사용자 등록</h3>
|
||
<button class="modal-close" onclick="closeUserModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form class="config-form" id="user-form" style="display: flex; flex-direction: column; gap: 16px;" onsubmit="handleUserSubmit(event)">
|
||
<div class="form-group">
|
||
<label for="form-user-id">사용자 ID (수정불가)</label>
|
||
<input type="text" class="text-input" id="form-user-id" required placeholder="예: user_id">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-user-pw">비밀번호</label>
|
||
<input type="password" class="text-input" id="form-user-pw" required placeholder="••••••••">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-user-nm">이름</label>
|
||
<input type="text" class="text-input" id="form-user-nm" required placeholder="예: 홍길동">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="form-user-company">회사명</label>
|
||
<input type="text" class="text-input" id="form-user-company" placeholder="예: 한맥기술">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-user-dept">부서</label>
|
||
<input type="text" class="text-input" id="form-user-dept" placeholder="예: 개발본부">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="form-user-position">직급</label>
|
||
<input type="text" class="text-input" id="form-user-position" placeholder="예: 부장">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-user-group">권한 그룹</label>
|
||
<select class="select-input" id="form-user-group">
|
||
<option value="worker" selected>일반 (worker)</option>
|
||
<option value="dev">개발자 (dev)</option>
|
||
<option value="super">관리자 (super)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-user-resigned">재직 상태</label>
|
||
<select class="select-input" id="form-user-resigned">
|
||
<option value="false" selected>재직 중 (Active)</option>
|
||
<option value="true">퇴직/잠금 (Locked)</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px;">
|
||
<button class="btn btn-secondary" type="button" onclick="closeUserModal()">취소</button>
|
||
<button class="btn btn-primary" type="submit" id="user-submit-btn">등록 하기</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 3. Modal Overlay for User Assignment (2. 요구사항 팝업 구현) -->
|
||
<div class="modal-overlay" id="assignModalOverlay">
|
||
<div class="modal-card">
|
||
<div class="modal-header">
|
||
<h3 id="modal-project-title">👤 사용자 배정 추가</h3>
|
||
<button class="modal-close" onclick="closeAssignModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" id="assignModalBody">
|
||
<!-- Checkboxes of unassigned users rendered here -->
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary btn-sm" onclick="closeAssignModal()">취소</button>
|
||
<button class="btn btn-primary btn-sm" onclick="submitUserAssignment()">배정 완료</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 대분류 코드 등록/수정 모달 Overlay -->
|
||
<div class="modal-overlay" id="codeMasterModalOverlay">
|
||
<div class="modal-card" style="max-width: 550px;">
|
||
<div class="modal-header">
|
||
<h3 id="code-master-modal-title">➕ 신규 대분류 등록</h3>
|
||
<button class="modal-close" onclick="closeCodeMasterModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form class="config-form" id="code-master-form" style="display: flex; flex-direction: column; gap: 16px;" onsubmit="handleCodeMasterSubmit(event)">
|
||
<div class="form-group">
|
||
<label for="form-master-code">대분류 코드 (수정불가)</label>
|
||
<input type="text" class="text-input" id="form-master-code" required placeholder="예: PROJECT_CATEGORY">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-master-name">대분류 코드명</label>
|
||
<input type="text" class="text-input" id="form-master-name" required placeholder="예: 프로젝트 카테고리">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-master-useyn">사용 여부</label>
|
||
<select class="select-input" id="form-master-useyn">
|
||
<option value="Y" selected>사용 (Y)</option>
|
||
<option value="N">미사용 (N)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-master-rmk">설명 (비고)</label>
|
||
<input type="text" class="text-input" id="form-master-rmk" placeholder="대분류 코드 설명 입력">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px;">
|
||
<button class="btn btn-secondary" type="button" onclick="closeCodeMasterModal()">취소</button>
|
||
<button class="btn btn-primary" type="submit" id="code-master-submit-btn">등록 하기</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 세부 코드 등록/수정 모달 Overlay -->
|
||
<div class="modal-overlay" id="codeDetailModalOverlay">
|
||
<div class="modal-card" style="max-width: 550px;">
|
||
<div class="modal-header">
|
||
<h3 id="code-detail-modal-title">➕ 신규 세부코드 등록</h3>
|
||
<button class="modal-close" onclick="closeCodeDetailModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form class="config-form" id="code-detail-form" style="display: flex; flex-direction: column; gap: 16px;" onsubmit="handleCodeDetailSubmit(event)">
|
||
<div class="form-group">
|
||
<label for="form-detail-maincode">대분류 코드 (고정)</label>
|
||
<input type="text" class="text-input" id="form-detail-maincode" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-detail-subcode">소분류 코드 (수정불가)</label>
|
||
<input type="text" class="text-input" id="form-detail-subcode" required placeholder="예: tdc">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-detail-name">코드 명칭 (한글)</label>
|
||
<input type="text" class="text-input" id="form-detail-name" required placeholder="예: TDC 사업부">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="form-detail-sort">정렬 순서</label>
|
||
<input type="number" class="text-input" id="form-detail-sort" value="1" min="1" max="1000">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-detail-useyn">사용 여부</label>
|
||
<select class="select-input" id="form-detail-useyn">
|
||
<option value="Y" selected>사용 (Y)</option>
|
||
<option value="N">미사용 (N)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="form-detail-rmk">설명 (비고)</label>
|
||
<input type="text" class="text-input" id="form-detail-rmk" placeholder="세부 코드 설명 입력">
|
||
</div>
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px;">
|
||
<button class="btn btn-secondary" type="button" onclick="closeCodeDetailModal()">취소</button>
|
||
<button class="btn btn-primary" type="submit" id="code-detail-submit-btn">등록 하기</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- UI Interaction Scripts -->
|
||
<script>
|
||
// --- 1. Mock DB Data ---
|
||
let projects = [
|
||
{ id: "PM_TEST_01", name: "한국 가상 교량 건설 프로젝트", shortName: "가상교량", storage: 10, active: true, category: "bimproject" },
|
||
{ id: "PM_TEST_02", name: "가상 도로 건설 프로젝트", shortName: "가상도로", storage: 10, active: true, category: "gpd" }
|
||
];
|
||
|
||
let users = [
|
||
{ id: "test_user", name: "테스트사용자", company: "한맥기술", dept: "개발본부", position: "부장", group: "dev", resigned: false },
|
||
{ id: "admin", name: "관리자", company: "한맥기술", dept: "시스템관리", position: "차장", group: "super", resigned: false },
|
||
{ id: "retire_user", name: "퇴사자A", company: "협력회사", dept: "설계팀", position: "사원", group: "worker", resigned: true }
|
||
];
|
||
|
||
// Project Permission mapping list (Mock DB)
|
||
let permissions = [
|
||
{ projectId: "PM_TEST_01", userId: "test_user", level: 7 },
|
||
{ projectId: "PM_TEST_01", userId: "admin", level: 255 },
|
||
{ projectId: "PM_TEST_02", userId: "test_user", level: 7 }
|
||
];
|
||
|
||
// Banner Notice History list (1. 요구사항 이력 데이터)
|
||
let bannerHistories = [
|
||
{ regDate: "2026-06-11", projectId: "PM_TEST_01", text: "[공지] 교량 현장 안전교육 진행 안내", startDate: "2026-06-11", endDate: "2026-06-15", status: "active" },
|
||
{ regDate: "2026-06-10", projectId: "all", text: "[긴급] 서버 스토리지 디스크 이중화 점검 공지", startDate: "2026-06-10", endDate: "2026-06-10", status: "expired" },
|
||
{ regDate: "2026-06-09", projectId: "PM_TEST_02", text: "[예약] 가상도로 시뮬레이션 서버 2차 오픈 안내", startDate: "2026-06-15", endDate: "2026-06-20", status: "scheduled" }
|
||
];
|
||
|
||
let autoDeleteLogs = [
|
||
{ date: "2026-06-11 09:15:32", projectId: "PM_TEST_01", path: "/01_설계도서/구조계산서/임시보관", rule: "3개 미만 / 15일", result: "영구 삭제 및 휴지통 이동 완료" },
|
||
{ date: "2026-06-10 14:02:11", projectId: "PM_TEST_02", path: "/자료실/dwg/일반도/임시", rule: "3개 미만 / 15일", result: "영구 삭제 및 휴지통 이동 완료" },
|
||
{ date: "2026-06-08 11:22:45", projectId: "PM_TEST_01", path: "/과업설명서/참조자료/2025버전", rule: "3개 미만 / 15일", result: "영구 삭제 및 휴지통 이동 완료" }
|
||
];
|
||
|
||
let globalPolicy = { active: true, files: 3, days: 15 };
|
||
|
||
// Common Code Management Mock DB Data
|
||
let codeMasters = [
|
||
{ code: "PROJECT_CATEGORY", name: "프로젝트 카테고리", useYn: "Y", rmk: "프로젝트 관리 화면의 Category 코드" },
|
||
{ code: "USER_GROUP", name: "사용자 그룹", useYn: "Y", rmk: "사용자 CRUD 및 권한 관리 그룹 코드" },
|
||
{ code: "NOTICE_STATUS", name: "배너 공지 상태", useYn: "Y", rmk: "실시간 배너 공지 노출 상태 코드" }
|
||
], codeDetails = [
|
||
// PROJECT_CATEGORY 관련
|
||
{ mainCode: "PROJECT_CATEGORY", subCode: "tdc", baseCode: "PROJECT_CATEGORY_tdc", name: "TDC (tdc)", sort: 1, useYn: "Y", rmk: "TDC 사업 구분" },
|
||
{ mainCode: "PROJECT_CATEGORY", subCode: "gpd", baseCode: "PROJECT_CATEGORY_gpd", name: "GPD (gpd)", sort: 2, useYn: "Y", rmk: "GPD 사업 구분" },
|
||
{ mainCode: "PROJECT_CATEGORY", subCode: "bimproject", baseCode: "PROJECT_CATEGORY_bimproject", name: "BIM 프로젝트 (bimproject)", sort: 3, useYn: "Y", rmk: "BIM 프로젝트 사업 구분" },
|
||
{ mainCode: "PROJECT_CATEGORY", subCode: "overseas", baseCode: "PROJECT_CATEGORY_overseas", name: "해외 프로젝트 (overseas)", sort: 4, useYn: "Y", rmk: "해외 프로젝트 사업 구분" },
|
||
|
||
// USER_GROUP 관련
|
||
{ mainCode: "USER_GROUP", subCode: "worker", baseCode: "USER_GROUP_worker", name: "일반 (worker)", sort: 1, useYn: "Y", rmk: "일반 업무자" },
|
||
{ mainCode: "USER_GROUP", subCode: "dev", baseCode: "USER_GROUP_dev", name: "개발자 (dev)", sort: 2, useYn: "Y", rmk: "시스템 개발자" },
|
||
{ mainCode: "USER_GROUP", subCode: "super", baseCode: "USER_GROUP_super", name: "관리자 (super)", sort: 3, useYn: "Y", rmk: "시스템 슈퍼 관리자" },
|
||
|
||
// NOTICE_STATUS 관련
|
||
{ mainCode: "NOTICE_STATUS", subCode: "active", baseCode: "NOTICE_STATUS_active", name: "송출중 (active)", sort: 1, useYn: "Y", rmk: "현재 실시간 송출 중" },
|
||
{ mainCode: "NOTICE_STATUS", subCode: "scheduled", baseCode: "NOTICE_STATUS_scheduled", name: "예약됨 (scheduled)", sort: 2, useYn: "Y", rmk: "송출 대기 및 예약 상태" },
|
||
{ mainCode: "NOTICE_STATUS", subCode: "expired", baseCode: "NOTICE_STATUS_expired", name: "기간 만료 (expired)", sort: 3, useYn: "Y", rmk: "송출 중지 또는 기간 만료" }
|
||
];
|
||
|
||
// Active State variables
|
||
let selectedPermissionProjectId = null; // Currently clicked project in permission tab
|
||
let selectedUserId = null; // Currently clicked user in user tab
|
||
let selectedCodeMasterId = null; // Currently clicked code master in code tab
|
||
|
||
// --- 2. Tab Menu Controller ---
|
||
function switchTab(tabId) {
|
||
const menuItems = document.querySelectorAll('.menu-item');
|
||
menuItems.forEach(item => item.classList.remove('active'));
|
||
const activeMenu = document.getElementById(`menu-${tabId}`);
|
||
if (activeMenu) activeMenu.classList.add('active');
|
||
|
||
const tabContents = document.querySelectorAll('.tab-content');
|
||
tabContents.forEach(content => content.classList.remove('active'));
|
||
const activeTab = document.getElementById(`tab-${tabId}`);
|
||
if (activeTab) activeTab.classList.add('active');
|
||
|
||
const headerTitle = document.getElementById('headerTitle');
|
||
const titles = {
|
||
'dashboard': '📊 종합 용량 및 접속자 현황',
|
||
'project-mgmt': '🏗️ 프로젝트 관리',
|
||
'banner-notice': '📢 실시간 배너 공지 관리',
|
||
'user-mgmt': '👥 사용자 관리',
|
||
'audit-logs': '🔎 시스템 민감 파일 감사 로그 조회 (tb_log)',
|
||
'delete-policy': '⚙️ 자동 보관 및 파일 삭제 정책 설정',
|
||
'code-mgmt': '🔑 공통 코드 관리'
|
||
};
|
||
headerTitle.textContent = titles[tabId] || '관리 시스템';
|
||
|
||
if (tabId === 'dashboard') renderDashboard();
|
||
if (tabId === 'project-mgmt') {
|
||
renderProjects();
|
||
renderAssignedUsers();
|
||
}
|
||
if (tabId === 'banner-notice') renderBannerTab();
|
||
if (tabId === 'user-mgmt') {
|
||
renderUsers();
|
||
renderUserPermissions();
|
||
}
|
||
if (tabId === 'delete-policy') renderPolicyTab();
|
||
if (tabId === 'code-mgmt') renderCodeMgmt();
|
||
}
|
||
|
||
// --- 3. Dashboard Renderer ---
|
||
function renderDashboard() {
|
||
let totalStorage = projects.reduce((acc, curr) => acc + curr.storage, 0);
|
||
let used = 9.7;
|
||
document.getElementById('kpi-storage').textContent = `${used.toFixed(2)} GB / ${totalStorage} GB`;
|
||
|
||
const progressContainer = document.getElementById('dashboard-progress-bars');
|
||
progressContainer.innerHTML = '';
|
||
|
||
projects.forEach(p => {
|
||
let mockUsed = p.id === "PM_TEST_01" ? 6.5 : (p.id === "PM_TEST_02" ? 3.2 : 0);
|
||
let mockFiles = p.id === "PM_TEST_01" ? 124 : (p.id === "PM_TEST_02" ? 45 : 0);
|
||
let pct = p.storage > 0 ? (mockUsed / p.storage) * 100 : 0;
|
||
|
||
let barHtml = `
|
||
<div class="progress-bar-container">
|
||
<div style="width: 160px; font-size: 0.85rem; font-weight: 600;">${p.name} (${p.id})</div>
|
||
<div class="progress-bar-bg">
|
||
<div class="progress-bar-fill" style="width: ${pct}%;"></div>
|
||
</div>
|
||
<div style="width: 180px; font-size: 0.85rem; text-align: right; color: var(--text-muted);">${mockUsed} GB (${pct.toFixed(0)}%) / <strong>${mockFiles}개</strong></div>
|
||
</div>
|
||
`;
|
||
progressContainer.insertAdjacentHTML('beforeend', barHtml);
|
||
});
|
||
}
|
||
|
||
// --- 4. Project CRUD Controller ---
|
||
function renderProjects() {
|
||
const listBody = document.getElementById('project-list-body');
|
||
listBody.innerHTML = '';
|
||
|
||
projects.forEach((p, idx) => {
|
||
let catMap = {
|
||
'tdc': 'TDC',
|
||
'gpd': 'GPD',
|
||
'bimproject': 'BIM 프로젝트',
|
||
'overseas': '해외 프로젝트'
|
||
};
|
||
let catName = catMap[p.category] || p.category || '-';
|
||
|
||
let isSelected = selectedPermissionProjectId === p.id;
|
||
let row = `
|
||
<tr class="clickable ${isSelected ? 'selected' : ''}" onclick="selectPermissionProject('${p.id}')">
|
||
<td>${idx + 1}</td>
|
||
<td><strong>${p.id}</strong></td>
|
||
<td>${p.name}</td>
|
||
<td><span class="badge active" style="background-color: var(--primary-soft); color: var(--primary);">${catName}</span></td>
|
||
<td>${p.storage} GB</td>
|
||
<td><span class="badge ${p.active ? 'active' : 'inactive'}">${p.active ? '활성' : '잠금'}</span></td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openProjectModal('update', '${p.id}')">수정</button>
|
||
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteProject('${p.id}')">삭제</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
listBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
|
||
syncProjectDropdowns();
|
||
}
|
||
|
||
function openProjectModal(mode, id = null) {
|
||
const overlay = document.getElementById('projectModalOverlay');
|
||
const title = document.getElementById('project-modal-title');
|
||
const submitBtn = document.getElementById('project-submit-btn');
|
||
|
||
const idInput = document.getElementById('form-project-id');
|
||
const nmInput = document.getElementById('form-project-nm');
|
||
const shortInput = document.getElementById('form-project-short');
|
||
const catSelect = document.getElementById('form-project-category');
|
||
const storageInput = document.getElementById('form-project-storage');
|
||
const activeSelect = document.getElementById('form-project-active');
|
||
|
||
if (mode === 'create') {
|
||
title.textContent = "➕ 신규 프로젝트 등록";
|
||
submitBtn.textContent = "등록 하기";
|
||
idInput.disabled = false;
|
||
|
||
idInput.value = "";
|
||
nmInput.value = "";
|
||
shortInput.value = "";
|
||
catSelect.value = "tdc";
|
||
storageInput.value = 10;
|
||
activeSelect.value = "true";
|
||
} else if (mode === 'update') {
|
||
let p = projects.find(proj => proj.id === id);
|
||
if (!p) return;
|
||
|
||
title.textContent = "📝 프로젝트 설정 수정";
|
||
submitBtn.textContent = "설정 저장";
|
||
idInput.disabled = true;
|
||
|
||
idInput.value = p.id;
|
||
nmInput.value = p.name;
|
||
shortInput.value = p.shortName || "";
|
||
catSelect.value = p.category || "tdc";
|
||
storageInput.value = p.storage;
|
||
activeSelect.value = p.active ? "true" : "false";
|
||
}
|
||
|
||
overlay.style.display = 'flex';
|
||
}
|
||
|
||
function closeProjectModal() {
|
||
document.getElementById('projectModalOverlay').style.display = 'none';
|
||
}
|
||
|
||
function deleteProject(id) {
|
||
if (confirm(`프로젝트 [${id}]를 삭제하시겠습니까?\n스토리지 내 모든 물리 파일 및 메타데이터가 영구 삭제됩니다.`)) {
|
||
projects = projects.filter(p => p.id !== id);
|
||
permissions = permissions.filter(perm => perm.projectId !== id); // clean up permissions
|
||
delete policySettings[id];
|
||
|
||
if (selectedPermissionProjectId === id) {
|
||
selectedPermissionProjectId = null;
|
||
}
|
||
|
||
renderProjects();
|
||
renderAssignedUsers();
|
||
alert('프로젝트가 삭제되었습니다.');
|
||
}
|
||
}
|
||
|
||
function handleProjectSubmit(e) {
|
||
e.preventDefault();
|
||
const idInput = document.getElementById('form-project-id');
|
||
const nmInput = document.getElementById('form-project-nm').value;
|
||
const shortInput = document.getElementById('form-project-short').value;
|
||
const catInput = document.getElementById('form-project-category').value;
|
||
const storageInput = parseInt(document.getElementById('form-project-storage').value);
|
||
const activeInput = document.getElementById('form-project-active').value === "true";
|
||
|
||
let pIndex = projects.findIndex(proj => proj.id === idInput.value);
|
||
|
||
if (idInput.disabled) { // Update Mode
|
||
if (pIndex > -1) {
|
||
projects[pIndex].name = nmInput;
|
||
projects[pIndex].shortName = shortInput;
|
||
projects[pIndex].category = catInput;
|
||
projects[pIndex].storage = storageInput;
|
||
projects[pIndex].active = activeInput;
|
||
alert('프로젝트 정보가 수정되었습니다.');
|
||
}
|
||
} else { // Create Mode
|
||
if (pIndex > -1) {
|
||
alert('이미 등록된 프로젝트 ID입니다.');
|
||
return;
|
||
}
|
||
projects.push({
|
||
id: idInput.value,
|
||
name: nmInput,
|
||
shortName: shortInput,
|
||
category: catInput,
|
||
storage: storageInput,
|
||
active: activeInput
|
||
});
|
||
alert('신규 프로젝트가 등록되었습니다.');
|
||
}
|
||
|
||
closeProjectModal();
|
||
renderProjects();
|
||
}
|
||
|
||
function syncProjectDropdowns() {
|
||
const dropdowns = ['banner-project-select', 'permission-project-select'];
|
||
dropdowns.forEach(ddId => {
|
||
const select = document.getElementById(ddId);
|
||
if (!select) return;
|
||
const savedVal = select.value;
|
||
select.innerHTML = '';
|
||
|
||
if (ddId === 'banner-project-select') {
|
||
select.insertAdjacentHTML('beforeend', `<option value="all">전체 프로젝트 동시 송출</option>`);
|
||
}
|
||
|
||
projects.forEach(p => {
|
||
select.insertAdjacentHTML('beforeend', `<option value="${p.id}">${p.name} (${p.id})</option>`);
|
||
});
|
||
|
||
select.value = savedVal || select.options[0]?.value;
|
||
});
|
||
}
|
||
|
||
// --- 5. Banner Notice Controller (1. 요구사항 반영) ---
|
||
function renderBannerTab() {
|
||
syncProjectDropdowns();
|
||
|
||
// Set Auto Registration Date (Today)
|
||
const today = new Date().toISOString().split('T')[0];
|
||
document.getElementById('banner-reg-date').value = today;
|
||
document.getElementById('banner-start-date').value = today;
|
||
|
||
// Set default end date (today + 7 days)
|
||
const nextWeek = new Date();
|
||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||
document.getElementById('banner-end-date').value = nextWeek.toISOString().split('T')[0];
|
||
|
||
// Clear search filter inputs
|
||
document.getElementById('search-banner-status').value = 'all';
|
||
document.getElementById('search-banner-from').value = '';
|
||
document.getElementById('search-banner-to').value = '';
|
||
|
||
renderBannerHistory();
|
||
}
|
||
|
||
function renderBannerHistory(dataList = null) {
|
||
const historyBody = document.getElementById('banner-history-body');
|
||
historyBody.innerHTML = '';
|
||
|
||
// Map each banner history with its original index
|
||
let itemsToRender = (dataList || bannerHistories).map((b, idx) => {
|
||
// If dataList is provided, we need to find the actual original index in bannerHistories
|
||
let originalIndex = dataList ? bannerHistories.findIndex(orig => orig === b) : idx;
|
||
return { ...b, originalIndex };
|
||
});
|
||
|
||
itemsToRender.forEach((b, index) => {
|
||
let statusBadge = '';
|
||
let actionBtn = '';
|
||
|
||
// Recalculate status based on current date for simulation safety if needed
|
||
if (b.status === 'active') {
|
||
statusBadge = '<span class="badge active">송출중</span>';
|
||
actionBtn = `<button class="btn btn-danger btn-sm" onclick="stopSpecificBanner(${b.originalIndex})">송출 중지</button>`;
|
||
} else if (b.status === 'scheduled') {
|
||
statusBadge = '<span class="badge warning">예약됨</span>';
|
||
actionBtn = `<button class="btn btn-danger btn-sm" onclick="stopSpecificBanner(${b.originalIndex})">송출 중지</button>`;
|
||
} else if (b.status === 'expired') {
|
||
statusBadge = '<span class="badge inactive">기간 만료</span>';
|
||
actionBtn = `<button class="btn btn-secondary btn-sm" disabled>중지 완료</button>`;
|
||
}
|
||
|
||
let projName = b.projectId === 'all' ? '전체 프로젝트' : b.projectId;
|
||
|
||
let row = `
|
||
<tr>
|
||
<td>${index + 1}</td>
|
||
<td>${b.regDate}</td>
|
||
<td><strong>${projName}</strong></td>
|
||
<td>${b.text}</td>
|
||
<td>${b.startDate}</td>
|
||
<td>${b.endDate}</td>
|
||
<td>${statusBadge}</td>
|
||
<td>${actionBtn}</td>
|
||
</tr>
|
||
`;
|
||
historyBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
}
|
||
|
||
function searchBannerHistory() {
|
||
const statusFilter = document.getElementById('search-banner-status').value;
|
||
const fromFilter = document.getElementById('search-banner-from').value;
|
||
const toFilter = document.getElementById('search-banner-to').value;
|
||
|
||
let filtered = bannerHistories.filter(b => {
|
||
// 1. Status Filter
|
||
if (statusFilter !== 'all' && b.status !== statusFilter) {
|
||
return false;
|
||
}
|
||
// 2. Date From Filter
|
||
if (fromFilter && b.regDate < fromFilter) {
|
||
return false;
|
||
}
|
||
// 3. Date To Filter
|
||
if (toFilter && b.regDate > toFilter) {
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
renderBannerHistory(filtered);
|
||
}
|
||
|
||
function handleBannerSubmit(e) {
|
||
e.preventDefault();
|
||
const projId = document.getElementById('banner-project-select').value;
|
||
const text = document.getElementById('banner-text').value;
|
||
const startDate = document.getElementById('banner-start-date').value;
|
||
const endDate = document.getElementById('banner-end-date').value;
|
||
const regDate = document.getElementById('banner-reg-date').value;
|
||
|
||
// Determine status
|
||
const todayStr = new Date().toISOString().split('T')[0];
|
||
let status = 'active';
|
||
if (startDate > todayStr) status = 'scheduled';
|
||
else if (endDate < todayStr) status = 'expired';
|
||
|
||
// Add new history record (Read/Create)
|
||
bannerHistories.unshift({ regDate, projectId: projId, text, startDate, endDate, status });
|
||
|
||
renderBannerHistory();
|
||
alert('실시간 공지가 성공적으로 배포/등록 되었습니다.');
|
||
document.getElementById('banner-text').value = '';
|
||
}
|
||
|
||
function stopSpecificBanner(originalIndex) {
|
||
if (originalIndex > -1 && originalIndex < bannerHistories.length) {
|
||
bannerHistories[originalIndex].status = 'expired';
|
||
searchBannerHistory(); // update the list keeping current filters
|
||
alert('해당 배너 공지의 송출이 중단(만료) 처리되었습니다.');
|
||
}
|
||
}
|
||
|
||
// --- 6. User CRUD Controller ---
|
||
function renderUsers() {
|
||
const listBody = document.getElementById('user-list-body');
|
||
listBody.innerHTML = '';
|
||
|
||
users.forEach((u, idx) => {
|
||
let isSelected = selectedUserId === u.id;
|
||
let row = `
|
||
<tr class="clickable ${isSelected ? 'selected' : ''}" onclick="selectUserForPermissions('${u.id}')">
|
||
<td>${idx + 1}</td>
|
||
<td><strong>${u.id}</strong></td>
|
||
<td>${u.name}</td>
|
||
<td>${u.company} / ${u.dept} ${u.position}</td>
|
||
<td>${u.group === 'super' ? '관리자(super)' : (u.group === 'dev' ? '개발자(dev)' : '일반(worker)')}</td>
|
||
<td><span class="badge ${u.resigned ? 'danger' : 'active'}">${u.resigned ? '퇴직/잠금' : '재직중'}</span></td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openUserModal('update', '${u.id}')">수정</button>
|
||
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteUser('${u.id}')">삭제</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
listBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
syncUserDropdowns();
|
||
renderUserPermissions();
|
||
}
|
||
|
||
function openUserModal(mode, id = null) {
|
||
const overlay = document.getElementById('userModalOverlay');
|
||
const title = document.getElementById('user-modal-title');
|
||
const submitBtn = document.getElementById('user-submit-btn');
|
||
|
||
const idInput = document.getElementById('form-user-id');
|
||
const pwInput = document.getElementById('form-user-pw');
|
||
const nmInput = document.getElementById('form-user-nm');
|
||
const compInput = document.getElementById('form-user-company');
|
||
const deptInput = document.getElementById('form-user-dept');
|
||
const posInput = document.getElementById('form-user-position');
|
||
const grpSelect = document.getElementById('form-user-group');
|
||
const resignSelect = document.getElementById('form-user-resigned');
|
||
|
||
if (mode === 'create') {
|
||
title.textContent = "➕ 신규 사용자 등록";
|
||
submitBtn.textContent = "등록 하기";
|
||
idInput.disabled = false;
|
||
pwInput.required = true;
|
||
|
||
idInput.value = "";
|
||
pwInput.value = "";
|
||
nmInput.value = "";
|
||
compInput.value = "";
|
||
deptInput.value = "";
|
||
posInput.value = "";
|
||
grpSelect.value = "worker";
|
||
resignSelect.value = "false";
|
||
} else if (mode === 'update') {
|
||
let u = users.find(usr => usr.id === id);
|
||
if (!u) return;
|
||
|
||
title.textContent = "📝 사용자 정보 수정";
|
||
submitBtn.textContent = "정보 저장";
|
||
idInput.disabled = true;
|
||
pwInput.required = false;
|
||
|
||
idInput.value = u.id;
|
||
pwInput.value = "";
|
||
nmInput.value = u.name;
|
||
compInput.value = u.company || "";
|
||
deptInput.value = u.dept || "";
|
||
posInput.value = u.position || "";
|
||
grpSelect.value = u.group;
|
||
resignSelect.value = u.resigned ? "true" : "false";
|
||
}
|
||
|
||
overlay.style.display = 'flex';
|
||
}
|
||
|
||
function closeUserModal() {
|
||
document.getElementById('userModalOverlay').style.display = 'none';
|
||
}
|
||
|
||
function selectUserForPermissions(userId) {
|
||
selectedUserId = userId;
|
||
renderUsers();
|
||
}
|
||
|
||
function renderUserPermissions() {
|
||
const titleEl = document.getElementById('user-permission-title');
|
||
const descEl = document.getElementById('user-permission-desc');
|
||
const listBody = document.getElementById('user-permission-list-body');
|
||
|
||
if (!selectedUserId) {
|
||
titleEl.textContent = "🔑 권한부여 프로젝트 목록";
|
||
descEl.textContent = "사용자 목록에서 사용자를 선택해 주세요.";
|
||
listBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="4" style="text-align: center; color: var(--text-light); padding: 40px 0;">사용자를 선택하시면 권한이 부여된 프로젝트 목록이 표시됩니다.</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let u = users.find(usr => usr.id === selectedUserId);
|
||
titleEl.textContent = `🔑 [${u.name} (${u.id})] 참여 프로젝트`;
|
||
descEl.textContent = "이 사용자가 권한을 부여받아 접근 가능한 프로젝트 및 등급 목록입니다.";
|
||
|
||
let userPerms = permissions.filter(p => p.userId === selectedUserId);
|
||
|
||
if (userPerms.length === 0) {
|
||
listBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="4" style="text-align: center; color: var(--text-light); padding: 40px 0;">이 사용자에 부여된 프로젝트 권한이 없습니다.</td>
|
||
</tr>
|
||
`;
|
||
} else {
|
||
listBody.innerHTML = '';
|
||
userPerms.forEach((perm, idx) => {
|
||
let p = projects.find(proj => proj.id === perm.projectId);
|
||
let pName = p ? p.name : '알 수 없는 프로젝트';
|
||
|
||
let lvMap = {
|
||
255: 'Owner / Admin (255)',
|
||
7: 'Sub-Master / 관리 (7)',
|
||
4: 'Worker / 쓰기 (4)',
|
||
1: 'Viewer / 읽기 (1)'
|
||
};
|
||
let lvName = lvMap[perm.level] || `기타 레벨 (${perm.level})`;
|
||
|
||
let row = `
|
||
<tr>
|
||
<td>${idx + 1}</td>
|
||
<td><strong>${perm.projectId}</strong></td>
|
||
<td>${pName}</td>
|
||
<td><span class="badge active" style="background-color: var(--primary-soft); color: var(--primary);">${lvName}</span></td>
|
||
</tr>
|
||
`;
|
||
listBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
}
|
||
}
|
||
|
||
function deleteUser(id) {
|
||
if (confirm(`사용자 계정 [${id}]를 정말로 영구 삭제하시겠습니까?\n해당 유저가 등록한 프로젝트 권한 정보도 모두 삭제됩니다.`)) {
|
||
users = users.filter(u => u.id !== id);
|
||
permissions = permissions.filter(p => p.userId !== id); // clean up permission assign
|
||
|
||
if (selectedUserId === id) {
|
||
selectedUserId = null;
|
||
}
|
||
|
||
renderUsers();
|
||
alert('사용자가 삭제되었습니다.');
|
||
}
|
||
}
|
||
|
||
function handleUserSubmit(e) {
|
||
e.preventDefault();
|
||
const idInput = document.getElementById('form-user-id');
|
||
const pwInput = document.getElementById('form-user-pw').value;
|
||
const nmInput = document.getElementById('form-user-nm').value;
|
||
const compInput = document.getElementById('form-user-company').value;
|
||
const deptInput = document.getElementById('form-user-dept').value;
|
||
const posInput = document.getElementById('form-user-position').value;
|
||
const grpInput = document.getElementById('form-user-group').value;
|
||
const resignInput = document.getElementById('form-user-resigned').value === "true";
|
||
|
||
let uIndex = users.findIndex(usr => usr.id === idInput.value);
|
||
|
||
if (idInput.disabled) { // Update Mode
|
||
if (uIndex > -1) {
|
||
users[uIndex].name = nmInput;
|
||
users[uIndex].company = compInput;
|
||
users[uIndex].dept = deptInput;
|
||
users[uIndex].position = posInput;
|
||
users[uIndex].group = grpInput;
|
||
users[uIndex].resigned = resignInput;
|
||
if (pwInput !== "") {
|
||
users[uIndex].pw = pwInput;
|
||
}
|
||
alert('사용자 정보가 정상 수정되었습니다.');
|
||
}
|
||
} else { // Create Mode
|
||
if (uIndex > -1) {
|
||
alert('이미 존재하는 계정 ID입니다.');
|
||
return;
|
||
}
|
||
users.push({
|
||
id: idInput.value,
|
||
pw: pwInput,
|
||
name: nmInput,
|
||
company: compInput,
|
||
dept: deptInput,
|
||
position: posInput,
|
||
group: grpInput,
|
||
resigned: resignInput
|
||
});
|
||
alert('신규 사용자가 성공적으로 생성되었습니다.');
|
||
}
|
||
|
||
closeUserModal();
|
||
renderUsers();
|
||
}
|
||
|
||
function syncUserDropdowns() {
|
||
const select = document.getElementById('permission-user-select');
|
||
if (!select) return;
|
||
const savedVal = select.value;
|
||
select.innerHTML = '';
|
||
|
||
users.forEach(u => {
|
||
select.insertAdjacentHTML('beforeend', `<option value="${u.id}">${u.name} (${u.id})</option>`);
|
||
});
|
||
|
||
select.value = savedVal || select.options[0]?.value;
|
||
}
|
||
|
||
function selectPermissionProject(projectId) {
|
||
selectedPermissionProjectId = projectId;
|
||
renderProjects();
|
||
renderAssignedUsers();
|
||
}
|
||
|
||
function renderAssignedUsers() {
|
||
const userListBody = document.getElementById('assigned-user-list-body');
|
||
const assignedTitle = document.getElementById('assigned-project-title');
|
||
const assignedDesc = document.getElementById('assigned-project-desc');
|
||
const assignBtn = document.getElementById('btn-show-assign-modal');
|
||
const assignedCount = document.getElementById('assigned-user-count');
|
||
|
||
if (!selectedPermissionProjectId) {
|
||
assignedTitle.textContent = "🌉 프로젝트 선택 대기 중";
|
||
assignedDesc.textContent = "좌측에서 프로젝트를 선택해 주세요.";
|
||
if (assignBtn) assignBtn.disabled = true;
|
||
if (assignedCount) assignedCount.textContent = "0명";
|
||
userListBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" style="text-align: center; color: var(--text-light); padding: 40px 0;">프로젝트를 선택하시면 배정된 사용자 목록이 여기에 표시됩니다.</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Project details load
|
||
let activeProj = projects.find(p => p.id === selectedPermissionProjectId);
|
||
assignedTitle.textContent = `🔑 ${activeProj.name} (${activeProj.id})`;
|
||
assignedDesc.textContent = "이 현장에 참여 권한이 부여된 사용자 목록 및 등급을 관리합니다.";
|
||
if (assignBtn) assignBtn.disabled = false;
|
||
|
||
// 1. Load assigned users list
|
||
let assignedList = permissions.filter(perm => perm.projectId === selectedPermissionProjectId);
|
||
if (assignedCount) assignedCount.textContent = `${assignedList.length}명`;
|
||
|
||
if (assignedList.length === 0) {
|
||
userListBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" style="text-align: center; color: var(--text-light); padding: 40px 0;">이 프로젝트에 배정된 사용자가 아직 없습니다. 사용자 추가를 해 주세요.</td>
|
||
</tr>
|
||
`;
|
||
} else {
|
||
userListBody.innerHTML = '';
|
||
assignedList.forEach((perm, idx) => {
|
||
let usr = users.find(u => u.id === perm.userId);
|
||
if (!usr) return;
|
||
|
||
let options = `
|
||
<select class="select-input btn-sm" style="width: auto; background-color: #ffffff; padding: 4px 8px;" onchange="updateUserPermissionLevel('${perm.projectId}', '${perm.userId}', this.value)">
|
||
<option value="255" ${perm.level === 255 ? 'selected' : ''}>Owner / Admin (255)</option>
|
||
<option value="7" ${perm.level === 7 ? 'selected' : ''}>Sub-Master / 관리 (7)</option>
|
||
<option value="4" ${perm.level === 4 ? 'selected' : ''}>Worker / 쓰기 (4)</option>
|
||
<option value="1" ${perm.level === 1 ? 'selected' : ''}>Viewer / 읽기 (1)</option>
|
||
</select>
|
||
`;
|
||
|
||
let row = `
|
||
<tr>
|
||
<td>${idx + 1}</td>
|
||
<td><strong>${usr.id}</strong></td>
|
||
<td>${usr.name}</td>
|
||
<td>${usr.company} / ${usr.dept}</td>
|
||
<td>${options}</td>
|
||
<td>
|
||
<button class="btn btn-danger btn-sm" onclick="removeUserFromProject('${perm.projectId}', '${perm.userId}')">배정 제외</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
userListBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Live Permission level update from selector inside table
|
||
function updateUserPermissionLevel(projectId, userId, newLevel) {
|
||
let perm = permissions.find(p => p.projectId === projectId && p.userId === userId);
|
||
if (perm) {
|
||
perm.level = parseInt(newLevel);
|
||
console.log(`Permission level changed: ${projectId} - ${userId} to ${newLevel}`);
|
||
}
|
||
}
|
||
|
||
// Remove user assignment
|
||
function removeUserFromProject(projectId, userId) {
|
||
if (confirm(`사용자 [${userId}]의 프로젝트 접근 권한을 배정 해제하시겠습니까?`)) {
|
||
permissions = permissions.filter(p => !(p.projectId === projectId && p.userId === userId));
|
||
renderAssignedUsers();
|
||
}
|
||
}
|
||
|
||
// --- 8. Modal User Assign Control ---
|
||
function openAssignModal() {
|
||
if (!selectedPermissionProjectId) return;
|
||
const modal = document.getElementById('assignModalOverlay');
|
||
const modalTitle = document.getElementById('modal-project-title');
|
||
const modalBody = document.getElementById('assignModalBody');
|
||
|
||
let activeProj = projects.find(p => p.id === selectedPermissionProjectId);
|
||
modalTitle.textContent = `➕ [${activeProj.shortName || activeProj.name}] 현장 참여자 추가 배정`;
|
||
|
||
// Filter out users who are already assigned to this project
|
||
let assignedUserIds = permissions.filter(p => p.projectId === selectedPermissionProjectId).map(p => p.userId);
|
||
let unassignedUsers = users.filter(u => !assignedUserIds.includes(u.id) && !u.resigned);
|
||
|
||
modalBody.innerHTML = '';
|
||
if (unassignedUsers.length === 0) {
|
||
modalBody.innerHTML = '<p style="text-align: center; color: var(--text-light); padding: 20px 0;">이 현장에 배정 가능한 대기 중인 사용자가 없습니다.</p>';
|
||
} else {
|
||
unassignedUsers.forEach(u => {
|
||
let itemHtml = `
|
||
<label class="check-list-item">
|
||
<input type="checkbox" name="assignUserCheck" value="${u.id}">
|
||
<div class="user-info">
|
||
<span class="user-info-name">${u.name} (${u.id})</span>
|
||
<span class="user-info-desc">${u.company} / ${u.dept} ${u.position}</span>
|
||
</div>
|
||
</label>
|
||
`;
|
||
modalBody.insertAdjacentHTML('beforeend', itemHtml);
|
||
});
|
||
}
|
||
|
||
modal.style.display = 'flex';
|
||
}
|
||
|
||
function closeAssignModal() {
|
||
document.getElementById('assignModalOverlay').style.display = 'none';
|
||
}
|
||
|
||
function submitUserAssignment() {
|
||
const checkedBoxes = document.querySelectorAll('input[name="assignUserCheck"]:checked');
|
||
if (checkedBoxes.length === 0) {
|
||
alert('배정할 사용자를 1명 이상 선택해 주세요.');
|
||
return;
|
||
}
|
||
|
||
// Push assignments to permissions array (Mock DB Update)
|
||
checkedBoxes.forEach(box => {
|
||
permissions.push({
|
||
projectId: selectedPermissionProjectId,
|
||
userId: box.value,
|
||
level: 7 // default General Sub-master Level
|
||
});
|
||
});
|
||
|
||
closeAssignModal();
|
||
renderAssignedUsers();
|
||
alert('사용자 권한 배정이 정상 추가되었습니다.');
|
||
}
|
||
|
||
// --- 9. Policy & Auto-Delete History Controller ---
|
||
function renderPolicyTab() {
|
||
renderPolicyHistory();
|
||
|
||
// Set fields with global policy
|
||
document.getElementById('policyActiveToggle').value = globalPolicy.active ? "true" : "false";
|
||
document.getElementById('fileThresholdInput').value = globalPolicy.files;
|
||
document.getElementById('daysThresholdInput').value = globalPolicy.days;
|
||
|
||
updatePolicySummary();
|
||
}
|
||
|
||
function renderPolicyHistory() {
|
||
const logBody = document.getElementById('auto-delete-history-body');
|
||
logBody.innerHTML = '';
|
||
|
||
autoDeleteLogs.forEach((log, idx) => {
|
||
let row = `
|
||
<tr>
|
||
<td>${idx + 1}</td>
|
||
<td>${log.date}</td>
|
||
<td><strong>${log.projectId}</strong></td>
|
||
<td><code>${log.path}</code></td>
|
||
<td>${log.rule}</td>
|
||
<td><span class="badge active">${log.result}</span></td>
|
||
</tr>
|
||
`;
|
||
logBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
}
|
||
|
||
function updatePolicySummary() {
|
||
const active = document.getElementById('policyActiveToggle').value === "true";
|
||
const files = document.getElementById('fileThresholdInput').value;
|
||
const days = document.getElementById('daysThresholdInput').value;
|
||
const summaryText = document.getElementById('policySummaryText');
|
||
|
||
if (!active) {
|
||
summaryText.innerHTML = `<span style="color: var(--text-light); font-style: italic;">시스템 공통 자동 삭제 정책 작동이 중지된 상태입니다. 보관 수명 기준이 도달하더라도 임시 파일이 자동으로 삭제되지 않습니다.</span>`;
|
||
return;
|
||
}
|
||
|
||
const countdownDays = days - 1;
|
||
summaryText.innerHTML = `현재 설정에 따라 전체 프로젝트의 폴더 내부의 총 파일 개수가 <strong>${files}개 미만</strong>이 된 시점부터 <strong>${days}일</strong> 동안 상태가 유지되면, 사용자가 아카이브 진입 시 해당 폴더 옆에 <strong>D-${countdownDays}</strong> 타이머가 시작되고 만료 즉시 <strong>자동 삭제</strong> 처리되어 폴더 내 파일은 휴지통으로 이동합니다.`;
|
||
}
|
||
|
||
function resetPolicyForm() {
|
||
document.getElementById('policyActiveToggle').value = "true";
|
||
document.getElementById('fileThresholdInput').value = 3;
|
||
document.getElementById('daysThresholdInput').value = 15;
|
||
updatePolicySummary();
|
||
}
|
||
|
||
function savePolicyConfig() {
|
||
const active = document.getElementById('policyActiveToggle').value === "true";
|
||
const files = document.getElementById('fileThresholdInput').value;
|
||
const days = document.getElementById('daysThresholdInput').value;
|
||
|
||
globalPolicy = { active, files: parseInt(files), days: parseInt(days) };
|
||
|
||
const now = new Date();
|
||
const dateStr = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
|
||
|
||
autoDeleteLogs.unshift({
|
||
date: dateStr,
|
||
projectId: 'SYSTEM',
|
||
path: '(공통 정책 값 갱신)',
|
||
rule: `${files}개 미만 / ${days}일`,
|
||
result: active ? "활성화 및 정책 동기화 완료" : "비활성화 및 정책 동기화 완료"
|
||
});
|
||
renderPolicyHistory();
|
||
|
||
alert(`[시스템 공통 정책] 설정 저장 완료!\n- 활성화 여부: ${active ? '예' : '아니오'}\n- 기준 파일수: ${files}개 미만\n- 제한 일수: ${days}일`);
|
||
}
|
||
|
||
// --- 10. Common Code Management Controller ---
|
||
function renderCodeMgmt() {
|
||
renderCodeMasters();
|
||
renderCodeDetails();
|
||
}
|
||
|
||
function renderCodeMasters() {
|
||
const listBody = document.getElementById('code-master-list-body');
|
||
listBody.innerHTML = '';
|
||
|
||
codeMasters.forEach((cm, idx) => {
|
||
let isSelected = selectedCodeMasterId === cm.code;
|
||
let row = `
|
||
<tr class="clickable ${isSelected ? 'selected' : ''}" onclick="selectCodeMaster('${cm.code}')">
|
||
<td>${idx + 1}</td>
|
||
<td><strong>${cm.code}</strong></td>
|
||
<td>${cm.name}</td>
|
||
<td><span class="badge ${cm.useYn === 'Y' ? 'active' : 'inactive'}">${cm.useYn === 'Y' ? '사용' : '미사용'}</span></td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openCodeMasterModal('update', '${cm.code}')">수정</button>
|
||
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteCodeMaster('${cm.code}')">삭제</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
listBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
}
|
||
|
||
function selectCodeMaster(code) {
|
||
selectedCodeMasterId = code;
|
||
renderCodeMasters();
|
||
renderCodeDetails();
|
||
}
|
||
|
||
function renderCodeDetails() {
|
||
const listBody = document.getElementById('code-detail-list-body');
|
||
const titleEl = document.getElementById('code-detail-title');
|
||
const descEl = document.getElementById('code-detail-desc');
|
||
const addBtn = document.getElementById('btn-show-code-detail-modal');
|
||
|
||
if (!selectedCodeMasterId) {
|
||
titleEl.textContent = '📑 세부 코드 목록 (code_detail)';
|
||
descEl.textContent = '상단에서 대분류 코드를 선택해 주세요.';
|
||
if (addBtn) addBtn.disabled = true;
|
||
listBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="7" style="text-align: center; color: var(--text-light); padding: 40px 0;">상단에서 대분류 코드를 선택하시면 세부 코드 목록이 표시됩니다.</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let cm = codeMasters.find(master => master.code === selectedCodeMasterId);
|
||
titleEl.textContent = `📑 [${cm.name} (${cm.code})] 세부 코드 목록`;
|
||
descEl.textContent = '이 대분류에 배정된 소분류 세부 코드 및 조합 코드 목록입니다.';
|
||
if (addBtn) addBtn.disabled = false;
|
||
|
||
// Filter details and sort them
|
||
let filteredDetails = codeDetails
|
||
.filter(d => d.mainCode === selectedCodeMasterId)
|
||
.sort((a, b) => a.sort - b.sort);
|
||
|
||
if (filteredDetails.length === 0) {
|
||
listBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="7" style="text-align: center; color: var(--text-light); padding: 40px 0;">등록된 세부 코드가 없습니다. 세부 코드를 추가해 주세요.</td>
|
||
</tr>
|
||
`;
|
||
} else {
|
||
listBody.innerHTML = '';
|
||
filteredDetails.forEach((cd, idx) => {
|
||
let row = `
|
||
<tr>
|
||
<td>${idx + 1}</td>
|
||
<td><strong>${cd.subCode}</strong></td>
|
||
<td><code>${cd.baseCode}</code></td>
|
||
<td>${cd.name}</td>
|
||
<td>${cd.sort}</td>
|
||
<td><span class="badge ${cd.useYn === 'Y' ? 'active' : 'inactive'}">${cd.useYn === 'Y' ? '사용' : '미사용'}</span></td>
|
||
<td>
|
||
<div class="action-btns">
|
||
<button class="btn btn-secondary btn-sm" onclick="openCodeDetailModal('update', '${cd.subCode}')">수정</button>
|
||
<button class="btn btn-danger btn-sm" onclick="deleteCodeDetail('${cd.subCode}')">삭제</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
listBody.insertAdjacentHTML('beforeend', row);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Code Master Modal Controls
|
||
function openCodeMasterModal(mode, code = null) {
|
||
const overlay = document.getElementById('codeMasterModalOverlay');
|
||
const title = document.getElementById('code-master-modal-title');
|
||
const submitBtn = document.getElementById('code-master-submit-btn');
|
||
|
||
const codeInput = document.getElementById('form-master-code');
|
||
const nameInput = document.getElementById('form-master-name');
|
||
const useSelect = document.getElementById('form-master-useyn');
|
||
const rmkInput = document.getElementById('form-master-rmk');
|
||
|
||
if (mode === 'create') {
|
||
title.textContent = '➕ 신규 대분류 등록';
|
||
submitBtn.textContent = '등록 하기';
|
||
codeInput.disabled = false;
|
||
|
||
codeInput.value = '';
|
||
nameInput.value = '';
|
||
useSelect.value = 'Y';
|
||
rmkInput.value = '';
|
||
} else if (mode === 'update') {
|
||
let cm = codeMasters.find(master => master.code === code);
|
||
if (!cm) return;
|
||
|
||
title.textContent = '📝 대분류 설정 수정';
|
||
submitBtn.textContent = '설정 저장';
|
||
codeInput.disabled = true;
|
||
|
||
codeInput.value = cm.code;
|
||
nameInput.value = cm.name;
|
||
useSelect.value = cm.useYn;
|
||
rmkInput.value = cm.rmk || '';
|
||
}
|
||
|
||
overlay.style.display = 'flex';
|
||
}
|
||
|
||
function closeCodeMasterModal() {
|
||
document.getElementById('codeMasterModalOverlay').style.display = 'none';
|
||
}
|
||
|
||
function handleCodeMasterSubmit(e) {
|
||
e.preventDefault();
|
||
const codeInput = document.getElementById('form-master-code');
|
||
const nameValue = document.getElementById('form-master-name').value;
|
||
const useValue = document.getElementById('form-master-useyn').value;
|
||
const rmkValue = document.getElementById('form-master-rmk').value;
|
||
|
||
let cmIndex = codeMasters.findIndex(cm => cm.code === codeInput.value);
|
||
|
||
if (codeInput.disabled) { // Update
|
||
if (cmIndex > -1) {
|
||
codeMasters[cmIndex].name = nameValue;
|
||
codeMasters[cmIndex].useYn = useValue;
|
||
codeMasters[cmIndex].rmk = rmkValue;
|
||
alert('대분류 코드 정보가 수정되었습니다.');
|
||
}
|
||
} else { // Create
|
||
if (cmIndex > -1) {
|
||
alert('이미 등록된 대분류 코드입니다.');
|
||
return;
|
||
}
|
||
codeMasters.push({
|
||
code: codeInput.value,
|
||
name: nameValue,
|
||
useYn: useValue,
|
||
rmk: rmkValue
|
||
});
|
||
alert('신규 대분류 코드가 등록되었습니다.');
|
||
}
|
||
|
||
closeCodeMasterModal();
|
||
renderCodeMasters();
|
||
}
|
||
|
||
function deleteCodeMaster(code) {
|
||
if (confirm(`대분류 코드 [${code}]를 삭제하시겠습니까?\n이 코드에 배정된 모든 세부 소분류 코드 정보도 함께 영구 삭제됩니다.`)) {
|
||
codeMasters = codeMasters.filter(cm => cm.code !== code);
|
||
codeDetails = codeDetails.filter(cd => cd.mainCode !== code); // cascade delete
|
||
|
||
if (selectedCodeMasterId === code) {
|
||
selectedCodeMasterId = null;
|
||
}
|
||
|
||
renderCodeMgmt();
|
||
alert('대분류 코드가 삭제되었습니다.');
|
||
}
|
||
}
|
||
|
||
// Code Detail Modal Controls
|
||
function openCodeDetailModal(mode, subCode = null) {
|
||
if (!selectedCodeMasterId) {
|
||
alert('상단 대분류 코드를 먼저 선택해 주세요.');
|
||
return;
|
||
}
|
||
|
||
const overlay = document.getElementById('codeDetailModalOverlay');
|
||
const title = document.getElementById('code-detail-modal-title');
|
||
const submitBtn = document.getElementById('code-detail-submit-btn');
|
||
|
||
const maincodeInput = document.getElementById('form-detail-maincode');
|
||
const subcodeInput = document.getElementById('form-detail-subcode');
|
||
const nameInput = document.getElementById('form-detail-name');
|
||
const sortInput = document.getElementById('form-detail-sort');
|
||
const useSelect = document.getElementById('form-detail-useyn');
|
||
const rmkInput = document.getElementById('form-detail-rmk');
|
||
|
||
maincodeInput.value = selectedCodeMasterId;
|
||
|
||
if (mode === 'create') {
|
||
title.textContent = '➕ 신규 세부코드 등록';
|
||
submitBtn.textContent = '등록 하기';
|
||
subcodeInput.disabled = false;
|
||
|
||
subcodeInput.value = '';
|
||
nameInput.value = '';
|
||
sortInput.value = codeDetails.filter(d => d.mainCode === selectedCodeMasterId).length + 1;
|
||
useSelect.value = 'Y';
|
||
rmkInput.value = '';
|
||
} else if (mode === 'update') {
|
||
let cd = codeDetails.find(d => d.mainCode === selectedCodeMasterId && d.subCode === subCode);
|
||
if (!cd) return;
|
||
|
||
title.textContent = '📝 세부코드 설정 수정';
|
||
submitBtn.textContent = '설정 저장';
|
||
subcodeInput.disabled = true;
|
||
|
||
subcodeInput.value = cd.subCode;
|
||
nameInput.value = cd.name;
|
||
sortInput.value = cd.sort;
|
||
useSelect.value = cd.useYn;
|
||
rmkInput.value = cd.rmk || '';
|
||
}
|
||
|
||
overlay.style.display = 'flex';
|
||
}
|
||
|
||
function closeCodeDetailModal() {
|
||
document.getElementById('codeDetailModalOverlay').style.display = 'none';
|
||
}
|
||
|
||
function handleCodeDetailSubmit(e) {
|
||
e.preventDefault();
|
||
const subcodeInput = document.getElementById('form-detail-subcode');
|
||
const nameValue = document.getElementById('form-detail-name').value;
|
||
const sortValue = parseInt(document.getElementById('form-detail-sort').value);
|
||
const useValue = document.getElementById('form-detail-useyn').value;
|
||
const rmkValue = document.getElementById('form-detail-rmk').value;
|
||
|
||
let cdIndex = codeDetails.findIndex(cd => cd.mainCode === selectedCodeMasterId && cd.subCode === subcodeInput.value);
|
||
|
||
if (subcodeInput.disabled) { // Update
|
||
if (cdIndex > -1) {
|
||
codeDetails[cdIndex].name = nameValue;
|
||
codeDetails[cdIndex].sort = sortValue;
|
||
codeDetails[cdIndex].useYn = useValue;
|
||
codeDetails[cdIndex].rmk = rmkValue;
|
||
alert('세부코드 정보가 수정되었습니다.');
|
||
}
|
||
} else { // Create
|
||
if (cdIndex > -1) {
|
||
alert('동일한 대분류 내에 이미 등록된 소분류 코드입니다.');
|
||
return;
|
||
}
|
||
codeDetails.push({
|
||
mainCode: selectedCodeMasterId,
|
||
subCode: subcodeInput.value,
|
||
baseCode: `${selectedCodeMasterId}_${subcodeInput.value}`,
|
||
name: nameValue,
|
||
sort: sortValue,
|
||
useYn: useValue,
|
||
rmk: rmkValue
|
||
});
|
||
alert('신규 세부코드가 등록되었습니다.');
|
||
}
|
||
|
||
closeCodeDetailModal();
|
||
renderCodeDetails();
|
||
}
|
||
|
||
function deleteCodeDetail(subCode) {
|
||
if (confirm(`세부 코드 [${subCode}]를 삭제하시겠습니까?`)) {
|
||
codeDetails = codeDetails.filter(cd => !(cd.mainCode === selectedCodeMasterId && cd.subCode === subCode));
|
||
renderCodeDetails();
|
||
alert('세부 코드가 삭제되었습니다.');
|
||
}
|
||
}
|
||
|
||
// --- 11. Page Initialize ---
|
||
window.onload = function() {
|
||
renderDashboard();
|
||
syncProjectDropdowns();
|
||
syncUserDropdowns();
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|