feat: [approvals] 기안 본문 Quill.js 편집기 토글 기능 추가

- create/edit: 본문 라벨 옆 편집기 체크박스 + Quill.js v2 WYSIWYG 에디터
- edit: 기존 HTML body 자동 감지 → 편집기 자동 활성화
- show: HTML body 안전 렌더링 (strip_tags), plain text는 기존 방식 유지
- textarea ↔ Quill 토글 시 내용 상호 이관
This commit is contained in:
김보곤
2026-02-28 14:18:16 +09:00
parent 60aef7992b
commit c58ca65dc7
3 changed files with 166 additions and 5 deletions

View File

@@ -43,10 +43,18 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-
{{-- 본문 --}}
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">본문</label>
<label class="block text-sm font-medium text-gray-700 mb-1">
본문
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
<input type="checkbox" id="useEditor" onchange="toggleEditor()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-xs text-gray-500 font-normal">편집기</span>
</label>
</label>
<textarea id="body" rows="12" placeholder="기안 내용을 입력하세요..."
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-height: 300px;"></textarea>
<div id="quill-container" style="display: none; min-height: 300px;"></div>
</div>
</div>
</div>
@@ -77,8 +85,73 @@ class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg tran
</div>
@endsection
@push('styles')
<link href="https://cdn.quilljs.com/2.0.3/quill.snow.css" rel="stylesheet">
<style>
#quill-container .ql-editor { min-height: 260px; font-size: 0.875rem; }
#quill-container .ql-toolbar { border-radius: 0.5rem 0.5rem 0 0; border-color: #d1d5db; }
#quill-container .ql-container { border-radius: 0 0 0.5rem 0.5rem; border-color: #d1d5db; }
</style>
@endpush
@push('scripts')
<script src="https://cdn.quilljs.com/2.0.3/quill.js"></script>
<script>
let quillInstance = null;
function toggleEditor() {
const useEditor = document.getElementById('useEditor').checked;
const textarea = document.getElementById('body');
const container = document.getElementById('quill-container');
if (useEditor) {
container.style.display = '';
textarea.style.display = 'none';
if (!quillInstance) {
quillInstance = new Quill('#quill-container', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'align': [] }],
['blockquote', 'code-block'],
['link'],
['clean'],
],
},
placeholder: '기안 내용을 입력하세요...',
});
}
const text = textarea.value.trim();
if (text) {
if (/<[a-z][\s\S]*>/i.test(text)) {
quillInstance.root.innerHTML = text;
} else {
quillInstance.setText(text);
}
}
} else {
container.style.display = 'none';
textarea.style.display = '';
if (quillInstance) {
const html = quillInstance.root.innerHTML;
textarea.value = (html === '<p><br></p>') ? '' : html;
}
}
}
function getBodyContent() {
const useEditor = document.getElementById('useEditor')?.checked;
if (useEditor && quillInstance) {
const html = quillInstance.root.innerHTML;
return (html === '<p><br></p>') ? '' : html;
}
return document.getElementById('body').value;
}
async function saveApproval(action) {
const title = document.getElementById('title').value.trim();
if (!title) {
@@ -97,7 +170,7 @@ class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg tran
const payload = {
form_id: document.getElementById('form_id').value,
title: title,
body: document.getElementById('body').value,
body: getBodyContent(),
is_urgent: document.getElementById('is_urgent').checked,
steps: steps,
};

View File

@@ -65,10 +65,18 @@ class="rounded border-gray-300 text-red-600 focus:ring-red-500">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">본문</label>
<label class="block text-sm font-medium text-gray-700 mb-1">
본문
<label class="inline-flex items-center gap-1 ml-3 cursor-pointer">
<input type="checkbox" id="useEditor" onchange="toggleEditor()"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-xs text-gray-500 font-normal">편집기</span>
</label>
</label>
<textarea id="body" rows="12"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
style="min-height: 300px;">{{ $approval->body }}</textarea>
<div id="quill-container" style="display: none; min-height: 300px;"></div>
</div>
</div>
</div>
@@ -110,8 +118,82 @@ class="w-full bg-red-100 hover:bg-red-200 text-red-700 px-4 py-2 rounded-lg tran
</div>
@endsection
@push('styles')
<link href="https://cdn.quilljs.com/2.0.3/quill.snow.css" rel="stylesheet">
<style>
#quill-container .ql-editor { min-height: 260px; font-size: 0.875rem; }
#quill-container .ql-toolbar { border-radius: 0.5rem 0.5rem 0 0; border-color: #d1d5db; }
#quill-container .ql-container { border-radius: 0 0 0.5rem 0.5rem; border-color: #d1d5db; }
</style>
@endpush
@push('scripts')
<script src="https://cdn.quilljs.com/2.0.3/quill.js"></script>
<script>
let quillInstance = null;
function toggleEditor() {
const useEditor = document.getElementById('useEditor').checked;
const textarea = document.getElementById('body');
const container = document.getElementById('quill-container');
if (useEditor) {
container.style.display = '';
textarea.style.display = 'none';
if (!quillInstance) {
quillInstance = new Quill('#quill-container', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'align': [] }],
['blockquote', 'code-block'],
['link'],
['clean'],
],
},
placeholder: '기안 내용을 입력하세요...',
});
}
const text = textarea.value.trim();
if (text) {
if (/<[a-z][\s\S]*>/i.test(text)) {
quillInstance.root.innerHTML = text;
} else {
quillInstance.setText(text);
}
}
} else {
container.style.display = 'none';
textarea.style.display = '';
if (quillInstance) {
const html = quillInstance.root.innerHTML;
textarea.value = (html === '<p><br></p>') ? '' : html;
}
}
}
function getBodyContent() {
const useEditor = document.getElementById('useEditor')?.checked;
if (useEditor && quillInstance) {
const html = quillInstance.root.innerHTML;
return (html === '<p><br></p>') ? '' : html;
}
return document.getElementById('body').value;
}
// 기존 HTML body 자동 감지 → 편집기 자동 활성화
document.addEventListener('DOMContentLoaded', function () {
const existingBody = document.getElementById('body').value;
if (/<[a-z][\s\S]*>/i.test(existingBody)) {
document.getElementById('useEditor').checked = true;
toggleEditor();
}
});
async function updateApproval(action) {
const title = document.getElementById('title').value.trim();
if (!title) {
@@ -130,7 +212,7 @@ class="w-full bg-red-100 hover:bg-red-200 text-red-700 px-4 py-2 rounded-lg tran
const payload = {
form_id: document.getElementById('form_id').value,
title: title,
body: document.getElementById('body').value,
body: getBodyContent(),
is_urgent: document.getElementById('is_urgent').checked,
steps: steps,
};

View File

@@ -73,7 +73,13 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition
<div class="border-t pt-4">
<h2 class="text-lg font-semibold text-gray-800 mb-2">{{ $approval->title }}</h2>
<div class="prose prose-sm max-w-none text-gray-700 whitespace-pre-wrap">{{ $approval->body ?? '(내용 없음)' }}</div>
@if($approval->body && preg_match('/<[a-z][\s\S]*>/i', $approval->body))
<div class="prose prose-sm max-w-none text-gray-700">
{!! strip_tags($approval->body, '<p><br><strong><b><em><i><u><s><del><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><span><div><table><thead><tbody><tr><th><td>') !!}
</div>
@else
<div class="prose prose-sm max-w-none text-gray-700 whitespace-pre-wrap">{{ $approval->body ?? '(내용 없음)' }}</div>
@endif
</div>
</div>