From 267255bbe6b27fffa8459bd7261b13775a12bb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 12 Mar 2026 21:11:21 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[pmis]=20PMIS=20=EC=9E=90=EB=A3=8C?= =?UTF-8?q?=EC=8B=A4/=EC=95=88=EC=A0=84=EA=B4=80=EB=A6=AC/=ED=92=88?= =?UTF-8?q?=EC=A7=88=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자료실 하위 3개 메뉴: 자료보관함, 매뉴얼, 공지사항 - 자료보관함: 폴더 트리 + 파일 업로드/다운로드/삭제 - 매뉴얼/공지사항: 게시판형 CRUD + 첨부파일 - 안전관리: 안전보건교육, TBM현황, 위험성평가, 재해예방조치 - 품질관리: 시정조치 UI 페이지 - 대시보드: 슈퍼관리자 전용 레거시 사이트 참고 카드 - 작업일보/출면일보 오류 수정 및 기능 개선 - 설비 사진 업로드, 근로계약서 종료일 수정 --- .../Controllers/Juil/PlanningController.php | 76 ++ .../Juil/PmisArchiveController.php | 201 +++++ .../Juil/PmisDailyWorkReportController.php | 322 ++++++++ .../Controllers/Juil/PmisManualController.php | 187 +++++ .../Controllers/Juil/PmisNoticeController.php | 185 +++++ app/Models/Juil/PmisArchiveFile.php | 76 ++ app/Models/Juil/PmisArchiveFolder.php | 49 ++ app/Models/Juil/PmisManual.php | 43 + app/Models/Juil/PmisManualAttachment.php | 36 + app/Models/Juil/PmisNotice.php | 42 + app/Models/Juil/PmisNoticeAttachment.php | 36 + ...3_12_203417_create_pmis_archive_tables.php | 51 ++ ...03_12_204558_create_pmis_manuals_table.php | 41 + ...03_12_205226_create_pmis_notices_table.php | 40 + resources/views/equipment/create.blade.php | 138 +++- resources/views/esign/create.blade.php | 1 + resources/views/juil/bim-viewer.blade.php | 11 +- .../views/juil/construction-pmis.blade.php | 51 +- .../views/juil/pmis-archive-files.blade.php | 591 ++++++++++++++ .../views/juil/pmis-archive-manual.blade.php | 430 ++++++++++ .../views/juil/pmis-archive-notice.blade.php | 420 ++++++++++ .../juil/pmis-corrective-action.blade.php | 221 +++++ .../juil/pmis-daily-attendance.blade.php | 264 +++--- .../views/juil/pmis-daily-report.blade.php | 686 ++++++++++++++-- .../juil/pmis-disaster-prevention.blade.php | 230 ++++++ resources/views/juil/pmis-equipment.blade.php | 17 +- resources/views/juil/pmis-materials.blade.php | 17 +- .../views/juil/pmis-risk-assessment.blade.php | 208 +++++ .../juil/pmis-safety-education.blade.php | 765 ++++++++++++++++++ resources/views/juil/pmis-tbm.blade.php | 218 +++++ .../views/juil/pmis-work-volume.blade.php | 17 +- resources/views/juil/pmis-workforce.blade.php | 17 +- routes/web.php | 49 ++ 33 files changed, 5503 insertions(+), 233 deletions(-) create mode 100644 app/Http/Controllers/Juil/PmisArchiveController.php create mode 100644 app/Http/Controllers/Juil/PmisDailyWorkReportController.php create mode 100644 app/Http/Controllers/Juil/PmisManualController.php create mode 100644 app/Http/Controllers/Juil/PmisNoticeController.php create mode 100644 app/Models/Juil/PmisArchiveFile.php create mode 100644 app/Models/Juil/PmisArchiveFolder.php create mode 100644 app/Models/Juil/PmisManual.php create mode 100644 app/Models/Juil/PmisManualAttachment.php create mode 100644 app/Models/Juil/PmisNotice.php create mode 100644 app/Models/Juil/PmisNoticeAttachment.php create mode 100644 database/migrations/2026_03_12_203417_create_pmis_archive_tables.php create mode 100644 database/migrations/2026_03_12_204558_create_pmis_manuals_table.php create mode 100644 database/migrations/2026_03_12_205226_create_pmis_notices_table.php create mode 100644 resources/views/juil/pmis-archive-files.blade.php create mode 100644 resources/views/juil/pmis-archive-manual.blade.php create mode 100644 resources/views/juil/pmis-archive-notice.blade.php create mode 100644 resources/views/juil/pmis-corrective-action.blade.php create mode 100644 resources/views/juil/pmis-disaster-prevention.blade.php create mode 100644 resources/views/juil/pmis-risk-assessment.blade.php create mode 100644 resources/views/juil/pmis-safety-education.blade.php create mode 100644 resources/views/juil/pmis-tbm.blade.php diff --git a/app/Http/Controllers/Juil/PlanningController.php b/app/Http/Controllers/Juil/PlanningController.php index bad52423..002b9d0c 100644 --- a/app/Http/Controllers/Juil/PlanningController.php +++ b/app/Http/Controllers/Juil/PlanningController.php @@ -113,6 +113,80 @@ public function pmisDailyReport(Request $request): View|Response return view('juil.pmis-daily-report'); } + public function pmisCorrectiveAction(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.corrective-action')); + } + + return view('juil.pmis-corrective-action'); + } + + // ── 안전관리 ── + + public function pmisSafetyEducation(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.safety-education')); + } + + return view('juil.pmis-safety-education'); + } + + public function pmisTbm(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.tbm')); + } + + return view('juil.pmis-tbm'); + } + + public function pmisRiskAssessment(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.risk-assessment')); + } + + return view('juil.pmis-risk-assessment'); + } + + public function pmisDisasterPrevention(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.disaster-prevention')); + } + + return view('juil.pmis-disaster-prevention'); + } + + public function pmisArchiveFiles(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.archive-files')); + } + + return view('juil.pmis-archive-files'); + } + + public function pmisArchiveManual(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.archive-manual')); + } + + return view('juil.pmis-archive-manual'); + } + + public function pmisArchiveNotice(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.archive-notice')); + } + + return view('juil.pmis-archive-notice'); + } + public function pmisWeather(WeatherService $weatherService): JsonResponse { $forecasts = $weatherService->getWeeklyForecast(); @@ -142,6 +216,8 @@ public function pmisProfile(): JsonResponse 'created_at' => $worker->created_at?->format('Y-m-d'), 'last_login_at' => $worker->last_login_at?->format('Y-m-d H:i') ?? $user->last_login_at?->format('Y-m-d H:i'), + 'is_admin' => $user->isAdmin(), + 'is_super_admin' => $user->isSuperAdmin(), ], ]); } diff --git a/app/Http/Controllers/Juil/PmisArchiveController.php b/app/Http/Controllers/Juil/PmisArchiveController.php new file mode 100644 index 00000000..4ed261e6 --- /dev/null +++ b/app/Http/Controllers/Juil/PmisArchiveController.php @@ -0,0 +1,201 @@ +tenantId()) + ->whereNull('parent_id') + ->with('children.children.children') + ->orderBy('sort_order') + ->get(); + + return response()->json(['folders' => $folders]); + } + + public function folderStore(Request $request): JsonResponse + { + $request->validate([ + 'name' => 'required|string|max:200', + 'parent_id' => 'nullable|integer', + ]); + + $folder = PmisArchiveFolder::create([ + 'tenant_id' => $this->tenantId(), + 'parent_id' => $request->parent_id, + 'name' => $request->name, + 'sort_order' => PmisArchiveFolder::tenant($this->tenantId()) + ->where('parent_id', $request->parent_id) + ->count(), + ]); + + return response()->json(['folder' => $folder], 201); + } + + public function folderUpdate(Request $request, int $id): JsonResponse + { + $folder = PmisArchiveFolder::tenant($this->tenantId())->findOrFail($id); + + $request->validate(['name' => 'required|string|max:200']); + $folder->update(['name' => $request->name]); + + return response()->json(['folder' => $folder]); + } + + public function folderDestroy(int $id): JsonResponse + { + $folder = PmisArchiveFolder::tenant($this->tenantId())->findOrFail($id); + $descendantIds = $folder->allDescendantIds(); + + PmisArchiveFile::tenant($this->tenantId()) + ->whereIn('folder_id', $descendantIds) + ->each(function ($file) { + Storage::disk('public')->delete($file->file_path); + $file->delete(); + }); + + PmisArchiveFolder::tenant($this->tenantId()) + ->whereIn('id', $descendantIds) + ->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + /* ─── 파일 목록 ─── */ + + public function fileList(Request $request): JsonResponse + { + $folderId = $request->query('folder_id'); + $tab = $request->query('tab', '전체'); + $search = $request->query('search', ''); + $dateFrom = $request->query('date_from'); + $dateTo = $request->query('date_to'); + $includeSubfolder = $request->boolean('include_subfolder', true); + + $query = PmisArchiveFile::tenant($this->tenantId()); + + if ($folderId) { + if ($includeSubfolder) { + $folder = PmisArchiveFolder::tenant($this->tenantId())->find($folderId); + if ($folder) { + $folderIds = $folder->allDescendantIds(); + $query->whereIn('folder_id', $folderIds); + } + } else { + $query->where('folder_id', $folderId); + } + } + + if ($tab !== '전체') { + $query->where('file_type', $tab); + } + + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('original_name', 'like', "%{$search}%") + ->orWhereHas('folder', fn ($fq) => $fq->where('name', 'like', "%{$search}%")); + }); + } + + if ($dateFrom) { + $query->whereDate('created_at', '>=', $dateFrom); + } + if ($dateTo) { + $query->whereDate('created_at', '<=', $dateTo); + } + + $files = $query->orderByDesc('created_at')->get(); + + $result = $files->map(fn ($f) => [ + 'id' => $f->id, + 'title' => $f->title ?: $f->original_name, + 'fileName' => $f->original_name, + 'filePath' => Storage::disk('public')->url($f->file_path), + 'fileType' => $f->file_type, + 'siteName' => $f->site_name, + 'size' => PmisArchiveFile::formatSize($f->file_size), + 'sizeRaw' => $f->file_size, + 'registrant' => $f->registeredByUser?->name ?? '-', + 'registeredAt' => $f->created_at->format('Y-m-d'), + 'folderId' => $f->folder_id, + ]); + + return response()->json(['files' => $result]); + } + + /* ─── 파일 업로드 ─── */ + + public function fileStore(Request $request): JsonResponse + { + $request->validate([ + 'folder_id' => 'required|integer|exists:pmis_archive_folders,id', + 'files' => 'required|array|min:1', + 'files.*' => 'file|max:51200', + 'title' => 'nullable|string|max:300', + 'site_name' => 'nullable|string|max:200', + ]); + + $user = auth()->user(); + $uploaded = []; + + foreach ($request->file('files') as $file) { + $ext = $file->getClientOriginalExtension(); + $path = $file->store('pmis/archive', 'public'); + + $record = PmisArchiveFile::create([ + 'tenant_id' => $this->tenantId(), + 'folder_id' => $request->folder_id, + 'title' => $request->title ?: '', + 'original_name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_type' => PmisArchiveFile::detectFileType($ext), + 'file_size' => $file->getSize(), + 'site_name' => $request->site_name ?: '', + 'registered_by' => $user?->id, + ]); + + $uploaded[] = $record; + } + + return response()->json(['files' => $uploaded, 'message' => count($uploaded).'개 파일이 업로드되었습니다.'], 201); + } + + /* ─── 파일 삭제 ─── */ + + public function fileDestroy(int $id): JsonResponse + { + $file = PmisArchiveFile::tenant($this->tenantId())->findOrFail($id); + + Storage::disk('public')->delete($file->file_path); + $file->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + /* ─── 파일 다운로드 ─── */ + + public function fileDownload(int $id) + { + $file = PmisArchiveFile::tenant($this->tenantId())->findOrFail($id); + $fullPath = Storage::disk('public')->path($file->file_path); + + return response()->download($fullPath, $file->original_name); + } +} diff --git a/app/Http/Controllers/Juil/PmisDailyWorkReportController.php b/app/Http/Controllers/Juil/PmisDailyWorkReportController.php new file mode 100644 index 00000000..8231678c --- /dev/null +++ b/app/Http/Controllers/Juil/PmisDailyWorkReportController.php @@ -0,0 +1,322 @@ +input('date', now()->toDateString()); + $company = $request->input('company') ?? ''; + + $report = PmisDailyWorkReport::tenant($this->tenantId()) + ->where('date', $date) + ->when($company, fn ($q) => $q->where('company_name', $company)) + ->first(); + + if (! $report) { + $report = PmisDailyWorkReport::create([ + 'tenant_id' => $this->tenantId(), + 'date' => $date, + 'company_name' => $company, + 'weather' => '맑음', + 'status' => 'draft', + ]); + } + + $report->load(['workers', 'equipments', 'materials', 'volumes', 'photos']); + + return response()->json($report); + } + + public function monthStatus(Request $request): JsonResponse + { + $year = $request->integer('year', now()->year); + $month = $request->integer('month', now()->month); + $company = $request->input('company') ?? ''; + + $reports = PmisDailyWorkReport::tenant($this->tenantId()) + ->whereYear('date', $year) + ->whereMonth('date', $month) + ->when($company, fn ($q) => $q->where('company_name', $company)) + ->withCount(['workers', 'equipments', 'materials', 'volumes', 'photos']) + ->get(); + + $result = []; + foreach ($reports as $r) { + $day = (int) $r->date->format('d'); + $hasData = $r->workers_count > 0 || $r->equipments_count > 0 + || $r->materials_count > 0 || $r->volumes_count > 0 + || $r->photos_count > 0 + || $r->work_content_today; + if ($hasData) { + $result[$day] = $r->status; + } + } + + return response()->json($result); + } + + public function update(Request $request, int $id): JsonResponse + { + $report = PmisDailyWorkReport::tenant($this->tenantId())->findOrFail($id); + + $validated = $request->validate([ + 'weather' => 'sometimes|string|max:50', + 'temp_low' => 'sometimes|nullable|numeric', + 'temp_high' => 'sometimes|nullable|numeric', + 'precipitation' => 'sometimes|nullable|numeric', + 'snowfall' => 'sometimes|nullable|numeric', + 'fine_dust' => 'sometimes|nullable|string|max:50', + 'ultra_fine_dust' => 'sometimes|nullable|string|max:50', + 'work_content_today' => 'sometimes|nullable|string', + 'work_content_tomorrow' => 'sometimes|nullable|string', + 'notes' => 'sometimes|nullable|string', + 'status' => 'sometimes|in:draft,review,approved', + 'options' => 'sometimes|nullable|array', + ]); + + // NOT NULL string 컬럼의 null → 빈 문자열 변환 + foreach (['fine_dust', 'ultra_fine_dust', 'weather'] as $col) { + if (array_key_exists($col, $validated) && $validated[$col] === null) { + $validated[$col] = ''; + } + } + + $report->update($validated); + $report->load(['workers', 'equipments', 'materials', 'volumes', 'photos']); + + return response()->json($report); + } + + public function destroy(int $id): JsonResponse + { + $report = PmisDailyWorkReport::tenant($this->tenantId())->findOrFail($id); + $report->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + public function saveReviewers(Request $request, int $id): JsonResponse + { + $report = PmisDailyWorkReport::tenant($this->tenantId())->findOrFail($id); + $reviewers = $request->input('reviewers', []); + $options = $report->options ?? []; + $options['reviewers'] = $reviewers; + $report->update(['options' => $options]); + + return response()->json(['message' => '검토자가 저장되었습니다.']); + } + + // ─── Worker CRUD ─── + + public function workerStore(Request $request): JsonResponse + { + $v = $request->validate([ + 'report_id' => 'required|integer|exists:pmis_daily_work_reports,id', + 'work_type' => 'required|string|max:200', + 'job_type' => 'required|string|max:200', + 'prev_cumulative' => 'nullable|integer|min:0', + 'today_count' => 'nullable|integer|min:0', + ]); + $v['tenant_id'] = $this->tenantId(); + $v['prev_cumulative'] = $v['prev_cumulative'] ?? 0; + $v['today_count'] = $v['today_count'] ?? 0; + $v['sort_order'] = (PmisWorkReportWorker::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1; + + return response()->json(PmisWorkReportWorker::create($v), 201); + } + + public function workerUpdate(Request $request, int $id): JsonResponse + { + $w = PmisWorkReportWorker::where('tenant_id', $this->tenantId())->findOrFail($id); + $w->update($request->validate([ + 'work_type' => 'sometimes|string|max:200', + 'job_type' => 'sometimes|string|max:200', + 'prev_cumulative' => 'nullable|integer|min:0', + 'today_count' => 'nullable|integer|min:0', + ])); + + return response()->json($w); + } + + public function workerDestroy(int $id): JsonResponse + { + PmisWorkReportWorker::where('tenant_id', $this->tenantId())->findOrFail($id)->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + // ─── Equipment CRUD ─── + + public function equipmentStore(Request $request): JsonResponse + { + $v = $request->validate([ + 'report_id' => 'required|integer|exists:pmis_daily_work_reports,id', + 'equipment_name' => 'required|string|max:200', + 'specification' => 'nullable|string|max:300', + 'prev_cumulative' => 'nullable|integer|min:0', + 'today_count' => 'nullable|integer|min:0', + ]); + $v['tenant_id'] = $this->tenantId(); + $v['specification'] = $v['specification'] ?? ''; + $v['prev_cumulative'] = $v['prev_cumulative'] ?? 0; + $v['today_count'] = $v['today_count'] ?? 0; + $v['sort_order'] = (PmisWorkReportEquipment::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1; + + return response()->json(PmisWorkReportEquipment::create($v), 201); + } + + public function equipmentUpdate(Request $request, int $id): JsonResponse + { + $e = PmisWorkReportEquipment::where('tenant_id', $this->tenantId())->findOrFail($id); + $e->update($request->validate([ + 'equipment_name' => 'sometimes|string|max:200', + 'specification' => 'nullable|string|max:300', + 'prev_cumulative' => 'nullable|integer|min:0', + 'today_count' => 'nullable|integer|min:0', + ])); + + return response()->json($e); + } + + public function equipmentDestroy(int $id): JsonResponse + { + PmisWorkReportEquipment::where('tenant_id', $this->tenantId())->findOrFail($id)->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + // ─── Material CRUD ─── + + public function materialStore(Request $request): JsonResponse + { + $v = $request->validate([ + 'report_id' => 'required|integer|exists:pmis_daily_work_reports,id', + 'material_name' => 'required|string|max:200', + 'specification' => 'nullable|string|max:300', + 'unit' => 'nullable|string|max:50', + 'design_qty' => 'nullable|numeric|min:0', + 'prev_cumulative' => 'nullable|numeric|min:0', + 'today_count' => 'nullable|numeric|min:0', + ]); + $v['tenant_id'] = $this->tenantId(); + $v['sort_order'] = (PmisWorkReportMaterial::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1; + + return response()->json(PmisWorkReportMaterial::create($v), 201); + } + + public function materialUpdate(Request $request, int $id): JsonResponse + { + $m = PmisWorkReportMaterial::where('tenant_id', $this->tenantId())->findOrFail($id); + $m->update($request->validate([ + 'material_name' => 'sometimes|string|max:200', + 'specification' => 'nullable|string|max:300', + 'unit' => 'nullable|string|max:50', + 'design_qty' => 'nullable|numeric|min:0', + 'prev_cumulative' => 'nullable|numeric|min:0', + 'today_count' => 'nullable|numeric|min:0', + ])); + + return response()->json($m); + } + + public function materialDestroy(int $id): JsonResponse + { + PmisWorkReportMaterial::where('tenant_id', $this->tenantId())->findOrFail($id)->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + // ─── Volume CRUD ─── + + public function volumeStore(Request $request): JsonResponse + { + $v = $request->validate([ + 'report_id' => 'required|integer|exists:pmis_daily_work_reports,id', + 'work_type' => 'required|string|max:200', + 'sub_work_type' => 'nullable|string|max:200', + 'unit' => 'nullable|string|max:50', + 'design_qty' => 'nullable|numeric|min:0', + 'prev_cumulative' => 'nullable|numeric|min:0', + 'today_count' => 'nullable|numeric|min:0', + ]); + $v['tenant_id'] = $this->tenantId(); + $v['sort_order'] = (PmisWorkReportVolume::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1; + + return response()->json(PmisWorkReportVolume::create($v), 201); + } + + public function volumeUpdate(Request $request, int $id): JsonResponse + { + $vol = PmisWorkReportVolume::where('tenant_id', $this->tenantId())->findOrFail($id); + $vol->update($request->validate([ + 'work_type' => 'sometimes|string|max:200', + 'sub_work_type' => 'nullable|string|max:200', + 'unit' => 'nullable|string|max:50', + 'design_qty' => 'nullable|numeric|min:0', + 'prev_cumulative' => 'nullable|numeric|min:0', + 'today_count' => 'nullable|numeric|min:0', + ])); + + return response()->json($vol); + } + + public function volumeDestroy(int $id): JsonResponse + { + PmisWorkReportVolume::where('tenant_id', $this->tenantId())->findOrFail($id)->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + // ─── Photo CRUD ─── + + public function photoStore(Request $request): JsonResponse + { + $v = $request->validate([ + 'report_id' => 'required|integer|exists:pmis_daily_work_reports,id', + 'location' => 'nullable|string|max:200', + 'content' => 'nullable|string|max:500', + 'photo' => 'nullable|image|max:10240', + ]); + + $path = ''; + if ($request->hasFile('photo')) { + $path = $request->file('photo')->store('pmis/work-report-photos', 'public'); + } + + $photo = PmisWorkReportPhoto::create([ + 'tenant_id' => $this->tenantId(), + 'report_id' => $v['report_id'], + 'photo_path' => $path, + 'location' => $v['location'] ?? '', + 'content' => $v['content'] ?? '', + 'photo_date' => now()->toDateString(), + 'sort_order' => (PmisWorkReportPhoto::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1, + ]); + + return response()->json($photo, 201); + } + + public function photoDestroy(int $id): JsonResponse + { + PmisWorkReportPhoto::where('tenant_id', $this->tenantId())->findOrFail($id)->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } +} diff --git a/app/Http/Controllers/Juil/PmisManualController.php b/app/Http/Controllers/Juil/PmisManualController.php new file mode 100644 index 00000000..48cf484e --- /dev/null +++ b/app/Http/Controllers/Juil/PmisManualController.php @@ -0,0 +1,187 @@ +query('search', ''); + + $query = PmisManual::tenant($this->tenantId()) + ->with('author:id,name') + ->withCount('attachments') + ->orderByDesc('id'); + + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('tags', 'like', "%{$search}%"); + }); + } + + $manuals = $query->get()->map(fn ($m) => [ + 'id' => $m->id, + 'title' => $m->title, + 'author' => $m->author?->name ?? '관리자', + 'createdAt' => $m->created_at->format('Y-m-d'), + 'views' => $m->views, + 'hasAttachment' => $m->attachments_count > 0, + ]); + + return response()->json(['manuals' => $manuals]); + } + + public function show(int $id): JsonResponse + { + $manual = PmisManual::tenant($this->tenantId()) + ->with(['author:id,name', 'attachments']) + ->findOrFail($id); + + $manual->increment('views'); + + return response()->json([ + 'manual' => [ + 'id' => $manual->id, + 'title' => $manual->title, + 'content' => $manual->content, + 'tags' => $manual->tags, + 'author' => $manual->author?->name ?? '관리자', + 'createdAt' => $manual->created_at->format('Y-m-d'), + 'views' => $manual->views, + 'attachments' => $manual->attachments->map(fn ($a) => [ + 'id' => $a->id, + 'fileName' => $a->original_name, + 'size' => PmisManualAttachment::formatSize($a->file_size), + 'downloadUrl' => "/juil/construction-pmis/api/manuals/attachments/{$a->id}/download", + ]), + ], + ]); + } + + public function store(Request $request): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $request->validate([ + 'title' => 'required|string|max:300', + 'content' => 'nullable|string', + 'tags' => 'nullable|string|max:500', + 'files' => 'nullable|array', + 'files.*' => 'file|max:51200', + ]); + + $manual = PmisManual::create([ + 'tenant_id' => $this->tenantId(), + 'title' => $request->title, + 'content' => $request->content ?? '', + 'tags' => $request->tags ?? '', + 'author_id' => $user->id, + ]); + + if ($request->hasFile('files')) { + foreach ($request->file('files') as $file) { + $path = $file->store('pmis/manuals', 'public'); + $manual->attachments()->create([ + 'original_name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_size' => $file->getSize(), + ]); + } + } + + return response()->json(['manual' => $manual->load('attachments'), 'message' => '등록되었습니다.'], 201); + } + + public function update(Request $request, int $id): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $manual = PmisManual::tenant($this->tenantId())->findOrFail($id); + + $request->validate([ + 'title' => 'required|string|max:300', + 'content' => 'nullable|string', + 'tags' => 'nullable|string|max:500', + 'files' => 'nullable|array', + 'files.*' => 'file|max:51200', + ]); + + $manual->update([ + 'title' => $request->title, + 'content' => $request->content ?? '', + 'tags' => $request->tags ?? '', + ]); + + if ($request->hasFile('files')) { + foreach ($request->file('files') as $file) { + $path = $file->store('pmis/manuals', 'public'); + $manual->attachments()->create([ + 'original_name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_size' => $file->getSize(), + ]); + } + } + + return response()->json(['manual' => $manual->load('attachments'), 'message' => '수정되었습니다.']); + } + + public function destroy(int $id): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $manual = PmisManual::tenant($this->tenantId())->findOrFail($id); + + foreach ($manual->attachments as $att) { + Storage::disk('public')->delete($att->file_path); + } + + $manual->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + public function attachmentDownload(int $id) + { + $att = PmisManualAttachment::findOrFail($id); + $fullPath = Storage::disk('public')->path($att->file_path); + + return response()->download($fullPath, $att->original_name); + } + + public function attachmentDestroy(int $id): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $att = PmisManualAttachment::findOrFail($id); + Storage::disk('public')->delete($att->file_path); + $att->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } +} diff --git a/app/Http/Controllers/Juil/PmisNoticeController.php b/app/Http/Controllers/Juil/PmisNoticeController.php new file mode 100644 index 00000000..c12ec415 --- /dev/null +++ b/app/Http/Controllers/Juil/PmisNoticeController.php @@ -0,0 +1,185 @@ +query('search', ''); + $withTrashed = $request->boolean('with_trashed', false); + + $query = PmisNotice::tenant($this->tenantId()) + ->with('author:id,name') + ->withCount('attachments') + ->orderByDesc('id'); + + if ($withTrashed && auth()->user()->isAdmin()) { + $query->withTrashed(); + } + + if ($search) { + $query->where('title', 'like', "%{$search}%"); + } + + $notices = $query->get()->map(fn ($n) => [ + 'id' => $n->id, + 'title' => $n->title, + 'author' => $n->author?->name ?? '관리자', + 'createdAt' => $n->created_at->format('Y-m-d H:i:s'), + 'views' => $n->views, + 'hasAttachment' => $n->attachments_count > 0, + 'isDeleted' => $n->trashed(), + ]); + + return response()->json(['notices' => $notices]); + } + + public function show(int $id): JsonResponse + { + $notice = PmisNotice::tenant($this->tenantId()) + ->with(['author:id,name', 'attachments']) + ->findOrFail($id); + + $notice->increment('views'); + + return response()->json([ + 'notice' => [ + 'id' => $notice->id, + 'title' => $notice->title, + 'content' => $notice->content, + 'author' => $notice->author?->name ?? '관리자', + 'createdAt' => $notice->created_at->format('Y-m-d H:i:s'), + 'views' => $notice->views, + 'attachments' => $notice->attachments->map(fn ($a) => [ + 'id' => $a->id, + 'fileName' => $a->original_name, + 'size' => PmisNoticeAttachment::formatSize($a->file_size), + 'downloadUrl' => "/juil/construction-pmis/api/notices/attachments/{$a->id}/download", + ]), + ], + ]); + } + + public function store(Request $request): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $request->validate([ + 'title' => 'required|string|max:300', + 'content' => 'nullable|string', + 'files' => 'nullable|array', + 'files.*' => 'file|max:51200', + ]); + + $notice = PmisNotice::create([ + 'tenant_id' => $this->tenantId(), + 'title' => $request->title, + 'content' => $request->content ?? '', + 'author_id' => $user->id, + ]); + + if ($request->hasFile('files')) { + foreach ($request->file('files') as $file) { + $path = $file->store('pmis/notices', 'public'); + $notice->attachments()->create([ + 'original_name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_size' => $file->getSize(), + ]); + } + } + + return response()->json(['notice' => $notice->load('attachments'), 'message' => '등록되었습니다.'], 201); + } + + public function update(Request $request, int $id): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $notice = PmisNotice::tenant($this->tenantId())->findOrFail($id); + + $request->validate([ + 'title' => 'required|string|max:300', + 'content' => 'nullable|string', + 'files' => 'nullable|array', + 'files.*' => 'file|max:51200', + ]); + + $notice->update([ + 'title' => $request->title, + 'content' => $request->content ?? '', + ]); + + if ($request->hasFile('files')) { + foreach ($request->file('files') as $file) { + $path = $file->store('pmis/notices', 'public'); + $notice->attachments()->create([ + 'original_name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'file_size' => $file->getSize(), + ]); + } + } + + return response()->json(['notice' => $notice->load('attachments'), 'message' => '수정되었습니다.']); + } + + public function destroy(int $id): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $notice = PmisNotice::tenant($this->tenantId())->findOrFail($id); + + foreach ($notice->attachments as $att) { + Storage::disk('public')->delete($att->file_path); + } + + $notice->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } + + public function attachmentDownload(int $id) + { + $att = PmisNoticeAttachment::findOrFail($id); + $fullPath = Storage::disk('public')->path($att->file_path); + + return response()->download($fullPath, $att->original_name); + } + + public function attachmentDestroy(int $id): JsonResponse + { + $user = auth()->user(); + if (! $user->isAdmin()) { + return response()->json(['message' => '권한이 없습니다.'], 403); + } + + $att = PmisNoticeAttachment::findOrFail($id); + Storage::disk('public')->delete($att->file_path); + $att->delete(); + + return response()->json(['message' => '삭제되었습니다.']); + } +} diff --git a/app/Models/Juil/PmisArchiveFile.php b/app/Models/Juil/PmisArchiveFile.php new file mode 100644 index 00000000..f905a198 --- /dev/null +++ b/app/Models/Juil/PmisArchiveFile.php @@ -0,0 +1,76 @@ + 'array', + ]; + + public function scopeTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } + + public function folder(): BelongsTo + { + return $this->belongsTo(PmisArchiveFolder::class, 'folder_id'); + } + + public function registeredByUser(): BelongsTo + { + return $this->belongsTo(User::class, 'registered_by'); + } + + public static function detectFileType(string $extension): string + { + $ext = strtolower($extension); + $imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']; + $videoExts = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm']; + + if (in_array($ext, $imageExts)) { + return '사진'; + } + if (in_array($ext, $videoExts)) { + return '동영상'; + } + + return '문서'; + } + + public static function formatSize(int $bytes): string + { + if ($bytes >= 1073741824) { + return round($bytes / 1073741824, 1).'GB'; + } + if ($bytes >= 1048576) { + return round($bytes / 1048576, 1).'MB'; + } + if ($bytes >= 1024) { + return round($bytes / 1024, 1).'KB'; + } + + return $bytes.'B'; + } +} diff --git a/app/Models/Juil/PmisArchiveFolder.php b/app/Models/Juil/PmisArchiveFolder.php new file mode 100644 index 00000000..e12c7aa4 --- /dev/null +++ b/app/Models/Juil/PmisArchiveFolder.php @@ -0,0 +1,49 @@ + 'array', + ]; + + public function scopeTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } + + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); + } + + public function files(): HasMany + { + return $this->hasMany(PmisArchiveFile::class, 'folder_id'); + } + + public function allDescendantIds(): array + { + $ids = [$this->id]; + foreach ($this->children as $child) { + $ids = array_merge($ids, $child->allDescendantIds()); + } + + return $ids; + } +} diff --git a/app/Models/Juil/PmisManual.php b/app/Models/Juil/PmisManual.php new file mode 100644 index 00000000..2f642af3 --- /dev/null +++ b/app/Models/Juil/PmisManual.php @@ -0,0 +1,43 @@ + 'array', + ]; + + public function scopeTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } + + public function attachments(): HasMany + { + return $this->hasMany(PmisManualAttachment::class, 'manual_id'); + } +} diff --git a/app/Models/Juil/PmisManualAttachment.php b/app/Models/Juil/PmisManualAttachment.php new file mode 100644 index 00000000..9eba18dc --- /dev/null +++ b/app/Models/Juil/PmisManualAttachment.php @@ -0,0 +1,36 @@ +belongsTo(PmisManual::class, 'manual_id'); + } + + public static function formatSize(int $bytes): string + { + if ($bytes >= 1073741824) { + return round($bytes / 1073741824, 1).' G'; + } + if ($bytes >= 1048576) { + return round($bytes / 1048576, 1).' M'; + } + if ($bytes >= 1024) { + return round($bytes / 1024, 1).' K'; + } + + return $bytes.' B'; + } +} diff --git a/app/Models/Juil/PmisNotice.php b/app/Models/Juil/PmisNotice.php new file mode 100644 index 00000000..1128c349 --- /dev/null +++ b/app/Models/Juil/PmisNotice.php @@ -0,0 +1,42 @@ + 'array', + ]; + + public function scopeTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } + + public function attachments(): HasMany + { + return $this->hasMany(PmisNoticeAttachment::class, 'notice_id'); + } +} diff --git a/app/Models/Juil/PmisNoticeAttachment.php b/app/Models/Juil/PmisNoticeAttachment.php new file mode 100644 index 00000000..27e0024c --- /dev/null +++ b/app/Models/Juil/PmisNoticeAttachment.php @@ -0,0 +1,36 @@ +belongsTo(PmisNotice::class, 'notice_id'); + } + + public static function formatSize(int $bytes): string + { + if ($bytes >= 1073741824) { + return round($bytes / 1073741824, 1).' G'; + } + if ($bytes >= 1048576) { + return round($bytes / 1048576, 1).' M'; + } + if ($bytes >= 1024) { + return round($bytes / 1024, 1).' K'; + } + + return $bytes.' B'; + } +} diff --git a/database/migrations/2026_03_12_203417_create_pmis_archive_tables.php b/database/migrations/2026_03_12_203417_create_pmis_archive_tables.php new file mode 100644 index 00000000..048748a0 --- /dev/null +++ b/database/migrations/2026_03_12_203417_create_pmis_archive_tables.php @@ -0,0 +1,51 @@ +id(); + $table->unsignedBigInteger('tenant_id'); + $table->unsignedBigInteger('parent_id')->nullable(); + $table->string('name', 200); + $table->unsignedSmallInteger('sort_order')->default(0); + $table->json('options')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('parent_id'); + }); + + Schema::create('pmis_archive_files', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('tenant_id'); + $table->foreignId('folder_id')->constrained('pmis_archive_folders')->cascadeOnDelete(); + $table->string('title', 300)->default(''); + $table->string('original_name', 500); + $table->string('file_path', 500); + $table->string('file_type', 50)->default('문서'); + $table->unsignedBigInteger('file_size')->default(0); + $table->string('site_name', 200)->default(''); + $table->unsignedBigInteger('registered_by')->nullable(); + $table->json('options')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + $table->index('folder_id'); + $table->index('file_type'); + }); + } + + public function down(): void + { + Schema::dropIfExists('pmis_archive_files'); + Schema::dropIfExists('pmis_archive_folders'); + } +}; diff --git a/database/migrations/2026_03_12_204558_create_pmis_manuals_table.php b/database/migrations/2026_03_12_204558_create_pmis_manuals_table.php new file mode 100644 index 00000000..7f5b07f5 --- /dev/null +++ b/database/migrations/2026_03_12_204558_create_pmis_manuals_table.php @@ -0,0 +1,41 @@ +id(); + $table->unsignedBigInteger('tenant_id'); + $table->string('title', 300); + $table->text('content')->nullable(); + $table->string('tags', 500)->default(''); + $table->unsignedBigInteger('author_id')->nullable(); + $table->unsignedInteger('views')->default(0); + $table->json('options')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + }); + + Schema::create('pmis_manual_attachments', function (Blueprint $table) { + $table->id(); + $table->foreignId('manual_id')->constrained('pmis_manuals')->cascadeOnDelete(); + $table->string('original_name', 500); + $table->string('file_path', 500); + $table->unsignedBigInteger('file_size')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('pmis_manual_attachments'); + Schema::dropIfExists('pmis_manuals'); + } +}; diff --git a/database/migrations/2026_03_12_205226_create_pmis_notices_table.php b/database/migrations/2026_03_12_205226_create_pmis_notices_table.php new file mode 100644 index 00000000..86b3f480 --- /dev/null +++ b/database/migrations/2026_03_12_205226_create_pmis_notices_table.php @@ -0,0 +1,40 @@ +id(); + $table->unsignedBigInteger('tenant_id'); + $table->string('title', 300); + $table->text('content')->nullable(); + $table->unsignedBigInteger('author_id')->nullable(); + $table->unsignedInteger('views')->default(0); + $table->json('options')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('tenant_id'); + }); + + Schema::create('pmis_notice_attachments', function (Blueprint $table) { + $table->id(); + $table->foreignId('notice_id')->constrained('pmis_notices')->cascadeOnDelete(); + $table->string('original_name', 500); + $table->string('file_path', 500); + $table->unsignedBigInteger('file_size')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('pmis_notice_attachments'); + Schema::dropIfExists('pmis_notices'); + } +}; diff --git a/resources/views/equipment/create.blade.php b/resources/views/equipment/create.blade.php index 6c67142e..6cd31fe6 100644 --- a/resources/views/equipment/create.blade.php +++ b/resources/views/equipment/create.blade.php @@ -161,10 +161,10 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc - -