Files
sam-api/app/Services/ChecklistTemplateService.php
권혁성 5448f0e57d deploy: 2026-03-12 배포
- feat: [barobill] 바로빌 카드/은행/홈택스 REST API 구현
- feat: [equipment] 설비관리 API 백엔드 구현
- feat: [payroll] 급여관리 계산 엔진 및 일괄 처리 API
- feat: [QMS] 점검표 템플릿 관리 + 로트심사 개선
- feat: [생산/출하] 수주 단위 출하 자동생성 + 상태 흐름 개선
- feat: [receiving] 입고 성적서 파일 연결
- feat: [견적] 제어기 타입 체계 변경
- feat: [email] 테넌트 메일 설정 마이그레이션 및 모델
- feat: [pmis] 시공관리 테이블 마이그레이션
- feat: [R2] 파일 업로드 커맨드 + filesystems 설정
- feat: [배포] Jenkinsfile 롤백 기능 추가
- fix: [approval] SAM API 규칙 준수 코드 개선
- fix: [account-codes] 계정과목 중복 데이터 정리
- fix: [payroll] 일괄 생성 시 삭제된 사용자 건너뛰기
- fix: [db] codebridge DB 분리 후 깨진 FK 제약조건 제거
- refactor: [barobill] 바로빌 연동 코드 전면 개선
2026-03-12 15:20:20 +09:00

257 lines
8.0 KiB
PHP

