Files
sam-docs/dev/dev_plans/quality/quality-management-plan.md

40 KiB

품질관리 기능 개발 계획

작성일: 2026-03-05 목적: 스토리보드 D1.9 기반 품질관리 백엔드 API 구현 + 프론트엔드 API 연동 기준 문서: docs/dev/dev_plans/quality/quality-management-storyboard-analysis.md 상태: 진행중


현재 진행 상태

항목 내용
마지막 완료 작업 Phase 4: 프론트엔드 API 연동 완료
다음 작업 E2E 통합 테스트 + Swagger 문서
진행률 17/19 (89%) - E2E 테스트 + Swagger 제외
마지막 업데이트 2026-03-05

1. 개요

1.1 배경

품질관리 프론트엔드가 Mock 데이터 기반으로 이미 완성되어 있으나, 백엔드 API가 없어 실제 데이터 연동이 불가능한 상태.

현재 상태:

영역 프론트 백엔드 비고
제품검사관리 완성 (Mock) 없음 InspectionManagement/
실적신고관리 완성 (Mock) 없음 PerformanceReportManagement/
품질인정심사 완성 (Mock) 없음 qms/ → 후속 TODO
  • 백엔드의 기존 inspections 테이블은 공정검사(IQC/PQC/FQC) 용도 → 제품검사(품질관리서)와 도메인이 다름
  • documents 시스템(EAV + template)이 80% 완성 → 검사 성적서에 활용 가능

1.2 핵심 설계 원칙

1. 검사 성적서 범용화: 기존 documents 시스템(EAV + template) 활용
   → 양식 변경이 코드 수정 없이 mng에서 가능
2. 컬럼 추가 정책: FK/조인키만 컬럼, 나머지 options JSON
3. 기존 프론트 유지: Mock → 실제 API 전환 (UI 재구축 없음)
4. Phase별 테스트: 각 Phase 완료 후 E2E 검증

1.3 범위

포함 제외 (후속 TODO)
제품검사관리 백엔드 API 품질인정심사 백엔드
실적신고관리 백엔드 API 품질인정심사 프론트 API 연동
프론트 Mock → API 전환 엑셀 다운로드 고도화
검사 성적서 범용화 (documents 연동) 결재 워크플로우 고도화
DB 마이그레이션 알림/푸시 기능

1.4 변경 승인 정책

분류 예시 승인
즉시 가능 options JSON 필드 추가, API 파라미터 추가, 문서 수정 불필요
컨펌 필요 테이블 스키마 변경, 비즈니스 로직 변경, 새 API 추가 필수
금지 기존 테이블 구조 파괴, 프론트 UI 대규모 변경 별도 협의

2. 아키텍처 설계

2.1 데이터 모델 (ER 관계)

quality_documents (품질관리서) ← 신규
├── tenant_id, quality_doc_number (채번)
├── site_name, status (received/in_progress/completed)
├── client_id (FK → clients)
├── inspector_id (FK → users)
├── received_date (접수일)
├── options JSON { construction_site, material_distributor, contractor,
│                  supervisor, site_address, inspection, manager }
├── created_by, updated_by, deleted_by
└── timestamps, soft_delete

quality_document_orders (품질관리서-수주 연결) ← 신규
├── quality_document_id (FK → quality_documents)
├── order_id (FK → orders)
└── timestamps

quality_document_locations (개소별 검사 데이터) ← 신규
├── quality_document_id (FK → quality_documents)
├── quality_document_order_id (FK → quality_document_orders)
├── order_item_id (FK → order_items)  ← 개소 단위
├── post_width, post_height (시공후 규격, nullable)
├── change_reason (변경사유, nullable)
├── document_id (FK → documents, nullable) ← 검사 성적서 연결
├── inspection_status (pending/completed)
└── timestamps

performance_reports (실적신고) ← 신규
├── tenant_id
├── quality_document_id (FK → quality_documents)
├── year, quarter (1-4)
├── confirmation_status (unconfirmed/confirmed/reported)
├── confirmed_date, confirmed_by
├── memo
├── created_by, updated_by, deleted_by
└── timestamps, soft_delete

ER 관계도:

clients ──1:N──> quality_documents <──1:N── users (inspector)
                      │
                      ├──1:N──> quality_document_orders ──N:1──> orders
                      │              │
                      │              └──1:N──> quality_document_locations ──N:1──> order_items
                      │                            │
                      │                            └──1:1──> documents (검사 성적서, EAV)
                      │
                      └──1:1──> performance_reports

2.2 기존 수주 데이터 구조 (참조)

스토리보드의 "개소"는 order_items + order_nodes에 매핑됨:

orders (수주 마스터)
├── id, order_no, client_id, status
├── delivery_date, site_name
└── hasMany order_items, order_nodes

