Files
sam-manage/resources/views/approvals/create.blade.php
김보곤 12c9ad620a feat: [approval] 결재관리 Phase 1 MVP 구현
- 모델 4개: Approval, ApprovalStep, ApprovalForm, ApprovalLine
- ApprovalService: 목록/CRUD/워크플로우(상신/승인/반려/회수) 비즈니스 로직
- ApprovalApiController: JSON API 엔드포인트 (기안함/결재함/완료함/참조함)
- ApprovalController: Blade 뷰 컨트롤러 (HX-Redirect 처리)
- 뷰 8개: drafts, pending, completed, references, create, edit, show
- partials: _status-badge, _step-progress, _approval-line-editor
- api.php/web.php 라우트 등록
2026-02-27 23:17:41 +09:00

148 lines
5.9 KiB
PHP

@extends('layouts.app')
@section('title', '기안 작성')
@section('content')
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
<h1 class="text-2xl font-bold text-gray-800">기안 작성</h1>
<a href="{{ route('approvals.drafts') }}" class="text-gray-600 hover:text-gray-800 text-sm">
&larr; 기안함으로 돌아가기
</a>
</div>
<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="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 }}">{{ $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" 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">
</div>
{{-- 긴급 여부 --}}
<div class="mb-4">
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="is_urgent" 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>
<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>
</div>
</div>
{{-- 우측: 결재선 --}}
<div class="shrink-0" style="width: 100%; max-width: 380px;">
@include('approvals.partials._approval-line-editor', [
'lines' => $lines,
'initialSteps' => [],
'selectedLineId' => '',
])
{{-- 액션 버튼 --}}
<div class="mt-4 space-y-2">
<button onclick="saveApproval('draft')"
class="w-full bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
임시저장
</button>
<button onclick="saveApproval('submit')"
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition text-sm font-medium">
상신
</button>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
async function saveApproval(action) {
const title = document.getElementById('title').value.trim();
if (!title) {
showToast('제목을 입력해주세요.', 'warning');
return;
}
const editor = document.querySelector('[x-data]').__x.$data;
const steps = editor.getStepsData();
if (action === 'submit' && steps.filter(s => s.step_type !== 'reference').length === 0) {
showToast('결재자를 1명 이상 추가해주세요.', 'warning');
return;
}
const payload = {
form_id: document.getElementById('form_id').value,
title: title,
body: document.getElementById('body').value,
is_urgent: document.getElementById('is_urgent').checked,
steps: steps,
};
try {
const response = await fetch('/api/admin/approvals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!data.success && !data.data) {
showToast(data.message || '저장에 실패했습니다.', 'error');
return;
}
if (action === 'submit') {
const submitResponse = await fetch(`/api/admin/approvals/${data.data.id}/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const submitData = await submitResponse.json();
if (submitData.success) {
showToast('결재가 상신되었습니다.', 'success');
setTimeout(() => location.href = '/approval-mgmt/drafts', 500);
} else {
showToast(submitData.message || '상신에 실패했습니다.', 'error');
}
} else {
showToast('임시저장되었습니다.', 'success');
setTimeout(() => location.href = `/approval-mgmt/${data.data.id}/edit`, 500);
}
} catch (e) {
showToast('서버 오류가 발생했습니다.', 'error');
}
}
</script>
@endpush