Files
sam-manage/resources/views/documents/index.blade.php
권혁성 c65d3f49dc feat: 문서 관리 시스템 MNG 관리자 패널 구현 (Phase 2)
- Document 관련 모델 4개 생성 (Document, DocumentApproval, DocumentData, DocumentAttachment)
- DocumentController 생성 (목록/생성/상세/수정 페이지)
- DocumentApiController 생성 (AJAX CRUD 처리)
- 문서 관리 뷰 3개 생성 (index, edit, show)
- 웹/API 라우트 등록

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:51:23 +09:00

240 lines
9.4 KiB
PHP

@extends('layouts.app')
@section('title', '문서 관리')
@section('content')
<div class="p-6">
{{-- 헤더 --}}
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">문서 관리</h1>
<p class="text-sm text-gray-500 mt-1">작성된 문서를 관리합니다.</p>
</div>
<a href="{{ route('documents.create') }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
문서 작성
</a>
</div>
{{-- 필터 --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<form id="filterForm" class="grid grid-cols-1 md:grid-cols-4 gap-4">
{{-- 상태 필터 --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
<select name="status" class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">전체</option>
@foreach($statuses as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
{{-- 템플릿 필터 --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">템플릿</label>
<select name="template_id" class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">전체</option>
@foreach($templates as $template)
<option value="{{ $template->id }}">{{ $template->name }}</option>
@endforeach
</select>
</div>
{{-- 검색 --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">검색</label>
<input type="text" name="search" placeholder="문서번호, 제목"
class="w-full rounded-lg border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500">
</div>
{{-- 버튼 --}}
<div class="flex items-end gap-2">
<button type="submit"
class="px-4 py-2 bg-gray-800 text-white text-sm font-medium rounded-lg hover:bg-gray-900 transition-colors">
검색
</button>
<button type="reset"
class="px-4 py-2 bg-gray-100 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-200 transition-colors">
초기화
</button>
</div>
</form>
</div>
{{-- 문서 목록 --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">문서번호</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">제목</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">템플릿</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">작성자</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">작성일</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
</tr>
</thead>
<tbody id="documentList" class="bg-white divide-y divide-gray-200">
{{-- HTMX로 로드 --}}
</tbody>
</table>
</div>
{{-- 페이지네이션 --}}
<div id="pagination" class="px-6 py-4 border-t border-gray-200">
{{-- HTMX로 로드 --}}
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
loadDocuments();
// 필터 폼 제출
document.getElementById('filterForm').addEventListener('submit', function(e) {
e.preventDefault();
loadDocuments();
});
// 초기화 버튼
document.getElementById('filterForm').addEventListener('reset', function() {
setTimeout(() => loadDocuments(), 10);
});
});
function loadDocuments(page = 1) {
const form = document.getElementById('filterForm');
const formData = new FormData(form);
const params = new URLSearchParams(formData);
params.set('page', page);
fetch(`/api/admin/documents?${params.toString()}`, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
}
})
.then(response => response.json())
.then(data => {
renderDocuments(data.data);
renderPagination(data);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('documentList').innerHTML = `
<tr>
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
데이터를 불러오는 중 오류가 발생했습니다.
</td>
</tr>
`;
});
}
function renderDocuments(documents) {
const tbody = document.getElementById('documentList');
if (!documents || documents.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
문서가 없습니다.
</td>
</tr>
`;
return;
}
tbody.innerHTML = documents.map(doc => `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-medium text-gray-900">${doc.document_no}</span>
</td>
<td class="px-6 py-4">
<a href="/documents/${doc.id}" class="text-sm text-blue-600 hover:text-blue-800 hover:underline">
${doc.title}
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-600">${doc.template?.name || '-'}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusClass(doc.status)}">
${getStatusLabel(doc.status)}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-600">${doc.creator?.name || '-'}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-600">${formatDate(doc.created_at)}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="/documents/${doc.id}" class="text-gray-600 hover:text-gray-900 mr-3">보기</a>
${doc.status === 'DRAFT' || doc.status === 'REJECTED' ? `
<a href="/documents/${doc.id}/edit" class="text-blue-600 hover:text-blue-900">수정</a>
` : ''}
</td>
</tr>
`).join('');
}
function renderPagination(data) {
const container = document.getElementById('pagination');
if (data.last_page <= 1) {
container.innerHTML = '';
return;
}
let html = '<div class="flex items-center justify-between"><div class="text-sm text-gray-500">';
html += `총 ${data.total}건 중 ${data.from}-${data.to}건`;
html += '</div><div class="flex gap-1">';
for (let i = 1; i <= data.last_page; i++) {
const isActive = i === data.current_page;
html += `<button onclick="loadDocuments(${i})" class="px-3 py-1 text-sm rounded ${isActive ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}">${i}</button>`;
}
html += '</div></div>';
container.innerHTML = html;
}
function getStatusLabel(status) {
const labels = {
'DRAFT': '작성중',
'PENDING': '결재중',
'APPROVED': '승인',
'REJECTED': '반려',
'CANCELLED': '취소'
};
return labels[status] || status;
}
function getStatusClass(status) {
const classes = {
'DRAFT': 'bg-gray-100 text-gray-800',
'PENDING': 'bg-yellow-100 text-yellow-800',
'APPROVED': 'bg-green-100 text-green-800',
'REJECTED': 'bg-red-100 text-red-800',
'CANCELLED': 'bg-gray-100 text-gray-600'
};
return classes[status] || 'bg-gray-100 text-gray-800';
}
function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
</script>
@endpush