order_items (수주 항목 = 개소 단위)
├── id, order_id, order_node_id
├── floor_code   ← 스토리보드의 "층수" (예: "1F")
├── symbol_code  ← 스토리보드의 "부호" (예: "A-01")
├── item_id, item_code, item_name, specification
├── quantity, unit_price, total_amount
└── attributes (JSON)

order_nodes (수주 노드 = N-depth 트리)
├── id, order_id, parent_id, node_type, code, name
├── options JSON  ← width, height 등 규격 정보
└── hasMany children (자기참조), items (order_items)

스토리보드 용어 → DB 매핑:

스토리보드 DB 비고
수주번호 orders.id 또는 order_no 필드
현장명 orders 관련 필드 client/site
납품일 orders.delivery_date
층수 order_items.floor_code "1F", "2F"
부호 order_items.symbol_code "A-01"
발주 규격 가로 order_nodes.options.width
발주 규격 세로 order_nodes.options.height
시공후 규격 quality_document_locations.post_width/height 신규

2.3 검사 성적서 범용화 (documents 시스템 활용)

기존 SAM documents 시스템(EAV + template)을 활용하여 검사항목 변경에 코드 수정이 불필요하도록 설계.

기존 documents 시스템 구조:

document_templates (양식 마스터 - mng에서 관리)
├── approval_lines     → 결재라인 (작성/승인)
├── basic_fields       → 기본필드 (제품명, LOT NO 등)
├── sections           → 섹션 (검사항목 그룹)
│   └── section_items  → 섹션 항목 (개별 검사항목)
└── columns            → 테이블 컬럼 정의

documents (문서 인스턴스)
├── document_approvals → 결재 이력
├── document_data      → EAV 패턴 (field_key + field_value)
├── document_links     → 다형성 연결 (Order, OrderItem 등)
└── document_attachments → 첨부파일 (사진 등)

활용 흐름:

  1. mng에서 DocumentTemplate 등록 (제품검사 성적서 양식)

    • sections: 갈모양(5개), 모터, 재질, 치수(4개), 작동테스트, 내화/차연/개폐시험
    • columns: NO, 검사항목, 세부, 검사기준, 검사결과, 검사주기, 특징값, 판정
    • basic_fields: 제품명, 제품코드, 수주처, 현장명, LOT NO, 로트크기, 검사일자, 검사자
  2. 개소별 Document 생성 (검사 진행 시)

    • quality_document_locations.document_id로 연결
    • document_links로 OrderItem/QualityDocument 다형성 연결
    • document_data(EAV)에 검사결과/실측값/판정 저장
  3. 프론트에서 렌더링

    • 기존 FqcDocumentContent 컴포넌트 재활용 (양식 기반 동적 렌더링)
    • 하드코딩 InspectionReportDocument는 fallback으로 유지

기존 문서 시스템 API (이미 구현됨):

API 용도
GET /v1/document-templates/{id} 양식 구조 조회
POST /v1/documents/upsert 문서 데이터 저장 (EAV)
GET /v1/documents/{id} 문서 상세 조회
POST /v1/documents/bulk-create-fqc 개소별 문서 일괄생성
GET /v1/documents/fqc-status 진행현황 조회

2.4 API 엔드포인트 설계

제품검사관리

Method Endpoint 설명
GET /v1/quality/documents 품질관리서 목록
GET /v1/quality/documents/stats 상태별 카운트
GET /v1/quality/documents/calendar 캘린더 스케줄
POST /v1/quality/documents 품질관리서 등록
GET /v1/quality/documents/{id} 품질관리서 상세
PUT /v1/quality/documents/{id} 품질관리서 수정
DELETE /v1/quality/documents/{id} 품질관리서 삭제
PATCH /v1/quality/documents/{id}/complete 검사 완료 처리
GET /v1/quality/documents/available-orders 검사 미등록 수주 목록
POST /v1/quality/documents/{id}/orders 수주 연결 추가
DELETE /v1/quality/documents/{id}/orders/{orderId} 수주 연결 삭제
POST /v1/quality/documents/{id}/locations/{locId}/inspect 개소 검사 저장
GET /v1/quality/documents/{id}/request-document 검사제품요청서 데이터
GET /v1/quality/documents/{id}/result-document 제품검사성적서 데이터

실적신고관리

Method Endpoint 설명
GET /v1/quality/performance-reports 실적신고 목록
GET /v1/quality/performance-reports/stats 상태별 카운트
PATCH /v1/quality/performance-reports/confirm 일괄 확정
PATCH /v1/quality/performance-reports/unconfirm 일괄 확정 해제
PATCH /v1/quality/performance-reports/memo 일괄 메모
GET /v1/quality/performance-reports/missing 누락체크 목록
GET /v1/quality/performance-reports/export 엑셀 다운로드

3. Phase 구성

