Files
PM_test/관리자화면_통합대시보드_UI.html
2026-06-12 17:14:03 +09:00

2481 lines
114 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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()">&times;</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()">&times;</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()">&times;</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()">&times;</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()">&times;</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>