feat: [approvals] 기안 작성/수정 결재선을 모달로 전환

- 2열 레이아웃(양식 50% + 결재선 50%)을 1열 풀와이드로 변경
- 결재선 편집기를 모달로 이동, 메인 화면에 요약 바만 표시
- ESC 키로 모달 닫기 지원
- edit 페이지 로드 시 기존 결재선 요약 즉시 표시
This commit is contained in:
김보곤
2026-02-28 14:41:37 +09:00
parent 40534498b3
commit cfdb1044fb
2 changed files with 268 additions and 116 deletions

View File

@@ -19,7 +19,7 @@
</div>
@if($approval->status === 'rejected')
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-6 rounded">
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-6 rounded" style="max-width: 960px; margin-left: auto; margin-right: auto;">
<div class="flex items-center">
<span class="text-red-700 font-medium">반려됨</span>
</div>
@@ -35,87 +35,118 @@
</div>
@endif
<div class="flex flex-col lg:flex-row gap-6">
{{-- 좌측: 양식 --}}
<div class="flex-1 min-w-0">
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">문서 내용</h2>
<div class="mx-auto" style="max-width: 960px;">
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">문서 내용</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">양식 <span class="text-red-500">*</span></label>
<select id="form_id" 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">
@foreach($forms as $form)
<option value="{{ $form->id }}" {{ $approval->form_id == $form->id ? 'selected' : '' }}>{{ $form->name }}</option>
@endforeach
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">양식 <span class="text-red-500">*</span></label>
<select id="form_id" 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">
@foreach($forms as $form)
<option value="{{ $form->id }}" {{ $approval->form_id == $form->id ? 'selected' : '' }}>{{ $form->name }}</option>
@endforeach
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">제목 <span class="text-red-500">*</span></label>
<input type="text" id="title" maxlength="200" value="{{ $approval->title }}"
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">
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">제목 <span class="text-red-500">*</span></label>
<input type="text" id="title" maxlength="200" value="{{ $approval->title }}"
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">
</div>
<div class="mb-4">
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="is_urgent" {{ $approval->is_urgent ? 'checked' : '' }}
class="rounded border-gray-300 text-red-600 focus:ring-red-500">
<span class="text-sm text-gray-700">긴급</span>
<div class="mb-4">
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="is_urgent" {{ $approval->is_urgent ? 'checked' : '' }}
class="rounded border-gray-300 text-red-600 focus:ring-red-500">
<span class="text-sm text-gray-700">긴급</span>
</label>
</div>
<div class="mb-4">
<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>
</div>
</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 class="mb-4">
<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 class="border-t pt-4 mt-4">
<div class="flex items-center justify-between mb-2">
<label class="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>
<div id="approval-line-summary" class="text-sm text-gray-400">
결재선이 설정되지 않았습니다.
</div>
</div>
</div>
{{-- 우측: 결재선 --}}
<div class="shrink-0" style="width: 100%; max-width: 680px;">
@php
$initialSteps = $approval->steps->map(fn($s) => [
'user_id' => $s->approver_id,
'user_name' => $s->approver_name ?? ($s->approver?->name ?? ''),
'department' => $s->approver_department ?? '',
'position' => $s->approver_position ?? '',
'step_type' => $s->step_type,
])->toArray();
@endphp
@include('approvals.partials._approval-line-editor', [
'lines' => $lines,
'initialSteps' => $initialSteps,
'selectedLineId' => $approval->line_id ?? '',
])
<div class="mt-4 space-y-2">
{{-- 액션 버튼 --}}
<div class="border-t pt-4 mt-4 flex gap-2 justify-end">
<button onclick="updateApproval('save')"
class="w-full bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
저장
</button>
<button onclick="updateApproval('submit')"
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
{{ $approval->status === 'rejected' ? '재상신' : '상신' }}
</button>
@if($approval->isDeletable())
<button onclick="deleteApproval()"
class="w-full bg-red-100 hover:bg-red-200 text-red-700 px-4 py-2 rounded-lg transition text-sm font-medium">
class="bg-red-100 hover:bg-red-200 text-red-700 px-6 py-2 rounded-lg transition text-sm font-medium">
삭제
</button>
@endif
</div>
</div>
</div>
{{-- 결재선 모달 --}}
<div id="approval-line-modal" style="display: none;" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" onclick="closeApprovalLineModal()"></div>
<div class="relative flex items-center justify-center min-h-full p-4">
<div class="bg-white rounded-xl shadow-2xl w-full overflow-hidden relative" style="max-width: 720px;">
<button type="button" onclick="closeApprovalLineModal()"
class="absolute top-3 right-3 z-10 p-1 text-gray-400 hover:text-gray-600 transition bg-white rounded-full shadow-sm">
<svg class="w-5 h-5" 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 class="overflow-y-auto" style="max-height: 70vh;">
@php
$initialSteps = $approval->steps->map(fn($s) => [
'user_id' => $s->approver_id,
'user_name' => $s->approver_name ?? ($s->approver?->name ?? ''),
'department' => $s->approver_department ?? '',
'position' => $s->approver_position ?? '',
'step_type' => $s->step_type,
])->toArray();
@endphp
@include('approvals.partials._approval-line-editor', [
'lines' => $lines,
'initialSteps' => $initialSteps,
'selectedLineId' => $approval->line_id ?? '',
])
</div>
<div class="px-5 py-3 border-t border-gray-200 flex justify-end">
<button type="button" onclick="closeApprovalLineModal()"
class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition">
확인
</button>
</div>
</div>
</div>
</div>
@endsection
@push('styles')
@@ -185,13 +216,58 @@ function getBodyContent() {
return document.getElementById('body').value;
}
// 기존 HTML body 자동 감지 → 편집기 자동 활성화
function openApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = '';
document.body.style.overflow = 'hidden';
}
function closeApprovalLineModal() {
document.getElementById('approval-line-modal').style.display = 'none';
document.body.style.overflow = '';
updateApprovalLineSummary();
}
function updateApprovalLineSummary() {
const editorEl = document.getElementById('approval-line-editor');
if (!editorEl || !editorEl._x_dataStack) return;
const steps = editorEl._x_dataStack[0].steps;
const summaryEl = document.getElementById('approval-line-summary');
if (!steps || steps.length === 0) {
summaryEl.innerHTML = '<span class="text-gray-400">결재선이 설정되지 않았습니다.</span>';
return;
}
const typeLabels = { approval: '결재', agreement: '합의', reference: '참조' };
const parts = steps.map(s => {
const label = typeLabels[s.step_type] || s.step_type;
return '<span class="inline-flex items-center gap-1"><strong class="text-gray-800">' + s.user_name + '</strong> <span class="text-gray-400 text-xs">(' + label + ')</span></span>';
});
summaryEl.innerHTML = '<div class="flex flex-wrap items-center gap-1 text-sm">' +
parts.join('<span class="text-gray-300 mx-0.5">&rarr;</span>') + '</div>';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('approval-line-modal');
if (modal && modal.style.display !== 'none') {
closeApprovalLineModal();
}
}
});
// 기존 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();
}
// Alpine 초기화 후 기존 결재선 요약 표시
setTimeout(updateApprovalLineSummary, 200);
});
async function updateApproval(action) {