<?php
namespace App\Services;
use App\Models\Commons\File;
use App\Models\Qualitys\ChecklistTemplate;
use App\Services\Audit\AuditLogger;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ChecklistTemplateService extends Service
{
private const AUDIT_TARGET = 'checklist_template';
private const DOCUMENT_TYPE = 'checklist_template';
public function __construct(
private readonly AuditLogger $auditLogger
) {}
/**
* 템플릿 조회 (type별)
*/
public function getByType(string $type): array
{
$template = ChecklistTemplate::query()
->where('type', $type)
->first();
if (! $template) {
throw new NotFoundHttpException(__('error.not_found'));
}
// 각 항목별 파일 수 포함
$fileCounts = File::query()
->where('document_type', self::DOCUMENT_TYPE)
->where('document_id', $template->id)
->whereNull('deleted_at')
->selectRaw('field_key, COUNT(*) as count')
->groupBy('field_key')
->pluck('count', 'field_key')
->toArray();
return [
'id' => $template->id,
'name' => $template->name,
'type' => $template->type,
'categories' => $template->categories,
'options' => $template->options,
'file_counts' => $fileCounts,
'updated_at' => $template->updated_at?->toIso8601String(),
'updated_by' => $template->updater?->name,
];
}
/**
* 템플릿 저장 (전체 덮어쓰기)
*/
public function save(int $id, array $data): array
{
$template = ChecklistTemplate::findOrFail($id);
$before = $template->toArray();
// 삭제된 항목의 파일 처리
$oldSubItemIds = $template->getAllSubItemIds();
$newSubItemIds = $this->extractSubItemIds($data['categories']);
$removedIds = array_diff($oldSubItemIds, $newSubItemIds);
DB::transaction(function () use ($template, $data, $removedIds) {
// 템플릿 업데이트
$template->update([
'name' => $data['name'] ?? $template->name,
'categories' => $data['categories'],
'options' => $data['options'] ?? $template->options,
'updated_by' => $this->apiUserId(),
]);
// 삭제된 항목의 파일 → soft delete
if (! empty($removedIds)) {
$orphanFiles = File::query()
->where('document_type', self::DOCUMENT_TYPE)
->where('document_id', $template->id)
->whereIn('field_key', $removedIds)
->get();
foreach ($orphanFiles as $file) {
$file->softDeleteFile($this->apiUserId());
}
}
});
$template->refresh();
$this->auditLogger->log(
$this->tenantId(),
self::AUDIT_TARGET,
$template->id,
'updated',
$before,
$template->toArray()
);
return $this->getByType($template->type);
}
/**
* 항목 완료 토글
*/
public function toggleItem(int $id, string $subItemId): array
{
$template = ChecklistTemplate::findOrFail($id);
$categories = $template->categories;
$toggled = null;
foreach ($categories as &$category) {
if (empty($category['subItems'])) {
continue;
}
foreach ($category['subItems'] as &$subItem) {
if ($subItem['id'] === $subItemId) {
$subItem['is_completed'] = ! ($subItem['is_completed'] ?? false);
$subItem['completed_at'] = $subItem['is_completed'] ? now()->toIso8601String() : null;
$toggled = $subItem;
break 2;
}
}
unset($subItem);
}
unset($category);
if (! $toggled) {
throw new NotFoundHttpException(__('error.not_found'));
}
$template->update([
'categories' => $categories,
'updated_by' => $this->apiUserId(),
]);
return [
'id' => $toggled['id'],
'name' => $toggled['name'],
'is_completed' => $toggled['is_completed'],
'completed_at' => $toggled['completed_at'],
];
}
/**
* 항목별 파일 목록 조회
*/
public function getDocuments(int $templateId, ?string $subItemId = null): array
{
$query = File::query()
->where('document_type', self::DOCUMENT_TYPE)
->where('document_id', $templateId)
->with('uploader:id,name');
if ($subItemId) {
$query->where('field_key', $subItemId);
}
$files = $query->orderBy('field_key')->orderByDesc('id')->get();
return $files->map(fn (File $file) => [
'id' => $file->id,
'field_key' => $file->field_key,
'display_name' => $file->display_name ?? $file->original_name,
'file_size' => $file->file_size,
'mime_type' => $file->mime_type,
'uploaded_by' => $file->uploader?->name,
'created_at' => $file->created_at?->toIso8601String(),
])->toArray();
}
/**
* 파일 업로드 (polymorphic)
*/
public function uploadDocument(int $templateId, string $subItemId, $uploadedFile): array
{
$template = ChecklistTemplate::findOrFail($templateId);
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
// 저장 경로: {tenant_id}/checklist-templates/{year}/{month}/{stored_name}
$date = now();
$storedName = bin2hex(random_bytes(16)).'.'.$uploadedFile->getClientOriginalExtension();
$filePath = sprintf(
'%d/checklist-templates/%s/%s/%s',
$tenantId,
$date->format('Y'),
$date->format('m'),
$storedName
);
// 파일 저장
Storage::disk('r2')->put($filePath, file_get_contents($uploadedFile->getPathname()));
// DB 레코드 생성
$file = File::create([
'tenant_id' => $tenantId,
'document_type' => self::DOCUMENT_TYPE,
'document_id' => $template->id,
'field_key' => $subItemId,
'display_name' => $uploadedFile->getClientOriginalName(),
'stored_name' => $storedName,
'file_path' => $filePath,
'file_size' => $uploadedFile->getSize(),
'mime_type' => $uploadedFile->getClientMimeType(),
'uploaded_by' => $userId,
'created_by' => $userId,
]);
return [
'id' => $file->id,
'field_key' => $file->field_key,
'display_name' => $file->display_name,
'file_size' => $file->file_size,
'mime_type' => $file->mime_type,
'created_at' => $file->created_at?->toIso8601String(),
];
}
/**
* 파일 삭제
* - 교체(replace=true): hard delete (물리 파일 + DB)
* - 일반 삭제: soft delete (휴지통)
*/
public function deleteDocument(int $fileId, bool $replace = false): void
{
$file = File::query()
->where('document_type', self::DOCUMENT_TYPE)
->findOrFail($fileId);
if ($replace) {
$file->permanentDelete();
} else {
$file->softDeleteFile($this->apiUserId());
}
}
/**
* categories JSON에서 sub_item_id 목록 추출
*/
private function extractSubItemIds(array $categories): array
{
$ids = [];
foreach ($categories as $category) {
foreach ($category['subItems'] ?? [] as $subItem) {
$ids[] = $subItem['id'];
}
}
return $ids;
}
}