Files
sam-manage/resources/views/boards/edit.blade.php
hskwon 5c892c1ed9 브라우저 alert/confirm을 SweetAlert2로 전환
- layouts/app.blade.php에 SweetAlert2 CDN 및 전역 헬퍼 함수 추가
  - showToast(): 토스트 알림 (success, error, warning, info)
  - showConfirm(): 확인 대화상자
  - showDeleteConfirm(): 삭제 확인 (경고 아이콘)
  - showPermanentDeleteConfirm(): 영구 삭제 확인 (빨간색 경고)
  - showSuccess(), showError(): 성공/에러 알림

- 변환된 파일 목록 (48개 Blade 파일):
  - menus/* (6개), boards/* (2개), posts/* (3개)
  - daily-logs/* (3개), project-management/* (6개)
  - dev-tools/flow-tester/* (6개)
  - quote-formulas/* (4개), permission-analyze/* (1개)
  - archived-records/* (1개), profile/* (1개)
  - roles/* (3개), permissions/* (3개)
  - departments/* (3개), tenants/* (3개), users/* (3개)

- 주요 개선사항:
  - Tailwind CSS 테마와 일관된 디자인
  - 비동기 콜백 패턴으로 리팩토링
  - 삭제/복원/영구삭제 각각 다른 스타일 적용
2025-12-05 09:49:56 +09:00

612 lines
26 KiB
PHP

@extends('layouts.app')
@section('title', '게시판 수정')
@section('content')
<!-- 페이지 헤더 -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">📋 게시판 수정: {{ $board->name }}</h1>
<a href="{{ route('boards.index') }}" class="text-gray-600 hover:text-gray-900">
&larr; 목록으로
</a>
</div>
<div class="grid grid-cols-3 gap-6">
<!-- 좌측: 기본 정보 -->
<div class="col-span-2">
<div class="bg-white rounded-lg shadow-sm p-6">
<form id="boardForm" class="space-y-6">
@csrf
@method('PUT')
<!-- 기본 정보 -->
<div class="border-b border-gray-200 pb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">기본 정보</h2>
<div class="grid grid-cols-2 gap-6">
<!-- 게시판 코드 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
게시판 코드 <span class="text-red-500">*</span>
</label>
<input type="text" name="board_code"
value="{{ $board->board_code }}"
pattern="[a-z0-9-]+"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<!-- 게시판명 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
게시판명 <span class="text-red-500">*</span>
</label>
<input type="text" name="name"
value="{{ $board->name }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<!-- 게시판 유형 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">게시판 유형</label>
<input type="text" name="board_type"
value="{{ $board->board_type }}"
list="boardTypeList"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<datalist id="boardTypeList">
<option value="notice">공지사항</option>
<option value="qna">1:1 문의</option>
<option value="faq">FAQ</option>
<option value="free">자유게시판</option>
<option value="gallery">갤러리</option>
<option value="download">자료실</option>
</datalist>
</div>
<!-- 에디터 타입 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">에디터 타입</label>
<select name="editor_type"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="wysiwyg" {{ $board->editor_type === 'wysiwyg' ? 'selected' : '' }}>WYSIWYG (위지윅)</option>
<option value="markdown" {{ $board->editor_type === 'markdown' ? 'selected' : '' }}>Markdown</option>
<option value="text" {{ $board->editor_type === 'text' ? 'selected' : '' }}>텍스트</option>
</select>
</div>
<!-- 설명 -->
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
<textarea name="description" rows="3"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">{{ $board->description }}</textarea>
</div>
</div>
</div>
<!-- 파일 설정 -->
<div class="border-b border-gray-200 pb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">파일 첨부 설정</h2>
<div class="grid grid-cols-3 gap-6">
<!-- 파일 첨부 허용 -->
<div>
<label class="flex items-center">
<input type="checkbox" name="allow_files" value="1"
{{ $board->allow_files ? 'checked' : '' }}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">파일 첨부 허용</span>
</label>
</div>
<!-- 최대 파일 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">최대 파일 </label>
<input type="number" name="max_file_count"
value="{{ $board->max_file_count }}"
min="0" max="100"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<!-- 최대 파일 크기 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">최대 파일 크기 (KB)</label>
<input type="number" name="max_file_size"
value="{{ $board->max_file_size }}"
min="0"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
</div>
<!-- 활성 상태 -->
<div>
<label class="flex items-center">
<input type="checkbox" name="is_active" value="1"
{{ $board->is_active ? 'checked' : '' }}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">활성화</span>
</label>
</div>
<!-- 에러 메시지 -->
<div id="errorMessage" class="hidden bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg"></div>
<!-- 버튼 -->
<div class="flex justify-end space-x-4">
<a href="{{ route('boards.index') }}"
class="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition">
취소
</a>
<button type="submit"
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition">
저장
</button>
</div>
</form>
</div>
</div>
<!-- 우측: 커스텀 필드 관리 -->
<div class="col-span-1">
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">커스텀 필드</h2>
<button type="button" onclick="openFieldModal()"
class="text-sm text-blue-600 hover:text-blue-800">
+ 필드 추가
</button>
</div>
<!-- 필드 목록 -->
<div id="fieldList" class="space-y-2">
@forelse($board->fields as $field)
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<span class="font-medium text-sm">{{ $field->name }}</span>
<span class="text-xs text-gray-500 ml-2">({{ $field->field_key }})</span>
<span class="text-xs px-2 py-0.5 bg-gray-200 rounded ml-2">{{ $field->field_type }}</span>
@if($field->is_required)
<span class="text-xs text-red-500 ml-1">*</span>
@endif
</div>
<div class="flex space-x-2">
<button type="button" onclick="editField({{ $field->id }})"
class="text-indigo-600 hover:text-indigo-900 text-sm">수정</button>
<button type="button" onclick="deleteField({{ $field->id }}, '{{ $field->name }}')"
class="text-red-600 hover:text-red-900 text-sm">삭제</button>
</div>
</div>
@empty
<p class="text-sm text-gray-500 text-center py-4">등록된 커스텀 필드가 없습니다.</p>
@endforelse
</div>
</div>
<!-- 게시판 정보 -->
<div class="bg-white rounded-lg shadow-sm p-6 mt-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">게시판 정보</h2>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500">생성일</dt>
<dd class="text-gray-900">{{ $board->created_at->format('Y-m-d H:i') }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500">수정일</dt>
<dd class="text-gray-900">{{ $board->updated_at->format('Y-m-d H:i') }}</dd>
</div>
@if($board->trashed())
<div class="flex justify-between">
<dt class="text-gray-500">삭제일</dt>
<dd class="text-red-600">{{ $board->deleted_at->format('Y-m-d H:i') }}</dd>
</div>
@endif
</dl>
</div>
</div>
</div>
<!-- 필드 추가 모달 (다중) -->
<div id="fieldModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl p-6" style="width: 700px; max-width: 90vw;">
<h3 id="fieldModalTitle" class="text-lg font-medium text-gray-900 mb-4">필드 추가</h3>
<form id="fieldForm">
<input type="hidden" id="fieldId" name="field_id">
<!-- 헤더 -->
<div class="flex gap-2 mb-2 text-sm font-medium text-gray-700">
<div style="width: 160px;">필드명 <span class="text-red-500">*</span></div>
<div style="width: 160px;">필드 <span class="text-red-500">*</span></div>
<div style="width: 120px;">타입 <span class="text-red-500">*</span></div>
<div style="width: 50px;" class="text-center">필수</div>
<div style="width: 30px;"></div>
</div>
<!-- 필드 컨테이너 -->
<div id="fieldRowsContainer" class="space-y-2 max-h-80 overflow-y-auto">
<!-- 동적으로 행이 추가됨 -->
</div>
<!-- 추가 버튼 -->
<button type="button" onclick="addFieldRow()"
class="mt-3 w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 transition flex items-center justify-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
필드 추가
</button>
<p class="text-xs text-gray-500 mt-2">* 필드 : 영문 소문자와 언더스코어만 사용</p>
<div class="flex justify-end space-x-3 pt-4 mt-4 border-t">
<button type="button" onclick="closeFieldModal()"
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
취소
</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">
저장
</button>
</div>
</form>
</div>
</div>
<!-- 필드 수정 모달 (단일) -->
<div id="fieldEditModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">필드 수정</h3>
<form id="fieldEditForm" class="space-y-4">
<input type="hidden" id="editFieldId">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">필드명 <span class="text-red-500">*</span></label>
<input type="text" id="editFieldName"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">필드 <span class="text-red-500">*</span></label>
<input type="text" id="editFieldKey"
pattern="[a-z_]+"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<p class="text-xs text-gray-500 mt-1">영문 소문자와 언더스코어만</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">필드 타입 <span class="text-red-500">*</span></label>
<select id="editFieldType"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="text">텍스트</option>
<option value="textarea"> 텍스트</option>
<option value="number">숫자</option>
<option value="date">날짜</option>
<option value="select">선택 (드롭다운)</option>
<option value="checkbox">체크박스</option>
<option value="radio">라디오</option>
<option value="file">파일</option>
</select>
</div>
<div>
<label class="flex items-center">
<input type="checkbox" id="editFieldRequired"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">필수 입력</span>
</label>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" onclick="closeFieldEditModal()"
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
취소
</button>
<button type="submit"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">
저장
</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
const boardId = {{ $board->id }};
let fieldRowIndex = 0;
// 폼 제출 (게시판 수정)
document.getElementById('boardForm').addEventListener('submit', async function(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const errorDiv = document.getElementById('errorMessage');
// 체크박스 처리
formData.set('allow_files', form.allow_files.checked ? '1' : '0');
formData.set('is_active', form.is_active.checked ? '1' : '0');
try {
const response = await fetch(`/api/admin/boards/${boardId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify(Object.fromEntries(formData))
});
const data = await response.json();
if (response.ok && data.success) {
showToast('게시판이 수정되었습니다.', 'success');
window.location.reload();
} else {
errorDiv.textContent = data.message || '게시판 수정에 실패했습니다.';
errorDiv.classList.remove('hidden');
}
} catch (error) {
errorDiv.textContent = '서버 오류가 발생했습니다.';
errorDiv.classList.remove('hidden');
}
});
// 필드 행 HTML 생성
function createFieldRowHtml(index) {
return `
<div class="flex gap-2 items-center field-row" data-index="${index}">
<input type="text" name="fields[${index}][name]" placeholder="필드명"
style="width: 160px;"
class="px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
required>
<input type="text" name="fields[${index}][field_key]" placeholder="field_key"
pattern="[a-z_]+"
style="width: 160px;"
class="px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
required>
<select name="fields[${index}][field_type]"
style="width: 120px;"
class="px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
required>
<option value="text">텍스트</option>
<option value="textarea">긴 텍스트</option>
<option value="number">숫자</option>
<option value="date">날짜</option>
<option value="select">선택</option>
<option value="checkbox">체크박스</option>
<option value="radio">라디오</option>
<option value="file">파일</option>
</select>
<div style="width: 50px;" class="text-center">
<input type="checkbox" name="fields[${index}][is_required]" value="1"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
</div>
<div style="width: 30px;" class="text-center">
<button type="button" onclick="removeFieldRow(this)"
class="text-red-500 hover:text-red-700 p-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
`;
}
// 필드 행 추가
function addFieldRow() {
const container = document.getElementById('fieldRowsContainer');
container.insertAdjacentHTML('beforeend', createFieldRowHtml(fieldRowIndex));
fieldRowIndex++;
}
// 필드 행 삭제
function removeFieldRow(button) {
const row = button.closest('.field-row');
const container = document.getElementById('fieldRowsContainer');
// 최소 1개 행은 유지
if (container.querySelectorAll('.field-row').length > 1) {
row.remove();
} else {
showToast('최소 1개의 필드는 필요합니다.', 'warning');
}
}
// 필드 모달 열기 (추가용)
function openFieldModal() {
const modal = document.getElementById('fieldModal');
const container = document.getElementById('fieldRowsContainer');
// 컨테이너 초기화
container.innerHTML = '';
fieldRowIndex = 0;
// 기본 1개 행 추가
addFieldRow();
modal.classList.remove('hidden');
}
// 필드 모달 닫기
function closeFieldModal() {
document.getElementById('fieldModal').classList.add('hidden');
}
// 필드 수정 모달 열기
async function editField(fieldId) {
try {
const response = await fetch(`/api/admin/boards/${boardId}/fields`);
const data = await response.json();
if (data.success) {
const field = data.data.find(f => f.id === fieldId);
if (field) {
document.getElementById('editFieldId').value = field.id;
document.getElementById('editFieldName').value = field.name;
document.getElementById('editFieldKey').value = field.field_key;
document.getElementById('editFieldType').value = field.field_type;
document.getElementById('editFieldRequired').checked = field.is_required;
document.getElementById('fieldEditModal').classList.remove('hidden');
}
}
} catch (error) {
showToast('필드 정보를 불러오는데 실패했습니다.', 'error');
}
}
// 필드 수정 모달 닫기
function closeFieldEditModal() {
document.getElementById('fieldEditModal').classList.add('hidden');
}
// 필드 삭제
async function deleteField(fieldId, fieldName) {
showDeleteConfirm(fieldName + ' 필드', async () => {
try {
const response = await fetch(`/api/admin/boards/${boardId}/fields/${fieldId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showToast('필드가 삭제되었습니다.', 'success');
window.location.reload();
} else {
showToast(data.message || '필드 삭제에 실패했습니다.', 'error');
}
} catch (error) {
showToast('서버 오류가 발생했습니다.', 'error');
}
});
}
// 필드 추가 폼 제출 (다중)
document.getElementById('fieldForm').addEventListener('submit', async function(e) {
e.preventDefault();
const rows = document.querySelectorAll('#fieldRowsContainer .field-row');
const fields = [];
rows.forEach(row => {
const name = row.querySelector('input[name$="[name]"]').value.trim();
const fieldKey = row.querySelector('input[name$="[field_key]"]').value.trim();
const fieldType = row.querySelector('select[name$="[field_type]"]').value;
const isRequired = row.querySelector('input[name$="[is_required]"]').checked;
if (name && fieldKey) {
fields.push({
name,
field_key: fieldKey,
field_type: fieldType,
is_required: isRequired
});
}
});
if (fields.length === 0) {
showToast('최소 1개의 필드를 입력해주세요.', 'warning');
return;
}
// 순차적으로 저장
let successCount = 0;
let errorMessages = [];
for (const field of fields) {
try {
const response = await fetch(`/api/admin/boards/${boardId}/fields`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify(field)
});
const data = await response.json();
if (data.success) {
successCount++;
} else {
errorMessages.push(`${field.name}: ${data.message || '저장 실패'}`);
}
} catch (error) {
errorMessages.push(`${field.name}: 서버 오류`);
}
}
if (errorMessages.length > 0) {
showToast(`${successCount}개 저장 완료, ${errorMessages.length}개 실패`, 'warning');
}
if (successCount > 0) {
closeFieldModal();
window.location.reload();
}
});
// 필드 수정 폼 제출 (단일)
document.getElementById('fieldEditForm').addEventListener('submit', async function(e) {
e.preventDefault();
const fieldId = document.getElementById('editFieldId').value;
const formData = {
name: document.getElementById('editFieldName').value,
field_key: document.getElementById('editFieldKey').value,
field_type: document.getElementById('editFieldType').value,
is_required: document.getElementById('editFieldRequired').checked
};
try {
const response = await fetch(`/api/admin/boards/${boardId}/fields/${fieldId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
showToast('필드가 수정되었습니다.', 'success');
closeFieldEditModal();
window.location.reload();
} else {
showToast(data.message || '필드 저장에 실패했습니다.', 'error');
}
} catch (error) {
showToast('서버 오류가 발생했습니다.', 'error');
}
});
// 모달 외부 클릭 시 닫기
document.getElementById('fieldModal').addEventListener('click', function(e) {
if (e.target === this) {
closeFieldModal();
}
});
document.getElementById('fieldEditModal').addEventListener('click', function(e) {
if (e.target === this) {
closeFieldEditModal();
}
});
</script>
@endpush