feat: [approval] 결재선 드롭다운 직접 배치 및 양식 본문 자동 채움

- 새기안/수정 화면에 결재선 드롭다운 추가 (모달 없이 빠른 선택)
- 양식 선택 시 body_template HTML 자동 채움 (편집기 자동 활성화)
- 모달 닫을 때 외부 드롭다운 동기화
- ApprovalForm 모델 fillable에 body_template 추가
This commit is contained in:
김보곤
2026-03-04 14:18:54 +09:00
parent a8b6a781bd
commit 25b6470555
3 changed files with 135 additions and 8 deletions

View File

@@ -35,10 +35,22 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
<div class="mb-4">
<div class="flex items-center justify-between mb-1">
<label class="block text-sm font-medium text-gray-700">결재선</label>
<button type="button" onclick="openApprovalLineModal()"
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition">
결재선 설정
</button>
<div class="flex items-center gap-2">
<select id="quick-line-select" onchange="applyQuickLine(this.value)"
class="px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-width: 160px;">
<option value="">결재선 선택</option>
@foreach($lines as $line)
<option value="{{ $line->id }}" {{ ($defaultLine?->id ?? '') == $line->id ? 'selected' : '' }}>
{{ $line->name }} ({{ count($line->steps ?? []) }}단계)
</option>
@endforeach
</select>
<button type="button" onclick="openApprovalLineModal()"
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition whitespace-nowrap">
세부 설정
</button>
</div>
</div>
<div id="approval-line-summary" class="p-3 bg-gray-50 rounded-lg border border-gray-200 flex items-center">
<span class="text-sm text-gray-400">결재선이 설정되지 않았습니다.</span>
@@ -157,6 +169,8 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
<script>
let quillInstance = null;
var summarySortableInstance = null;
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
const linesData = @json($lines);
function toggleEditor() {
const useEditor = document.getElementById('useEditor').checked;
@@ -220,6 +234,12 @@ function closeApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = 'none';
document.body.style.overflow = '';
updateApprovalLineSummary();
// 모달 내부 결재선 선택과 외부 드롭다운 동기화
const editorEl = document.getElementById('approval-line-editor');
if (editorEl && editorEl._x_dataStack) {
document.getElementById('quick-line-select').value = editorEl._x_dataStack[0].selectedLineId || '';
}
}
function updateApprovalLineSummary() {
@@ -289,6 +309,44 @@ function initSummarySortable() {
});
}
function applyQuickLine(lineId) {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const alpineData = editorEl._x_dataStack[0];
if (!lineId) {
alpineData.selectedLineId = '';
alpineData.steps = [];
} else {
alpineData.selectedLineId = lineId;
alpineData.loadLine();
}
setTimeout(updateApprovalLineSummary, 100);
}
function applyBodyTemplate(formId) {
const template = formBodyTemplates[formId];
if (!template) return;
const bodyContent = getBodyContent();
if (bodyContent.trim()) {
if (!confirm('현재 본문 내용을 양식 템플릿으로 교체하시겠습니까?')) return;
}
// 편집기 활성화 후 HTML 삽입
const useEditorEl = document.getElementById('useEditor');
if (!useEditorEl.checked) {
useEditorEl.checked = true;
toggleEditor();
}
if (quillInstance) {
quillInstance.root.innerHTML = template;
}
document.getElementById('body').value = template;
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('approval-line-modal');
@@ -300,6 +358,11 @@ function initSummarySortable() {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(updateApprovalLineSummary, 200);
// 양식 변경 시 본문 템플릿 자동 채움
document.getElementById('form_id').addEventListener('change', function() {
applyBodyTemplate(this.value);
});
});
async function saveApproval(action) {

View File

@@ -58,10 +58,22 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
<div class="mb-4">
<div class="flex items-center justify-between mb-1">
<label class="block text-sm font-medium text-gray-700">결재선</label>
<button type="button" onclick="openApprovalLineModal()"
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition">
결재선 설정
</button>
<div class="flex items-center gap-2">
<select id="quick-line-select" onchange="applyQuickLine(this.value)"
class="px-2 py-1.5 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-width: 160px;">
<option value="">결재선 선택</option>
@foreach($lines as $line)
<option value="{{ $line->id }}" {{ ($approval->line_id ?? '') == $line->id ? 'selected' : '' }}>
{{ $line->name }} ({{ count($line->steps ?? []) }}단계)
</option>
@endforeach
</select>
<button type="button" onclick="openApprovalLineModal()"
class="px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition whitespace-nowrap">
세부 설정
</button>
</div>
</div>
<div id="approval-line-summary" class="p-3 bg-gray-50 rounded-lg border border-gray-200 flex items-center">
<span class="text-sm text-gray-400">결재선이 설정되지 않았습니다.</span>
@@ -191,6 +203,8 @@ class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm fon
<script>
let quillInstance = null;
var summarySortableInstance = null;
const formBodyTemplates = @json($forms->pluck('body_template', 'id'));
const linesData = @json($lines);
function toggleEditor() {
const useEditor = document.getElementById('useEditor').checked;
@@ -254,6 +268,12 @@ function closeApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = 'none';
document.body.style.overflow = '';
updateApprovalLineSummary();
// 모달 내부 결재선 선택과 외부 드롭다운 동기화
const editorEl = document.getElementById('approval-line-editor');
if (editorEl && editorEl._x_dataStack) {
document.getElementById('quick-line-select').value = editorEl._x_dataStack[0].selectedLineId || '';
}
}
function updateApprovalLineSummary() {
@@ -323,6 +343,44 @@ function initSummarySortable() {
});
}
function applyQuickLine(lineId) {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const alpineData = editorEl._x_dataStack[0];
if (!lineId) {
alpineData.selectedLineId = '';
alpineData.steps = [];
} else {
alpineData.selectedLineId = lineId;
alpineData.loadLine();
}
setTimeout(updateApprovalLineSummary, 100);
}
function applyBodyTemplate(formId) {
const template = formBodyTemplates[formId];
if (!template) return;
// edit에서는 항상 confirm (기존 본문이 있을 가능성 높음)
const bodyContent = getBodyContent();
if (bodyContent.trim()) {
if (!confirm('현재 본문 내용을 양식 템플릿으로 교체하시겠습니까?')) return;
}
const useEditorEl = document.getElementById('useEditor');
if (!useEditorEl.checked) {
useEditorEl.checked = true;
toggleEditor();
}
if (quillInstance) {
quillInstance.root.innerHTML = template;
}
document.getElementById('body').value = template;
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('approval-line-modal');
@@ -342,6 +400,11 @@ function initSummarySortable() {
// Alpine 초기화 후 기존 결재선 요약 표시
setTimeout(updateApprovalLineSummary, 200);
// 양식 변경 시 본문 템플릿 자동 채움
document.getElementById('form_id').addEventListener('change', function() {
applyBodyTemplate(this.value);
});
});
async function updateApproval(action) {