Phase 1: DB + 모델 + 기본 CRUD (백엔드)

# 작업 항목 상태 비고
1.1 마이그레이션 3개 생성 (quality_documents, quality_document_orders, quality_document_locations)
1.2 마이그레이션 1개 생성 (performance_reports)
1.3 모델 4개 생성 (QualityDocument, QualityDocumentOrder, QualityDocumentLocation, PerformanceReport)
1.4 QualityDocumentService 기본 CRUD
1.5 QualityDocumentController + FormRequest
1.6 라우트 등록 (routes/api/v1/quality.php)
1.7 Swagger 문서 작성 Phase 2 완료 후 일괄 작성

검증: Swagger UI에서 기본 CRUD API 테스트

Phase 2: 제품검사 비즈니스 로직 (백엔드)

# 작업 항목 상태 비고
2.1 수주 연결/해제 API (orders 추가/삭제) Phase 1에서 구현 (attachOrders/detachOrder)
2.2 개소별 검사 저장 API (locations/inspect) 시공후 규격 + 상태 변경
2.3 검사 완료 처리 API (complete) Phase 1에서 구현 (실적신고 자동 생성 포함)
2.4 통계 API (stats) + 캘린더 API (calendar) Phase 1에서 구현
2.5 검사제품요청서 데이터 API (request-document)
2.6 제품검사성적서 데이터 API (result-document) documents 시스템 EAV 연동
2.7 품질관리서 번호 자동 채번 Phase 1에서 구현 (KD-QD-YYYYMM-NNNN)

검증: 프론트엔드 Mock 데이터와 동일한 형태로 응답 확인

Phase 3: 실적신고 비즈니스 로직 (백엔드)

# 작업 항목 상태 비고
3.1 PerformanceReportService CRUD Phase 1에서 구현
3.2 일괄 확정/해제 API 필수정보 검증 포함
3.3 일괄 메모 API
3.4 누락체크 API (missing) 출고완료 but 제품검사 미등록
3.5 통계 API (stats) 확정/미확정 카운트 + 총 개소수
3.6 제품검사 완료 시 실적신고 자동 생성 complete() 내 자동 생성

검증: 분기별 필터링, 확정/해제 플로우 테스트

Phase 4: 프론트엔드 API 연동

# 작업 항목 상태 비고
4.1 InspectionManagement/actions.ts API 전환 USE_MOCK_FALLBACK=false, /quality/documents 경로
4.2 PerformanceReportManagement/actions.ts API 전환 USE_MOCK_FALLBACK=false, /quality/performance-reports 경로
4.3 엔드포인트 경로 매핑 조정 /inspections→/quality/documents, /missed→/missing
4.4 타입 정의 조정 (API 응답 구조 매칭) InspectionFormData에 clientId/inspectorId/receptionDate 추가, transformFormToApi options 구조 변환
4.5 E2E 통합 테스트 등록→조회→수정→검사→완료 플로우

검증: 프론트엔드에서 실제 데이터 CRUD 전체 플로우 동작 확인


4. 생성/수정할 파일 목록

4.1 백엔드 (api/) - 신규 생성

api/
├── database/migrations/
│   ├── YYYY_MM_DD_000001_create_quality_documents_table.php
│   ├── YYYY_MM_DD_000002_create_quality_document_orders_table.php
│   ├── YYYY_MM_DD_000003_create_quality_document_locations_table.php
│   └── YYYY_MM_DD_000004_create_performance_reports_table.php
├── app/Models/Qualitys/
│   ├── QualityDocument.php          ← 신규
│   ├── QualityDocumentOrder.php     ← 신규
│   ├── QualityDocumentLocation.php  ← 신규
│   └── PerformanceReport.php        ← 신규
├── app/Services/
│   ├── QualityDocumentService.php   ← 신규
│   └── PerformanceReportService.php ← 신규
├── app/Http/Controllers/Api/V1/
│   ├── QualityDocumentController.php   ← 신규
│   └── PerformanceReportController.php ← 신규
├── app/Http/Requests/Quality/
│   ├── QualityDocumentStoreRequest.php    ← 신규
│   ├── QualityDocumentUpdateRequest.php   ← 신규
│   ├── QualityDocumentCompleteRequest.php ← 신규
│   ├── PerformanceReportConfirmRequest.php ← 신규
│   └── PerformanceReportMemoRequest.php   ← 신규
├── app/Swagger/v1/
│   ├── QualityDocumentApi.php       ← 신규
│   └── PerformanceReportApi.php     ← 신규
└── routes/api/v1/
    └── quality.php                  ← 신규

4.2 백엔드 (api/) - 수정

api/routes/api.php  ← quality.php include 추가

4.3 프론트엔드 (react/) - 수정

