Files
sam-manage/resources/views/quote-formulas/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

364 lines
16 KiB
PHP

@extends('layouts.app')
@section('title', '수식 수정')
@section('content')
<div class="container mx-auto max-w-4xl">
<!-- 헤더 -->
<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('quote-formulas.index') }}"
class="text-gray-600 hover:text-gray-800 text-sm font-medium">
&larr; 목록으로
</a>
</div>
<!-- 로딩 -->
<div id="loading" class="bg-white rounded-lg shadow-sm p-12 text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p class="text-gray-500 mt-4">데이터를 불러오는 ...</p>
</div>
<!-- (로드 표시) -->
<div id="formContainer" class="bg-white rounded-lg shadow-sm p-6 hidden">
<form id="formulaForm" class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 카테고리 -->
<div>
<label for="category_id" class="block text-sm font-medium text-gray-700 mb-1">
카테고리 <span class="text-red-500">*</span>
</label>
<select name="category_id" id="category_id"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="">카테고리 선택</option>
</select>
</div>
<!-- 수식 유형 -->
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-1">
수식 유형 <span class="text-red-500">*</span>
</label>
<select name="type" id="type"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="">유형 선택</option>
<option value="input">입력값</option>
<option value="calculation">계산식</option>
<option value="range">범위별</option>
<option value="mapping">매핑</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 수식명 -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
수식명 <span class="text-red-500">*</span>
</label>
<input type="text" name="name" id="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 for="variable" class="block text-sm font-medium text-gray-700 mb-1">
변수명 <span class="text-red-500">*</span>
</label>
<input type="text" name="variable" id="variable"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
required>
<p class="text-xs text-gray-500 mt-1">대문자로 시작, 대문자/숫자/언더스코어만 사용</p>
</div>
</div>
<!-- 계산식 (type=calculation 표시) -->
<div id="formulaSection" class="hidden">
<label for="formula" class="block text-sm font-medium text-gray-700 mb-1">
계산식 <span class="text-red-500">*</span>
</label>
<textarea name="formula" id="formula" 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 font-mono"></textarea>
<div class="flex items-center gap-4 mt-2">
<button type="button" onclick="validateFormula()"
class="text-sm text-blue-600 hover:text-blue-700">
수식 검증
</button>
<span id="validateResult" class="text-sm"></span>
</div>
<p class="text-xs text-gray-500 mt-1">지원 함수: SUM, ROUND, CEIL, FLOOR, ABS, MIN, MAX, IF</p>
</div>
<!-- 사용 가능한 변수 목록 -->
<div id="variablesSection" class="hidden">
<label class="block text-sm font-medium text-gray-700 mb-2">사용 가능한 변수</label>
<div id="variablesList" class="flex flex-wrap gap-2 p-3 bg-gray-50 rounded-lg max-h-32 overflow-y-auto">
<span class="text-sm text-gray-500">변수를 불러오는 ...</span>
</div>
</div>
<!-- 설명 -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">
설명
</label>
<textarea name="description" id="description" rows="2"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 정렬 순서 -->
<div>
<label for="sort_order" class="block text-sm font-medium text-gray-700 mb-1">
정렬 순서
</label>
<input type="number" name="sort_order" id="sort_order"
min="1"
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 class="flex items-center pt-6">
<input type="checkbox" name="is_active" id="is_active" value="1"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-700">
활성화
</label>
</div>
</div>
<!-- 에러 메시지 -->
<div id="errorMessage" class="hidden bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700"></div>
<!-- 버튼 -->
<div class="flex justify-end gap-3 pt-4 border-t">
<a href="{{ route('quote-formulas.index') }}"
class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg 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>
@endsection
@push('scripts')
<script>
const formulaId = {{ $id }};
let availableVariables = [];
// 카테고리 로드
async function loadCategories() {
try {
const response = await fetch('/api/admin/quote-formulas/categories/dropdown', {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const result = await response.json();
if (result.success && result.data) {
const select = document.getElementById('category_id');
result.data.forEach(cat => {
const option = document.createElement('option');
option.value = cat.id;
option.textContent = cat.name;
select.appendChild(option);
});
}
} catch (err) {
console.error('카테고리 로드 실패:', err);
}
}
// 변수 목록 로드
async function loadVariables() {
try {
const response = await fetch('/api/admin/quote-formulas/formulas/variables', {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const result = await response.json();
if (result.success && result.data) {
availableVariables = result.data;
renderVariables();
}
} catch (err) {
console.error('변수 로드 실패:', err);
}
}
// 변수 목록 렌더링
function renderVariables() {
const container = document.getElementById('variablesList');
if (availableVariables.length === 0) {
container.innerHTML = '<span class="text-sm text-gray-500">사용 가능한 변수가 없습니다.</span>';
return;
}
container.innerHTML = availableVariables.map(v =>
`<button type="button" onclick="insertVariable('${v.variable}')"
class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-mono hover:bg-blue-200"
title="${v.name} (${v.category})">
${v.variable}
</button>`
).join('');
}
// 변수 삽입
function insertVariable(variable) {
const formulaInput = document.getElementById('formula');
const cursorPos = formulaInput.selectionStart;
const textBefore = formulaInput.value.substring(0, cursorPos);
const textAfter = formulaInput.value.substring(cursorPos);
formulaInput.value = textBefore + variable + textAfter;
formulaInput.focus();
formulaInput.setSelectionRange(cursorPos + variable.length, cursorPos + variable.length);
}
// 데이터 로드
async function loadFormula() {
await loadCategories();
await loadVariables();
try {
const response = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}`, {
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
});
const result = await response.json();
if (response.ok && result.success) {
const data = result.data;
document.getElementById('category_id').value = data.category_id || '';
document.getElementById('type').value = data.type || '';
document.getElementById('name').value = data.name || '';
document.getElementById('variable').value = data.variable || '';
document.getElementById('formula').value = data.formula || '';
document.getElementById('description').value = data.description || '';
document.getElementById('sort_order').value = data.sort_order || '';
document.getElementById('is_active').checked = data.is_active;
// 수식 유형에 따라 섹션 표시
if (data.type === 'calculation') {
document.getElementById('formulaSection').classList.remove('hidden');
document.getElementById('variablesSection').classList.remove('hidden');
}
document.getElementById('loading').classList.add('hidden');
document.getElementById('formContainer').classList.remove('hidden');
} else {
showToast(result.message || '데이터를 불러오는데 실패했습니다.', 'error');
window.location.href = '{{ route("quote-formulas.index") }}';
}
} catch (err) {
showToast('서버 오류가 발생했습니다.', 'error');
window.location.href = '{{ route("quote-formulas.index") }}';
}
}
// 수식 유형 변경
document.getElementById('type').addEventListener('change', function() {
const formulaSection = document.getElementById('formulaSection');
const variablesSection = document.getElementById('variablesSection');
if (this.value === 'calculation') {
formulaSection.classList.remove('hidden');
variablesSection.classList.remove('hidden');
} else {
formulaSection.classList.add('hidden');
variablesSection.classList.add('hidden');
}
});
// 수식 검증
async function validateFormula() {
const formula = document.getElementById('formula').value;
const resultSpan = document.getElementById('validateResult');
if (!formula) {
resultSpan.textContent = '';
return;
}
try {
const response = await fetch('/api/admin/quote-formulas/formulas/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ formula })
});
const result = await response.json();
if (result.success) {
resultSpan.innerHTML = '<span class="text-green-600">유효한 수식입니다.</span>';
} else {
resultSpan.innerHTML = `<span class="text-red-600">${result.data?.errors?.join(', ') || '유효하지 않은 수식입니다.'}</span>`;
}
} catch (err) {
resultSpan.innerHTML = '<span class="text-red-600">검증 중 오류가 발생했습니다.</span>';
}
}
// 변수명 자동 대문자 변환
document.getElementById('variable').addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^A-Z0-9_]/g, '');
});
// 폼 제출
document.getElementById('formulaForm').addEventListener('submit', async function(e) {
e.preventDefault();
const errorDiv = document.getElementById('errorMessage');
errorDiv.classList.add('hidden');
const formData = new FormData(this);
const data = {
category_id: parseInt(formData.get('category_id')),
name: formData.get('name'),
variable: formData.get('variable'),
type: formData.get('type'),
formula: formData.get('formula') || null,
description: formData.get('description') || null,
sort_order: formData.get('sort_order') ? parseInt(formData.get('sort_order')) : null,
is_active: formData.has('is_active')
};
try {
const response = await fetch(`/api/admin/quote-formulas/formulas/${formulaId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok && result.success) {
window.location.href = '{{ route("quote-formulas.index") }}';
} else {
errorDiv.textContent = result.message || '저장에 실패했습니다.';
errorDiv.classList.remove('hidden');
}
} catch (err) {
errorDiv.textContent = '서버 오류가 발생했습니다.';
errorDiv.classList.remove('hidden');
}
});
// 초기화
loadFormula();
</script>
@endpush