Files
sam-manage/app/Http/Controllers/RdController.php
김보곤 64ab20becf feat: [rd] 기획디자인 플래닝 캔버스 페이지 추가
- 연구개발 > 기획디자인 메뉴 라우트/컨트롤러/뷰 추가
- Alpine.js 기반 캔버스 도구 (노드 배치, 연결, 줌/팬)
- 16종 노드 타입 (기획/분석/구조/산출물 카테고리)
- 타임라인/플로우 뷰 모드, 프로젝트 저장/불러오기
- 실행취소/재실행, 키보드 단축키 지원
2026-03-07 22:06:06 +09:00

317 lines
9.5 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\HR\Employee;
use App\Models\Rd\AiQuotation;
use App\Models\Tenants\Department;
use App\Models\Tenants\Tenant;
use App\Services\Rd\AiQuotationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class RdController extends Controller
{
public function __construct(
private readonly AiQuotationService $quotationService
) {}
/**
* R&D 대시보드
*/
public function index(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.index'));
}
$dashboard = $this->quotationService->getDashboardStats();
$statuses = AiQuotation::getStatuses();
return view('rd.index', compact('dashboard', 'statuses'));
}
/**
* 조직도 관리
*/
public function orgChart(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.org-chart'));
}
$tenantId = session('selected_tenant_id');
// 부서 트리 (parent_id=null이 최상위)
$departments = Department::where('tenant_id', $tenantId)
->where('is_active', true)
->orderBy('sort_order')
->orderBy('name')
->get();
// 전체 직원 (활성 상태)
$rawEmployees = Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('employee_status', 'active')
->with(['user', 'department'])
->orderBy('display_name')
->get();
// Blade @json 호환을 위해 미리 배열로 변환
$employees = $rawEmployees->map(function ($e) {
return [
'id' => $e->id,
'user_id' => $e->user_id,
'department_id' => $e->department_id,
'display_name' => $e->display_name ?? $e->user?->name ?? '(이름없음)',
'position_label' => $e->position_label,
];
})->values();
// 회사 정보 (조직도 최상단)
$tenant = Tenant::find($tenantId);
$companyName = $tenant->company_name ?? 'SAM';
$ceoName = $tenant->ceo_name ?? '';
return view('rd.org-chart', compact('departments', 'employees', 'companyName', 'ceoName'));
}
/**
* 조직도 - 직원 부서 배치
*/
public function orgChartAssign(Request $request): JsonResponse
{
$request->validate([
'employee_id' => 'required|integer',
'department_id' => 'required|integer',
]);
$tenantId = session('selected_tenant_id');
$employee = Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('id', $request->employee_id)
->first();
if (! $employee) {
return response()->json(['success' => false, 'message' => '직원을 찾을 수 없습니다.'], 404);
}
$employee->department_id = $request->department_id;
$employee->save();
return response()->json(['success' => true]);
}
/**
* 조직도 - 직원 부서 해제 (미배치로 이동)
*/
public function orgChartUnassign(Request $request): JsonResponse
{
$request->validate([
'employee_id' => 'required|integer',
]);
$tenantId = session('selected_tenant_id');
$employee = Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('id', $request->employee_id)
->first();
if (! $employee) {
return response()->json(['success' => false, 'message' => '직원을 찾을 수 없습니다.'], 404);
}
$employee->department_id = null;
$employee->save();
return response()->json(['success' => true]);
}
/**
* 조직도 - 부서 내 직원 순서/이동 일괄 처리
*/
public function orgChartReorder(Request $request): JsonResponse
{
$request->validate([
'moves' => 'required|array',
'moves.*.employee_id' => 'required|integer',
'moves.*.department_id' => 'nullable|integer',
]);
$tenantId = session('selected_tenant_id');
foreach ($request->moves as $move) {
Employee::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('id', $move['employee_id'])
->update(['department_id' => $move['department_id']]);
}
return response()->json(['success' => true]);
}
/**
* 조직도 - 부서 순서 변경 (드래그 앤 드롭)
*/
public function orgChartReorderDepts(Request $request): JsonResponse
{
$request->validate([
'orders' => 'required|array',
'orders.*.id' => 'required|integer',
'orders.*.parent_id' => 'nullable|integer',
'orders.*.sort_order' => 'required|integer',
]);
$tenantId = session('selected_tenant_id');
foreach ($request->orders as $order) {
Department::where('tenant_id', $tenantId)
->where('id', $order['id'])
->update([
'parent_id' => $order['parent_id'],
'sort_order' => $order['sort_order'],
]);
}
return response()->json(['success' => true]);
}
/**
* 조직도 - 부서 숨기기/표시 토글
*/
public function orgChartToggleHide(Request $request): JsonResponse
{
$request->validate([
'department_id' => 'required|integer',
'hidden' => 'required|boolean',
]);
$tenantId = session('selected_tenant_id');
$dept = Department::where('tenant_id', $tenantId)
->where('id', $request->department_id)
->first();
if (! $dept) {
return response()->json(['success' => false, 'message' => '부서를 찾을 수 없습니다.'], 404);
}
$options = $dept->options ?? [];
$options['orgchart_hidden'] = $request->hidden;
$dept->options = $options;
$dept->save();
return response()->json(['success' => true]);
}
/**
* 중대재해처벌법 실무 점검
*/
public function safetyAudit(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.safety-audit'));
}
return view('rd.safety-audit');
}
/**
* AI 견적 목록
*/
public function quotations(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.index'));
}
$statuses = AiQuotation::getStatuses();
return view('rd.ai-quotation.index', compact('statuses'));
}
/**
* AI 견적 생성 폼
*/
public function createQuotation(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.create'));
}
return view('rd.ai-quotation.create');
}
/**
* AI 견적 문서 (인쇄용 견적서)
*/
public function documentQuotation(Request $request, int $id): View
{
$quotation = $this->quotationService->getById($id);
if (! $quotation || ! $quotation->isCompleted()) {
abort(404, '완료된 견적만 문서로 조회할 수 있습니다.');
}
$template = $request->query('template', 'classic');
$allowed = ['classic', 'modern', 'blue', 'dark', 'colorful'];
if (! in_array($template, $allowed)) {
$template = 'classic';
}
return view('rd.ai-quotation.document', compact('quotation', 'template'));
}
/**
* AI 견적 상세
*/
public function showQuotation(Request $request, int $id): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.show', $id));
}
$quotation = $this->quotationService->getById($id);
if (! $quotation) {
abort(404, 'AI 견적을 찾을 수 없습니다.');
}
return view('rd.ai-quotation.show', compact('quotation'));
}
/**
* AI 견적 편집 (제조 모드)
*/
public function editQuotation(Request $request, int $id): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.edit', $id));
}
$quotation = $this->quotationService->getById($id);
if (! $quotation) {
abort(404, 'AI 견적을 찾을 수 없습니다.');
}
if (! $quotation->isCompleted()) {
abort(403, '완료된 견적만 편집할 수 있습니다.');
}
return view('rd.ai-quotation.edit', compact('quotation'));
}
/**
* 기획디자인 - 플래닝 캔버스
*/
public function planningDesign(Request $request): View|\Illuminate\Http\Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('rd.planning-design'));
}
return view('rd.planning-design.index');
}
}