react/src/components/quality/
├── InspectionManagement/
│   ├── actions.ts   ← API 경로 변경 + Mock fallback 해제
│   └── types.ts     ← API 응답 타입 조정 (필요시)
└── PerformanceReportManagement/
    ├── actions.ts   ← API 경로 변경 + Mock fallback 해제
    └── types.ts     ← API 응답 타입 조정 (필요시)

5. 백엔드 코드 패턴 (참고용)

5.1 모델 패턴

기존 api/app/Models/Qualitys/Inspection.php 패턴을 따름:

<?php
namespace App\Models\Qualitys;

use App\Traits\Auditable;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class QualityDocument extends Model
{
    use Auditable, BelongsToTenant, SoftDeletes;

    protected $table = 'quality_documents';

    // 상태 상수
    const STATUS_RECEIVED = 'received';
    const STATUS_IN_PROGRESS = 'in_progress';
    const STATUS_COMPLETED = 'completed';

    protected $fillable = [
        'tenant_id', 'quality_doc_number', 'site_name', 'status',
        'client_id', 'inspector_id', 'received_date', 'options',
        'created_by', 'updated_by', 'deleted_by',
    ];

    protected $casts = [
        'options' => 'array',
        'received_date' => 'date',
    ];

    // 관계
    public function client() { return $this->belongsTo(\App\Models\Tenants\Client::class, 'client_id'); }  // 경로 확인 필요
    public function inspector() { return $this->belongsTo(\App\Models\User::class, 'inspector_id'); }
    public function creator() { return $this->belongsTo(\App\Models\User::class, 'created_by'); }
    public function documentOrders() { return $this->hasMany(QualityDocumentOrder::class); }
    public function locations() { return $this->hasMany(QualityDocumentLocation::class); }
    public function performanceReport() { return $this->hasOne(PerformanceReport::class); }

    // 채번: KD-QD-YYYYMM-NNNN
    public static function generateDocNumber(int $tenantId): string { /* ... */ }

    // options accessor helpers
    public function getConstructionSiteAttribute() { return $this->options['construction_site'] ?? null; }
    public function getInspectionInfoAttribute() { return $this->options['inspection'] ?? null; }
    // ... 나머지 options 접근자
}

5.2 서비스 패턴

기존 api/app/Services/InspectionService.php 패턴을 따름:

<?php
namespace App\Services;

use App\Models\Qualitys\QualityDocument;
use Illuminate\Support\Facades\DB;

class QualityDocumentService extends Service
{
    public function index(array $params)
    {
        $query = QualityDocument::with(['client', 'inspector', 'creator', 'documentOrders', 'locations'])
            ->when($params['status'] ?? null, fn($q, $v) => $q->where('status', $v))
            ->when($params['search'] ?? null, fn($q, $v) => $q->where(function($q) use ($v) {
                $q->where('quality_doc_number', 'like', "%{$v}%")
                  ->orWhere('site_name', 'like', "%{$v}%");
            }))
            ->when($params['date_from'] ?? null, fn($q, $v) => $q->where('received_date', '>=', $v))
            ->when($params['date_to'] ?? null, fn($q, $v) => $q->where('received_date', '<=', $v))
            ->orderByDesc('id');

        return $query->paginate($params['per_page'] ?? 20);
    }

    public function store(array $data)
    {
        return DB::transaction(function () use ($data) {
            $data['quality_doc_number'] = QualityDocument::generateDocNumber($this->tenantId());
            $data['status'] = QualityDocument::STATUS_RECEIVED;
            $data['created_by'] = $this->apiUserId();
            $doc = QualityDocument::create($data);
            // 감사 로그
            return $doc->load(['client', 'inspector']);
        });
    }

    // ... show, update, destroy, complete, stats, calendar 등
}

5.3 컨트롤러 패턴

<?php
namespace App\Http\Controllers\Api\V1;

use App\Helpers\ApiResponse;
use App\Services\QualityDocumentService;

class QualityDocumentController extends Controller
{
    public function __construct(private QualityDocumentService $service) {}

    public function index(Request $request)
    {
        return ApiResponse::handle(
            fn() => $this->service->index($request->validated()),
            __('message.fetched')
        );
    }

    public function store(QualityDocumentStoreRequest $request)
    {
        return ApiResponse::handle(
            fn() => $this->service->store($request->validated()),
            __('message.created')
        );
    }
    // ... 나머지 메서드
}

5.4 라우트 패턴

