Files
sam-api/app/Services/PerformanceReportService.php
권혁성 a6e29bc1f3 feat: [품질관리] 백엔드 API 구현 - 품질관리서 + 실적신고
- 품질관리서(quality_documents) CRUD API 14개 엔드포인트
- 실적신고(performance_reports) 관리 API 6개 엔드포인트
- DB 마이그레이션 4개 테이블 (quality_documents, quality_document_orders, quality_document_locations, performance_reports)
- 모델 4개 + 서비스 2개 + 컨트롤러 2개 + FormRequest 4개
- stats() ambiguous column 버그 수정 (JOIN 시 테이블 접두사 추가)
- missing() status_code 컬럼명/값 수정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:28:53 +09:00

259 lines
8.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\Qualitys\PerformanceReport;
use App\Services\Audit\AuditLogger;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class PerformanceReportService extends Service
{
private const AUDIT_TARGET = 'performance_report';
public function __construct(
private readonly AuditLogger $auditLogger,
private readonly QualityDocumentService $qualityDocumentService
) {}
/**
* 목록 조회
*/
public function index(array $params)
{
$tenantId = $this->tenantId();
$perPage = (int) ($params['per_page'] ?? 20);
$q = trim((string) ($params['q'] ?? ''));
$year = $params['year'] ?? null;
$quarter = $params['quarter'] ?? null;
$confirmStatus = $params['confirm_status'] ?? null;
$query = PerformanceReport::query()
->where('performance_reports.tenant_id', $tenantId)
->with(['qualityDocument.client', 'qualityDocument.locations', 'confirmer:id,name']);
if ($q !== '') {
$query->whereHas('qualityDocument', function ($qq) use ($q) {
$qq->where('quality_doc_number', 'like', "%{$q}%")
->orWhere('site_name', 'like', "%{$q}%");
});
}
if ($year !== null) {
$query->where('year', $year);
}
if ($quarter !== null) {
$query->where('quarter', $quarter);
}
if ($confirmStatus !== null) {
$query->where('confirmation_status', $confirmStatus);
}
$query->orderByDesc('performance_reports.id');
$paginated = $query->paginate($perPage);
$transformedData = $paginated->getCollection()->map(fn ($report) => $this->transformToFrontend($report));
return [
'items' => $transformedData,
'current_page' => $paginated->currentPage(),
'last_page' => $paginated->lastPage(),
'per_page' => $paginated->perPage(),
'total' => $paginated->total(),
];
}
/**
* 통계 조회
*/
public function stats(array $params = []): array
{
$tenantId = $this->tenantId();
$query = PerformanceReport::where('performance_reports.tenant_id', $tenantId);
if (! empty($params['year'])) {
$query->where('performance_reports.year', $params['year']);
}
if (! empty($params['quarter'])) {
$query->where('performance_reports.quarter', $params['quarter']);
}
$counts = (clone $query)
->select('confirmation_status', DB::raw('count(*) as count'))
->groupBy('confirmation_status')
->pluck('count', 'confirmation_status')
->toArray();
$totalLocations = (clone $query)
->join('quality_documents', 'quality_documents.id', '=', 'performance_reports.quality_document_id')
->join('quality_document_locations', 'quality_document_locations.quality_document_id', '=', 'quality_documents.id')
->count('quality_document_locations.id');
return [
'total_count' => array_sum($counts),
'confirmed_count' => $counts[PerformanceReport::STATUS_CONFIRMED] ?? 0,
'unconfirmed_count' => $counts[PerformanceReport::STATUS_UNCONFIRMED] ?? 0,
'total_locations' => $totalLocations,
];
}
/**
* 일괄 확정
*/
public function confirm(array $ids)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($ids, $tenantId, $userId) {
$reports = PerformanceReport::where('tenant_id', $tenantId)
->whereIn('id', $ids)
->with(['qualityDocument'])
->get();
$errors = [];
foreach ($reports as $report) {
if ($report->isConfirmed() || $report->isReported()) {
continue;
}
// 필수정보 검증
$requiredInfo = $this->qualityDocumentService->calculateRequiredInfo($report->qualityDocument);
if ($requiredInfo !== '완료') {
$errors[] = [
'id' => $report->id,
'quality_doc_number' => $report->qualityDocument->quality_doc_number,
'reason' => $requiredInfo,
];
continue;
}
$report->update([
'confirmation_status' => PerformanceReport::STATUS_CONFIRMED,
'confirmed_date' => now()->toDateString(),
'confirmed_by' => $userId,
'updated_by' => $userId,
]);
}
if (! empty($errors)) {
throw new BadRequestHttpException(json_encode([
'message' => __('error.quality.confirm_failed'),
'errors' => $errors,
]));
}
return ['confirmed_count' => count($ids) - count($errors)];
});
}
/**
* 일괄 확정 해제
*/
public function unconfirm(array $ids)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($ids, $tenantId, $userId) {
PerformanceReport::where('tenant_id', $tenantId)
->whereIn('id', $ids)
->where('confirmation_status', PerformanceReport::STATUS_CONFIRMED)
->update([
'confirmation_status' => PerformanceReport::STATUS_UNCONFIRMED,
'confirmed_date' => null,
'confirmed_by' => null,
'updated_by' => $userId,
]);
return ['unconfirmed_count' => count($ids)];
});
}
/**
* 일괄 메모 업데이트
*/
public function updateMemo(array $ids, string $memo)
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
PerformanceReport::where('tenant_id', $tenantId)
->whereIn('id', $ids)
->update([
'memo' => $memo,
'updated_by' => $userId,
]);
return ['updated_count' => count($ids)];
}
/**
* 누락체크 (출고완료 but 제품검사 미등록)
*/
public function missing(array $params): array
{
$tenantId = $this->tenantId();
// 품질관리서가 등록된 수주 ID
$registeredOrderIds = DB::table('quality_document_orders')
->join('quality_documents', 'quality_documents.id', '=', 'quality_document_orders.quality_document_id')
->where('quality_documents.tenant_id', $tenantId)
->pluck('quality_document_orders.order_id');
// 출고완료 상태이지만 품질관리서 미등록 수주
$query = DB::table('orders')
->where('tenant_id', $tenantId)
->whereNotIn('id', $registeredOrderIds)
->where('status_code', 'SHIPPED'); // TODO: 출고완료 상태 추가 시 상수 확인
if (! empty($params['year'])) {
$query->whereYear('created_at', $params['year']);
}
if (! empty($params['quarter'])) {
$quarter = (int) $params['quarter'];
$startMonth = ($quarter - 1) * 3 + 1;
$endMonth = $quarter * 3;
$query->whereMonth('created_at', '>=', $startMonth)
->whereMonth('created_at', '<=', $endMonth);
}
return $query->orderByDesc('id')
->limit(100)
->get()
->map(fn ($order) => [
'id' => $order->id,
'order_number' => $order->order_no ?? '',
'site_name' => $order->site_name ?? '',
'client' => '', // 별도 조인 필요
'delivery_date' => $order->delivery_date ?? '',
])
->toArray();
}
/**
* DB → 프론트엔드 변환
*/
private function transformToFrontend(PerformanceReport $report): array
{
$doc = $report->qualityDocument;
return [
'id' => $report->id,
'quality_doc_number' => $doc?->quality_doc_number ?? '',
'created_date' => $report->created_at?->format('Y-m-d') ?? '',
'site_name' => $doc?->site_name ?? '',
'client' => $doc?->client?->name ?? '',
'location_count' => $doc?->locations?->count() ?? 0,
'required_info' => $doc ? $this->qualityDocumentService->calculateRequiredInfo($doc) : '',
'confirm_status' => $report->confirmation_status === PerformanceReport::STATUS_CONFIRMED ? 'confirmed' : 'unconfirmed',
'confirm_date' => $report->confirmed_date?->format('Y-m-d'),
'memo' => $report->memo ?? '',
'year' => $report->year,
'quarter' => $report->quarter,
];
}
}