Files
sam-manage/resources/views/additional/pptx/index.blade.php
김보곤 1bb2ac32f9 feat: [additional] PPTX 관리 페이지 추가
- 파일시스템 스캔 기반 PPTX 목록 조회/다운로드
- 카테고리별 필터, 파일명 검색 기능
- 경로 트래버설 방지 보안 검증
2026-02-23 07:57:39 +09:00

203 lines
10 KiB
PHP

@extends('layouts.app')
@section('title', 'PPTX 관리')
@push('styles')
<style>
.pptx { max-width: 1100px; margin: 0 auto; padding: 32px 20px 48px; }
/* 헤더 */
.pptx-header { text-align: center; margin-bottom: 32px; }
.pptx-header h1 { font-size: 1.75rem; font-weight: 700; color: #1e293b; margin-bottom: 6px; }
.pptx-header p { color: #64748b; font-size: 0.95rem; }
/* 통계 바 */
.pptx-stats { display: flex; gap: 16px; justify-content: center; margin-bottom: 28px; }
.pptx-stat { background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; padding: 14px 24px; display: flex; align-items: center; gap: 10px; }
.pptx-stat-icon { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center; }
.pptx-stat-icon svg { width: 20px; height: 20px; }
.pptx-stat-icon.count { background: #dbeafe; color: #2563eb; }
.pptx-stat-icon.size { background: #dcfce7; color: #16a34a; }
.pptx-stat-num { font-size: 1.3rem; font-weight: 700; color: #1e293b; }
.pptx-stat-label { font-size: 0.75rem; color: #94a3b8; }
/* 검색 + 필터 바 */
.pptx-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 24px; }
.pptx-search { flex: 1; min-width: 200px; position: relative; }
.pptx-search input { width: 100%; padding: 10px 14px 10px 38px; border: 1px solid #e2e8f0; border-radius: 10px; font-size: 0.88rem; outline: none; transition: border-color 0.2s; background: #fff; }
.pptx-search input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }
.pptx-search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: #94a3b8; }
/* 필터 탭 */
.pptx-filters { display: flex; flex-wrap: wrap; gap: 6px; }
.pptx-filter-btn { padding: 6px 14px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; font-size: 0.8rem; color: #64748b; cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.pptx-filter-btn:hover { border-color: #3b82f6; color: #3b82f6; }
.pptx-filter-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
/* 파일 목록 */
.pptx-list { display: flex; flex-direction: column; gap: 10px; }
.pptx-item { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 18px 20px; display: flex; align-items: center; gap: 16px; transition: all 0.2s; }
.pptx-item:hover { border-color: #3b82f6; box-shadow: 0 4px 12px rgba(59,130,246,0.08); }
/* 파일 아이콘 */
.pptx-icon { width: 44px; height: 44px; border-radius: 10px; background: #fff7ed; color: #ea580c; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.pptx-icon svg { width: 22px; height: 22px; }
/* 파일 정보 */
.pptx-info { flex: 1; min-width: 0; }
.pptx-name { font-size: 0.92rem; font-weight: 600; color: #1e293b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pptx-meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 5px; align-items: center; }
.pptx-badge { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 0.7rem; font-weight: 500; }
.pptx-badge.cat { background: #dbeafe; color: #1d4ed8; }
.pptx-badge.src { background: #f1f5f9; color: #64748b; }
.pptx-meta-text { font-size: 0.75rem; color: #94a3b8; }
.pptx-meta-sep { color: #e2e8f0; font-size: 0.7rem; }
/* 다운로드 버튼 */
.pptx-dl { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; font-size: 0.8rem; font-weight: 500; color: #334155; text-decoration: none; transition: all 0.2s; flex-shrink: 0; }
.pptx-dl:hover { background: #3b82f6; border-color: #3b82f6; color: #fff; }
.pptx-dl svg { width: 14px; height: 14px; }
/* 빈 상태 */
.pptx-empty { text-align: center; padding: 60px 20px; color: #94a3b8; }
.pptx-empty svg { width: 48px; height: 48px; margin: 0 auto 12px; color: #cbd5e1; }
.pptx-empty p { font-size: 0.9rem; }
@media (max-width: 768px) {
.pptx-stats { flex-direction: column; align-items: stretch; }
.pptx-item { flex-direction: column; align-items: flex-start; gap: 12px; }
.pptx-dl { align-self: flex-end; }
}
</style>
@endpush
@section('content')
<div class="pptx">
{{-- 헤더 --}}
<div class="pptx-header">
<h1>PPTX 관리</h1>
<p>회사 프레젠테이션 자료를 곳에서 관리합니다</p>
</div>
{{-- 통계 --}}
<div class="pptx-stats">
<div class="pptx-stat">
<div class="pptx-stat-icon count">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
</div>
<div>
<div class="pptx-stat-num">{{ $totalCount }}</div>
<div class="pptx-stat-label">전체 파일</div>
</div>
</div>
<div class="pptx-stat">
<div class="pptx-stat-icon size">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" /></svg>
</div>
<div>
<div class="pptx-stat-num">{{ $totalSize }}</div>
<div class="pptx-stat-label"> 크기</div>
</div>
</div>
</div>
{{-- 검색 + 필터 --}}
<div class="pptx-toolbar">
<div class="pptx-search">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
<input type="text" id="pptxSearch" placeholder="파일명 검색..." autocomplete="off">
</div>
<div class="pptx-filters">
<button class="pptx-filter-btn active" data-category="all">전체</button>
@foreach ($categories as $cat)
<button class="pptx-filter-btn" data-category="{{ $cat }}">{{ $cat }}</button>
@endforeach
</div>
</div>
{{-- 파일 목록 --}}
<div class="pptx-list" id="pptxList">
@forelse ($files as $file)
<div class="pptx-item" data-category="{{ $file['category'] }}" data-name="{{ mb_strtolower($file['name']) }}">
<div class="pptx-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
</div>
<div class="pptx-info">
<div class="pptx-name" title="{{ $file['name'] }}">{{ $file['name'] }}</div>
<div class="pptx-meta">
<span class="pptx-badge cat">{{ $file['category'] }}</span>
<span class="pptx-badge src">{{ $file['source_dir'] }}</span>
<span class="pptx-meta-sep">|</span>
<span class="pptx-meta-text">{{ $file['size'] }}</span>
<span class="pptx-meta-sep">|</span>
<span class="pptx-meta-text">{{ $file['modified_date'] }}</span>
</div>
</div>
<a href="{{ route('additional.pptx.download', ['file' => $file['path']]) }}" class="pptx-dl">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
다운로드
</a>
</div>
@empty
<div class="pptx-empty">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
<p>PPTX 파일이 없습니다</p>
</div>
@endforelse
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const searchInput = document.getElementById('pptxSearch');
const filterBtns = document.querySelectorAll('.pptx-filter-btn');
const items = document.querySelectorAll('.pptx-item');
let activeCategory = 'all';
function filterItems() {
const query = searchInput.value.toLowerCase().trim();
let visibleCount = 0;
items.forEach(function (item) {
const name = item.dataset.name;
const category = item.dataset.category;
const matchCategory = activeCategory === 'all' || category === activeCategory;
const matchSearch = !query || name.includes(query);
const visible = matchCategory && matchSearch;
item.style.display = visible ? '' : 'none';
if (visible) visibleCount++;
});
// 빈 상태 메시지
let emptyEl = document.getElementById('pptxFilterEmpty');
if (visibleCount === 0) {
if (!emptyEl) {
emptyEl = document.createElement('div');
emptyEl.id = 'pptxFilterEmpty';
emptyEl.className = 'pptx-empty';
emptyEl.innerHTML = '<p>검색 결과가 없습니다</p>';
document.getElementById('pptxList').appendChild(emptyEl);
}
emptyEl.style.display = '';
} else if (emptyEl) {
emptyEl.style.display = 'none';
}
}
filterBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
filterBtns.forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active');
activeCategory = btn.dataset.category;
filterItems();
});
});
searchInput.addEventListener('input', filterItems);
});
</script>
@endpush