// routes/api/v1/quality.php
Route::prefix('quality')->group(function () {
    // 제품검사 (품질관리서)
    Route::prefix('documents')->group(function () {
        Route::get('', [QualityDocumentController::class, 'index']);
        Route::get('/stats', [QualityDocumentController::class, 'stats']);
        Route::get('/calendar', [QualityDocumentController::class, 'calendar']);
        Route::get('/available-orders', [QualityDocumentController::class, 'availableOrders']);
        Route::post('', [QualityDocumentController::class, 'store']);
        Route::get('/{id}', [QualityDocumentController::class, 'show']);
        Route::put('/{id}', [QualityDocumentController::class, 'update']);
        Route::delete('/{id}', [QualityDocumentController::class, 'destroy']);
        Route::patch('/{id}/complete', [QualityDocumentController::class, 'complete']);
        Route::post('/{id}/orders', [QualityDocumentController::class, 'attachOrders']);
        Route::delete('/{id}/orders/{orderId}', [QualityDocumentController::class, 'detachOrder']);
        Route::post('/{id}/locations/{locId}/inspect', [QualityDocumentController::class, 'inspectLocation']);
        Route::get('/{id}/request-document', [QualityDocumentController::class, 'requestDocument']);
        Route::get('/{id}/result-document', [QualityDocumentController::class, 'resultDocument']);
    });

    // 실적신고
    Route::prefix('performance-reports')->group(function () {
        Route::get('', [PerformanceReportController::class, 'index']);
        Route::get('/stats', [PerformanceReportController::class, 'stats']);
        Route::get('/missing', [PerformanceReportController::class, 'missing']);
        Route::get('/export', [PerformanceReportController::class, 'export']);
        Route::patch('/confirm', [PerformanceReportController::class, 'confirm']);
        Route::patch('/unconfirm', [PerformanceReportController::class, 'unconfirm']);
        Route::patch('/memo', [PerformanceReportController::class, 'updateMemo']);
    });
});

6. API 응답 포맷 (프론트 타입 매칭)

프론트엔드 actions.ts에 이미 API 응답 타입이 정의되어 있음. 백엔드는 이 포맷에 맞춰 응답해야 함.

6.1 제품검사 목록 응답 (GET /v1/quality/documents)

프론트의 ProductInspectionApi 타입 (snake_case):

// react/src/components/quality/InspectionManagement/actions.ts:40-101
interface ProductInspectionApi {
  id: number;
  quality_doc_number: string;
  site_name: string;
  client: string;                    // client.name 조인
  location_count: number;            // locations count
  required_info: string;             // "완료" 또는 "3건 누락"
  inspection_period: string;         // "2026-01-01~2026-01-15"
  inspector: string;                 // inspector.name 조인
  status: 'reception' | 'in_progress' | 'completed';
  author: string;                    // creator.name 조인
  reception_date: string;            // received_date
  manager: string;                   // options.manager.name
  manager_contact: string;           // options.manager.phone
  construction_site: {
    site_name: string;               // options.construction_site.name
    land_location: string;
    lot_number: string;
  };
  material_distributor: {
    company_name: string;            // options.material_distributor.company
    company_address: string;
    representative_name: string;     // options.material_distributor.ceo
    phone: string;
  };
  constructor_info: {
    company_name: string;            // options.contractor.company
    company_address: string;
    name: string;
    phone: string;
  };
  supervisor: {
    office_name: string;             // options.supervisor.office
    office_address: string;
    name: string;
    phone: string;
  };
  schedule_info: {
    visit_request_date: string;      // options.inspection.request_date
    start_date: string;
    end_date: string;
    inspector: string;
    site_postal_code: string;        // options.site_address.postal_code
    site_address: string;
    site_address_detail: string;
  };
  order_items: Array<{
    id: string;
    order_number: string;
    site_name: string;
    delivery_date: string;
    floor: string;                   // order_items.floor_code
    symbol: string;                  // order_items.symbol_code
    order_width: number;             // order_nodes.options.width
    order_height: number;            // order_nodes.options.height
    construction_width: number;      // locations.post_width
    construction_height: number;     // locations.post_height
    change_reason: string;           // locations.change_reason
  }>;
}

상태 매핑 (백엔드 → 프론트):

백엔드 status 프론트 status 한글 표시
received reception 접수
in_progress in_progress 진행중
completed completed 완료

주의: 백엔드 received ≠ 프론트 reception. Service에서 변환하거나 프론트 actions.ts의 mapApiStatus에서 처리.

6.2 제품검사 통계 응답 (GET /v1/quality/documents/stats)

interface InspectionStatsApi {
  reception_count: number;
  in_progress_count: number;
  completed_count: number;
}

6.3 캘린더 응답 (GET /v1/quality/documents/calendar)

interface CalendarItemApi {
  id: number;
  start_date: string;          // options.inspection.start_date
  end_date: string;            // options.inspection.end_date
  inspector: string;           // inspector.name
  site_name: string;
  status: 'reception' | 'in_progress' | 'completed';
}

6.4 수주 선택 목록 응답 (GET /v1/quality/documents/available-orders)

interface OrderSelectItemApi {
  id: number;
  order_number: string;
  site_name: string;
  delivery_date: string;
  location_count: number;      // order_items 수
}

