Files
PM_test/관리자화면_보관삭제정책설정_UI제안.html
2026-06-12 17:14:03 +09:00

639 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>관리자 화면 - 자동 보관 및 삭제 정책 설정 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: #f8fafc;
--card-bg: #ffffff;
--text-title: #0f172a;
--text-desc: #475569;
--text-light: #94a3b8;
--primary: #f43f5e;
--primary-hover: #e11d48;
--primary-soft: #fff1f2;
--border: #e2e8f0;
--success: #10b981;
--success-soft: #ecfdf5;
--warning: #f59e0b;
--warning-soft: #fef3c7;
--radius-lg: 16px;
--radius-md: 10px;
--radius-sm: 6px;
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.05), 0 4px 6px -4px rgb(0 0 0 / 0.05);
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: 'Inter', 'Noto Sans KR', sans-serif;
background-color: var(--bg-color);
color: var(--text-title);
margin: 0;
padding: 40px 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
box-sizing: border-box;
}
.panel-container {
width: 100%;
max-width: 900px;
background-color: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
flex-direction: column;
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
/* Header Styling */
.panel-header {
padding: 30px;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header .title-area h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 6px 0;
display: flex;
align-items: center;
gap: 10px;
}
.panel-header .title-area p {
font-size: 0.875rem;
color: var(--text-light);
margin: 0;
}
.active-badge {
background-color: var(--success);
color: #ffffff;
font-size: 0.8rem;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
box-shadow: 0 2px 10px rgba(16, 185, 129, 0.3);
display: flex;
align-items: center;
gap: 6px;
}
.active-badge::before {
content: '';
width: 6px;
height: 6px;
background-color: #ffffff;
border-radius: 50%;
display: inline-block;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(0.9); opacity: 0.6; }
50% { transform: scale(1.3); opacity: 1; }
100% { transform: scale(0.9); opacity: 0.6; }
}
/* Body Grid Layout */
.panel-body {
padding: 30px;
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 30px;
}
@media (max-width: 768px) {
.panel-body {
grid-template-columns: 1fr;
}
}
/* Left side: Config Form */
.config-form {
display: flex;
flex-direction: column;
gap: 24px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-title);
}
.select-input, .number-input {
width: 100%;
padding: 12px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
font-size: 0.95rem;
color: var(--text-title);
background-color: #f8fafc;
box-sizing: border-box;
transition: var(--transition);
outline: none;
}
.select-input:focus, .number-input:focus {
border-color: var(--primary);
background-color: #ffffff;
box-shadow: 0 0 0 3px var(--primary-soft);
}
/* Custom Toggle Switch */
.toggle-group {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8fafc;
padding: 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
}
.toggle-label-desc h4 {
margin: 0 0 4px 0;
font-size: 0.95rem;
font-weight: 600;
}
.toggle-label-desc p {
margin: 0;
font-size: 0.8rem;
color: var(--text-desc);
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #cbd5e1;
transition: .3s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--primary);
}
input:checked + .slider:before {
transform: translateX(24px);
}
/* Number control wraps */
.input-with-unit {
position: relative;
display: flex;
align-items: center;
}
.input-with-unit input {
padding-right: 50px;
}
.input-with-unit .unit {
position: absolute;
right: 16px;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-desc);
}
/* Right side: Policy Card */
.policy-card-area {
display: flex;
flex-direction: column;
gap: 20px;
}
.summary-card {
background-color: var(--primary-soft);
border: 1px dashed var(--primary);
border-radius: var(--radius-lg);
padding: 24px;
height: fit-content;
display: flex;
flex-direction: column;
gap: 16px;
transition: var(--transition);
}
.summary-card .summary-header {
display: flex;
align-items: center;
gap: 10px;
color: var(--primary-dark);
font-weight: 700;
font-size: 1rem;
}
.summary-card .summary-content {
font-size: 0.92rem;
color: #be123c;
line-height: 1.6;
margin: 0;
}
.summary-card .summary-content span.highlight {
background-color: #ffe4e6;
padding: 2px 6px;
border-radius: 4px;
font-weight: 700;
color: var(--primary);
}
/* Warning Box */
.warning-box {
background-color: var(--warning-soft);
border: 1px solid #fde68a;
border-radius: var(--radius-md);
padding: 16px;
display: flex;
gap: 12px;
align-items: flex-start;
}
.warning-box span.icon {
font-size: 1.2rem;
}
.warning-box p {
margin: 0;
font-size: 0.82rem;
color: #78350f;
line-height: 1.5;
}
/* Button Action Footer */
.panel-footer {
padding: 20px 30px;
background-color: #f8fafc;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn {
padding: 12px 24px;
border-radius: var(--radius-md);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
border: none;
outline: none;
}
.btn-secondary {
background-color: #ffffff;
color: var(--text-desc);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background-color: #f1f5f9;
color: var(--text-title);
}
.btn-primary {
background-color: var(--primary);
color: #ffffff;
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.2);
}
.btn-primary:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(244, 63, 94, 0.3);
}
.btn-primary:active {
transform: translateY(0);
}
/* History Log Table */
.history-section {
margin-top: 30px;
border-top: 1px solid var(--border);
padding-top: 30px;
}
.history-section h3 {
font-size: 1.1rem;
margin: 0 0 16px 0;
font-weight: 700;
color: var(--text-title);
}
.log-table-wrapper {
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
table.log-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
text-align: left;
}
table.log-table th {
background-color: #f1f5f9;
color: var(--text-desc);
font-weight: 600;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
table.log-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
color: var(--text-desc);
}
table.log-table tr:last-child td {
border-bottom: none;
}
table.log-table tr:hover td {
background-color: #fafafa;
}
.status-pill {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.status-pill.success {
background-color: var(--success-soft);
color: var(--success);
}
</style>
</head>
<body>
<div class="panel-container">
<!-- Header -->
<div class="panel-header">
<div class="title-area">
<h1>⚙️ 자동 보관 및 파일 삭제 정책 설정</h1>
<p>프로젝트별 용량 확보를 위해 최소 파일 개수 미달 폴더를 자동으로 감지 및 삭제 처리합니다.</p>
</div>
<div class="active-badge" id="statusBadge">동작 활성화 중</div>
</div>
<!-- Body -->
<div class="panel-body">
<!-- Left: Config Config Form -->
<div class="config-form">
<!-- Project Selection -->
<div class="form-group">
<label for="projectSelector">설정 대상 프로젝트 선택</label>
<select class="select-input" id="projectSelector" onchange="onProjectChange()">
<option value="global" selected>🌐 글로벌 공통 정책 (기본값)</option>
<option value="PM_TEST_01">🌉 한국 가상 교량 건설 프로젝트 (PM_TEST_01)</option>
<option value="PM_TEST_02">🛣️ 가상 도로 건설 프로젝트 (PM_TEST_02)</option>
</select>
</div>
<!-- Toggle Switch -->
<div class="toggle-group">
<div class="toggle-label-desc">
<h4>자동 삭제 정책 토글</h4>
<p>현장의 보존 정책 스케줄러 자동 감지 작동 여부</p>
</div>
<label class="switch">
<input type="checkbox" id="policyActiveToggle" checked onchange="updateSummary()">
<span class="slider"></span>
</label>
</div>
<!-- Keep File Threshold -->
<div class="form-group">
<label for="fileThresholdInput">삭제 대상 판정 파일 개수 기준</label>
<div class="input-with-unit">
<input type="number" class="number-input" id="fileThresholdInput" value="3" min="1" max="100" oninput="updateSummary()">
<span class="unit">개 미만</span>
</div>
</div>
<!-- Keep Days Threshold -->
<div class="form-group">
<label for="daysThresholdInput">자동 삭제 대기 기간</label>
<div class="input-with-unit">
<input type="number" class="number-input" id="daysThresholdInput" value="15" min="1" max="365" oninput="updateSummary()">
<span class="unit">일 지속</span>
</div>
</div>
</div>
<!-- Right: Real-time Rule Card & Warning -->
<div class="policy-card-area">
<!-- Rule Card -->
<div class="summary-card">
<div class="summary-header">
<span>📌</span> 적용될 보존 정책 규칙 요약
</div>
<p class="summary-content" id="policySummaryText">
현재 설정에 따라, 폴더 내부의 총 파일 개수가 <span class="highlight">3개 미만</span>이 된 시점부터 <span class="highlight">15일</span> 동안 상태가 유지되면, 사용자가 아카이브 진입 시 해당 폴더는 즉시 <span class="highlight">자동 삭제</span> 처리되고 하위 파일들은 <span class="highlight">휴지통으로 즉시 이동</span>합니다.
</p>
</div>
<!-- Warning Alert -->
<div class="warning-box">
<span class="icon">⚠️</span>
<p>
<strong>주의 사항:</strong><br>
이 설정은 프론트엔드 코드에 하드코딩되어 있던 임계값(FOLDER_KEEP_FILE_THRESHOLD / FOLDER_KEEP_DAYS_THRESHOLD)을 대체합니다. 저장 시 즉시 데이터베이스의 <code>tb_project</code> 테이블 설정값으로 갱신되며, 로그인된 모든 일반 유저 화면에 동적 실시간 반영됩니다.
</p>
</div>
</div>
</div>
<!-- Footer Buttons -->
<div class="panel-footer">
<button class="btn btn-secondary" onclick="resetForm()">기본값 초기화</button>
<button class="btn btn-primary" onclick="saveConfig()">변경 사항 저장</button>
</div>
<!-- History Log Table (Optional mockup for premium feel) -->
<div style="padding: 0 30px 30px 30px;">
<div class="history-section">
<h3>🕒 최근 자동 처리 이력 (최근 5건)</h3>
<div class="log-table-wrapper">
<table class="log-table">
<thead>
<tr>
<th>처리일자</th>
<th>프로젝트 ID</th>
<th>대상 폴더 경로</th>
<th>감지 당시 파일수</th>
<th>처리 결과</th>
</tr>
</thead>
<tbody id="logTableBody">
<tr>
<td>2026-06-11 09:15:32</td>
<td>PM_TEST_01</td>
<td>/01_설계도서/구조계산서/임시보관</td>
<td>1개 (기준: 3개 미만)</td>
<td><span class="status-pill success">휴지통 이동 완료</span></td>
</tr>
<tr>
<td>2026-06-10 14:02:11</td>
<td>PM_TEST_02</td>
<td>/자료실/dwg/일반도/임시</td>
<td>0개 (기준: 3개 미만)</td>
<td><span class="status-pill success">휴지통 이동 완료</span></td>
</tr>
<tr>
<td>2026-06-08 11:22:45</td>
<td>PM_TEST_01</td>
<td>/과업설명서/참조자료/2025버전</td>
<td>2개 (기준: 3개 미만)</td>
<td><span class="status-pill success">휴지통 이동 완료</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// Mock Data for Project Settings
const mockSettings = {
global: { active: true, files: 3, days: 15 },
PM_TEST_01: { active: true, files: 3, days: 15 },
PM_TEST_02: { active: false, files: 5, days: 10 }
};
function onProjectChange() {
const selectedProj = document.getElementById('projectSelector').value;
const config = mockSettings[selectedProj];
document.getElementById('policyActiveToggle').checked = config.active;
document.getElementById('fileThresholdInput').value = config.files;
document.getElementById('daysThresholdInput').value = config.days;
updateSummary();
}
function updateSummary() {
const active = document.getElementById('policyActiveToggle').checked;
const files = document.getElementById('fileThresholdInput').value;
const days = document.getElementById('daysThresholdInput').value;
const summaryText = document.getElementById('policySummaryText');
const badge = document.getElementById('statusBadge');
// Update active badge
if (active) {
badge.textContent = "동작 활성화 중";
badge.style.backgroundColor = "var(--success)";
badge.style.color = "#ffffff";
badge.style.boxShadow = "0 2px 10px rgba(16, 185, 129, 0.3)";
} else {
badge.textContent = "정책 일시 중지";
badge.style.backgroundColor = "var(--text-light)";
badge.style.color = "#ffffff";
badge.style.boxShadow = "none";
}
if (!active) {
summaryText.innerHTML = `<span style="color: var(--text-light); font-style: italic;">현재 이 프로젝트(현장)에 대한 자동 삭제 정책 작동이 중지된 상태입니다. 파일이 기준 미달이더라도 자동으로 휴지통으로 이동하지 않습니다.</span>`;
return;
}
// Days calculation mapping
const countdownDays = days - 1; // 15일 설정 시 화면에 D-14로 표출되므로 매칭
summaryText.innerHTML = `현재 설정에 따라, 폴더 내부의 총 파일 개수가 <span class="highlight">${files}개 미만</span>이 된 시점부터 <span class="highlight">${days}일</span> 동안 상태가 유지되면, 사용자가 아카이브 진입 시 해당 폴더 옆에 <span class="highlight">D-${countdownDays}</span> 타이머가 시작되고, 만료 즉시 <span class="highlight">자동 삭제</span> 처리되어 폴더 내 파일은 휴지통으로 이동합니다.`;
}
function resetForm() {
document.getElementById('policyActiveToggle').checked = true;
document.getElementById('fileThresholdInput').value = 3;
document.getElementById('daysThresholdInput').value = 15;
updateSummary();
}
function saveConfig() {
const selectedProj = document.getElementById('projectSelector').value;
const active = document.getElementById('policyActiveToggle').checked;
const files = document.getElementById('fileThresholdInput').value;
const days = document.getElementById('daysThresholdInput').value;
// Save to mock
mockSettings[selectedProj] = { active, files: parseInt(files), days: parseInt(days) };
alert(`[${selectedProj === 'global' ? '글로벌 공통' : selectedProj}] 프로젝트 정책 설정 저장 완료!\n- 활성화 여부: ${active ? 'ON' : 'OFF'}\n- 기준 파일수: ${files}개 미만\n- 제한 일수: ${days}`);
}
// Initialize summary
updateSummary();
</script>
</body>
</html>