6.5 실적신고 목록 응답 (GET /v1/quality/performance-reports)

// 프론트 PerformanceReport 타입에 맞춤:
interface PerformanceReportApi {
  id: number;
  quality_doc_number: string;
  created_date: string;
  site_name: string;
  client: string;
  location_count: number;
  required_info: string;
  confirm_status: 'confirmed' | 'unconfirmed';
  confirm_date: string | null;
  memo: string;
  year: number;
  quarter: number;            // 1-4 → 프론트에서 "Q1" 변환
}

6.6 실적신고 통계 응답 (GET /v1/quality/performance-reports/stats)

interface PerformanceReportStatsApi {
  total_count: number;
  confirmed_count: number;
  unconfirmed_count: number;
  total_locations: number;
}

6.7 required_info 계산 로직

실적신고 필수정보 = 4개 섹션(건축공사장, 자재유통업자, 공사시공자, 공사감리자) 완성 여부:

// QualityDocumentService에서 계산
public function calculateRequiredInfo(QualityDocument $doc): string
{
    $options = $doc->options ?? [];
    $missing = 0;

    // 각 섹션의 필수 필드가 모두 채워졌는지 확인
    $sections = [
        'construction_site' => ['name', 'land_location', 'lot_number'],
        'material_distributor' => ['company', 'address', 'ceo', 'phone'],
        'contractor' => ['company', 'address', 'name', 'phone'],
        'supervisor' => ['office', 'address', 'name', 'phone'],
    ];

    foreach ($sections as $section => $fields) {
        $data = $options[$section] ?? [];
        foreach ($fields as $field) {
            if (empty($data[$field])) { $missing++; break; } // 섹션 단위
        }
    }

    return $missing === 0 ? '완료' : "{$missing}건 누락";
}

7. 마이그레이션 상세 코드

quality_documents

Schema::create('quality_documents', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained();
    $table->string('quality_doc_number', 30)->comment('품질관리서 번호');
    $table->string('site_name')->comment('현장명');
    $table->string('status', 20)->default('received')->comment('received/in_progress/completed');
    $table->foreignId('client_id')->nullable()->constrained('clients')->comment('수주처');
    $table->foreignId('inspector_id')->nullable()->constrained('users')->comment('검사자');
    $table->date('received_date')->nullable()->comment('접수일');
    $table->json('options')->nullable()->comment('관련자정보, 검사정보, 현장주소 등');
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->unsignedBigInteger('deleted_by')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->unique(['tenant_id', 'quality_doc_number']);
    $table->index(['tenant_id', 'status']);
    $table->index(['tenant_id', 'client_id']);
    $table->index(['tenant_id', 'inspector_id']);
    $table->index(['tenant_id', 'received_date']);
});

quality_document_orders

Schema::create('quality_document_orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('quality_document_id')->constrained()->cascadeOnDelete();
    $table->foreignId('order_id')->constrained('orders');
    $table->timestamps();

    $table->unique(['quality_document_id', 'order_id']);
});

quality_document_locations

Schema::create('quality_document_locations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('quality_document_id')->constrained()->cascadeOnDelete();
    $table->foreignId('quality_document_order_id')->constrained()->cascadeOnDelete();
    $table->foreignId('order_item_id')->constrained('order_items');
    $table->integer('post_width')->nullable()->comment('시공후 가로');
    $table->integer('post_height')->nullable()->comment('시공후 세로');
    $table->string('change_reason')->nullable()->comment('규격 변경사유');
    $table->foreignId('document_id')->nullable()->comment('검사성적서 문서 ID');
    $table->string('inspection_status', 20)->default('pending')->comment('pending/completed');
    $table->timestamps();

    $table->index(['quality_document_id', 'inspection_status']);
});

performance_reports

Schema::create('performance_reports', function (Blueprint $table) {
    $table->id();
    $table->foreignId('tenant_id')->constrained();
    $table->foreignId('quality_document_id')->constrained();
    $table->unsignedSmallInteger('year')->comment('연도');
    $table->unsignedTinyInteger('quarter')->comment('분기 1-4');
    $table->string('confirmation_status', 20)->default('unconfirmed')->comment('unconfirmed/confirmed/reported');
    $table->date('confirmed_date')->nullable();
    $table->foreignId('confirmed_by')->nullable()->constrained('users');
    $table->text('memo')->nullable();
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->unsignedBigInteger('deleted_by')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->unique(['tenant_id', 'quality_document_id']);
    $table->index(['tenant_id', 'year', 'quarter']);
    $table->index(['tenant_id', 'confirmation_status']);
});

8. options JSON 구조 상세

{
  "manager": {
    "name": "홍길동",
    "phone": "010-1234-5678"
  },
  "inspection": {
    "request_date": "2026-03-01",
    "start_date": "2026-03-10",
    "end_date": "2026-03-15"
  },
  "site_address": {
    "postal_code": "12345",
    "address": "서울시 강남구...",
    "detail": "101동 1층"
  },
  "construction_site": {
    "name": "OO아파트 신축공사",
    "land_location": "서울시 강남구 역삼동",
    "lot_number": "123-45"
  },
  "material_distributor": {
    "company": "OO건자재",
    "address": "서울시...",
    "ceo": "김OO",
    "phone": "02-1234-5678"
  },
  "contractor": {
    "company": "OO건설",
    "address": "서울시...",
    "name": "이OO",
    "phone": "02-9876-5432"
  },
  "supervisor": {
    "office": "OO감리사무소",
    "address": "서울시...",
    "name": "박OO",
    "phone": "02-5555-6666"
  }
}

9. 상태 머신

품질관리서 상태

received (접수) → in_progress (진행중) → completed (완료)
                       ↑                      |
                       └──────────────────────┘ (재검사 시)

트리거:
- received → in_progress: 검사시작일 도달 or 첫 검사 진행
- in_progress → completed: 모든 개소 검사 완료 + 검사완료 버튼
- completed → in_progress: 재검사 필요 시 (수동)

실적신고 상태

unconfirmed (미확정) → confirmed (확정) → reported (신고완료)
                           ↑                  |
                           └──────────────────┘ (해제)

트리거:
- 자동 생성: 품질관리서 완료 시 → unconfirmed
- unconfirmed → confirmed: 필수정보(4개 섹션) 검증 통과 + 확정 버튼
- confirmed → unconfirmed: 확정 해제
- confirmed → reported: 외부 신고 처리 후

실적신고 확정 필수정보 검증

확정하려면 quality_documents.options의 다음 4개 섹션이 모두 채워져야 함:

섹션 필수 필드
construction_site name, land_location, lot_number
material_distributor company, address, ceo, phone
contractor company, address, name, phone
supervisor office, address, name, phone

10. 프론트엔드 API 전환 가이드

10.1 InspectionManagement/actions.ts 수정 포인트

  1. API 경로 변경: /v1/inspections/v1/quality/documents
  2. USE_MOCK_FALLBACK = false 로 변경
  3. 상태 매핑: 백엔드 received → 프론트 reception (기존 mapApiStatus 함수 수정)
  4. 수주 선택: /v1/orders/select/v1/quality/documents/available-orders

기존 transformApiToFrontend() 함수가 snake_case → camelCase 변환을 처리하고 있으므로, 백엔드 응답만 맞추면 프론트 타입 수정은 최소화.

10.2 PerformanceReportManagement/actions.ts 수정 포인트

  1. API 경로 변경: /v1/performance-reports/v1/quality/performance-reports
  2. USE_MOCK_FALLBACK = false 로 변경
  3. 누락체크: /v1/performance-reports/missed/v1/quality/performance-reports/missing

10.3 프론트 타입 파일 (참조 경로)

파일 역할
react/src/components/quality/InspectionManagement/types.ts 제품검사 전체 타입 정의
react/src/components/quality/InspectionManagement/actions.ts API 호출 + 변환 함수
react/src/components/quality/InspectionManagement/mockData.ts Mock 데이터 (API 응답 형태 참고)
react/src/components/quality/InspectionManagement/fqcActions.ts FQC 문서 시스템 API
react/src/components/quality/PerformanceReportManagement/types.ts 실적신고 타입 정의
react/src/components/quality/PerformanceReportManagement/actions.ts 실적신고 API 호출

11. 로컬 테스트 방법

환경

  • Docker 로컬: *.sam.kr (dev.sam.kr, api.sam.kr, mng.sam.kr)
  • 프론트: dev.sam.kr (Next.js)
  • API: api.sam.kr (Laravel)
  • Swagger: api.sam.kr/api-docs/index.html

마이그레이션 실행

cd api
php artisan migrate:status    # 현재 상태 확인
php artisan migrate           # 마이그레이션 실행

API 테스트

  1. Swagger UI (api.sam.kr/api-docs)에서 엔드포인트 테스트
  2. 또는 curl/Postman으로 직접 호출

프론트 테스트

  1. dev.sam.kr/quality/inspections - 제품검사 목록
  2. dev.sam.kr/quality/inspections?mode=new - 제품검사 등록
  3. dev.sam.kr/quality/inspections/{id}?mode=view - 제품검사 상세
  4. dev.sam.kr/quality/performance-reports - 실적신고

12. 컨펌 대기 목록

# 항목 변경 내용 영향 범위 상태
- - - - -

13. 변경 이력

날짜 항목 변경 내용 파일 승인
2026-03-05 - 계획 문서 초안 작성 - -
2026-03-05 Phase 1 DB 마이그레이션 4개, 모델 4개, Service 2개, Controller 2개, FormRequest 4개, Route 17개 생성 api/

14. 후속 TODO (품질인정심사)

Phase 1~4 완료 후 별도 계획 문서로 진행:

항목 설명 프론트 경로 우선순위
품질인정심사 DB 설계 기준/매뉴얼 점검표 + 로트추적 테이블 react/.../quality/qms/
품질인정심사 백엔드 API 점검표 CRUD, 로트추적 드릴다운 -
qms/ 프론트 API 연동 Mock → 실제 API 전환 react/.../quality/qms/page.tsx
엑셀 다운로드 실적신고 확정건 엑셀 export -
결재 워크플로우 고도화 검사제품요청서/성적서 결재 -

15. 참고 문서

문서 경로 용도
스토리보드 분석 docs/dev/dev_plans/quality/quality-management-storyboard-analysis.md 화면 명세 (전체 참조)
스토리보드 원본 docs/dev/dev_plans/quality/SAM_MES_경동기업_품질관리_Storyboard_D1.9_260224/ 슬라이드 이미지
DB 스키마 (영업) docs/system/database/sales.md orders/order_items/order_nodes 구조
DB 스키마 (생산/품질) docs/system/database/production.md inspections, lots (기존)
API 규칙 docs/dev/standards/api-rules.md Service-First, FormRequest, ApiResponse
options 정책 docs/dev/standards/options-column-policy.md JSON 컬럼 정책
품질 체크리스트 docs/dev/standards/quality-checklist.md 코드 품질 기준

핵심 소스 코드

파일 역할 참고 이유
api/app/Models/Qualitys/Inspection.php 기존 공정검사 모델 모델 패턴, 채번 로직 참고
api/app/Services/InspectionService.php 기존 검사 서비스 Service 패턴 참고 (index/show/store/complete)
api/app/Http/Controllers/Api/V1/InspectionController.php 기존 검사 컨트롤러 Controller 패턴 참고
api/routes/api/v1/production.php 기존 라우트 라우트 등록 패턴 참고
api/app/Models/Orders/Order.php 수주 마스터 관계, 상태 상수
api/app/Models/Orders/OrderItem.php 수주 항목 (개소) floor_code, symbol_code
api/app/Models/Orders/OrderNode.php 수주 노드 (트리) options.width/height
react/src/components/quality/InspectionManagement/actions.ts 프론트 API 호출 API 응답 포맷 정의 (라인 40~101)
react/src/components/quality/InspectionManagement/types.ts 프론트 타입 전체 데이터 구조
react/src/components/quality/InspectionManagement/fqcActions.ts FQC 문서 API documents 시스템 연동 패턴
react/src/components/quality/PerformanceReportManagement/actions.ts 실적신고 API 응답 포맷, Mock 패턴
react/src/components/quality/PerformanceReportManagement/types.ts 실적신고 타입 데이터 구조

16. 검증 결과

각 Phase 완료 후 기록

Phase 1 검증

테스트 예상 결과 실제 결과 상태
마이그레이션 실행 테이블 4개 생성 4개 테이블 생성 완료
기본 CRUD API (Swagger) 200 OK + JSON 17개 라우트 등록 확인
멀티테넌시 격리 tenant_id 필터링 BelongsToTenant 적용
품질관리서 등록 채번 + options 저장 코드 구현 완료

Phase 2 검증

테스트 예상 결과 실제 결과 상태
수주 연결/해제 quality_document_orders 레코드
수주 연결 시 개소 자동 생성 quality_document_locations 레코드
개소 검사 저장 documents 테이블에 EAV 데이터
검사 완료 (미완료 개소 존재) 400 에러
검사 완료 (모두 완료) status → completed
품질관리서 번호 채번 KD-QD-202603-0001 형식
캘린더 API 월별 검사 스케줄

Phase 3 검증

테스트 예상 결과 실제 결과 상태
검사완료 → 실적신고 자동 생성 performance_reports 레코드
확정 (필수정보 누락) 400 에러 + 누락 목록
확정 (필수정보 완료) confirmed + 날짜
확정 해제 unconfirmed
일괄 메모 여러 건 메모 업데이트
누락체크 출고완료+미등록 건

Phase 4 검증

테스트 예상 결과 실제 결과 상태
제품검사 등록 (프론트) 폼 입력 → API 저장 → 목록 반영
수주 선택 팝업 available-orders API 연동
캘린더 표시 검사기간 바 표시
개소 검사 입력 FQC 문서 시스템 연동
검사 완료 처리 상태 변경 + 실적신고 자동 생성
실적신고 확정/해제 상태 변경 + 필수정보 검증
누락체크 탭 누락 건 표시

이 문서는 /plan 스킬로 생성되었습니다. (2026-03-05)