Files
sam-docs/frontend/api-specs/nonconforming-api.md
김보곤 013a7a38eb docs: [material] 부적합관리 FE 명세 결재 연동 추가
- 3.8 결재상신 API 섹션 추가 (요청/응답 예시)
- 단건 조회 응답에 approval 필드 추가
- 상태별 UI 제어 테이블 업데이트
- Server Actions에 submitNonconformingApproval 추가
2026-03-19 09:03:19 +09:00

16 KiB
Raw Blame History

부적합관리 API 명세

작성일: 2026-03-19 상태: Phase 1+2 API 완료 (결재 연동 포함) 대상: React 프론트엔드 개발자 라우트: /material/nonconforming-management


1. 개요

1.1 목적

제조/자재 입고 과정에서 발생하는 품질 부적합(불량)을 등록하고, 원인 분석 → 시정 조치 → 비용 산정까지 관리하는 기능이다.

1.2 화면 구성

수주서(OrderRegistration)와 동일한 IntegratedDetailTemplate + FormSection 패턴을 따른다.

  • 목록: /material/nonconforming-management
  • 등록: /material/nonconforming-management?mode=new
  • 상세: /material/nonconforming-management/{id}
  • 수정: /material/nonconforming-management/{id}?mode=edit

1.3 핵심 3가지

  1. 불량 내역 — 무엇이, 언제, 어디서 발생했는지
  2. 처리 방법 — 원인 분석 + 시정/개선 조치
  3. 자재 내역과 비용 — 자재 상세 테이블 + 비용 요약 (4항목 + 합계)

2. 코드 상수

2.1 부적합 유형 (nc_type)

라벨
material 자재불량
process 공정불량
construction 시공불량
other 기타

2.2 상태 (status)

라벨 Badge 색상 (권장)
RECEIVED 접수 default (회색)
ANALYZING 분석중 warning (노랑)
RESOLVED 조치완료 info (파랑)
CLOSED 종결 success (녹색)

2.3 상태 전이 규칙

RECEIVED → ANALYZING     (조건 없음)
ANALYZING → RESOLVED     (cause_analysis + corrective_action 필수)
RESOLVED → (결재상신) → 결재 승인 시 자동 CLOSED
                         결재 반려/회수 시 → RESOLVED 유지 (재상신 가능)

CLOSED 상태에서는 수정/삭제 불가 (API가 403 반환)


3. API 엔드포인트

기본 URL: /api/v1/material/nonconforming-reports 인증: Bearer Token + X-API-KEY + X-TENANT-ID


3.1 목록 조회

GET /api/v1/material/nonconforming-reports
파라미터 타입 필수 설명
page int - 페이지 (기본: 1)
per_page int - 페이지 크기 (기본: 20)
search string - 검색어 (부적합번호, 현장명, 품목명)
status string - 상태 필터 (RECEIVED, ANALYZING, RESOLVED, CLOSED)
nc_type string - 유형 필터 (material, process, construction, other)
from_date date - 발생일 시작 (YYYY-MM-DD)
to_date date - 발생일 종료 (YYYY-MM-DD)

응답:

{
  "success": true,
  "message": "데이터를 조회했습니다.",
  "data": {
    "data": [
      {
        "id": 1,
        "nc_number": "NC-20260319-001",
        "status": "RECEIVED",
        "nc_type": "material",
        "occurred_at": "2026-03-19",
        "confirmed_at": "2026-03-19",
        "site_name": "강남 현장",
        "total_cost": 180000,
        "creator": { "id": 5, "name": "김보곤" },
        "item": { "id": 45, "name": "알루미늄 프레임 50T" },
        "order": { "id": 152, "order_number": "ORD-20260315-001" }
      }
    ],
    "current_page": 1,
    "last_page": 1,
    "per_page": 20,
    "total": 1
  }
}

3.2 상태별 통계

GET /api/v1/material/nonconforming-reports/stats

응답:

{
  "success": true,
  "data": {
    "by_status": {
      "RECEIVED": 3,
      "ANALYZING": 2,
      "RESOLVED": 5,
      "CLOSED": 10
    },
    "total_count": 20,
    "total_cost": 2500000
  }
}

3.3 단건 조회

GET /api/v1/material/nonconforming-reports/{id}

응답:

{
  "success": true,
  "data": {
    "id": 1,
    "nc_number": "NC-20260319-001",
    "status": "RECEIVED",
    "nc_type": "material",
    "occurred_at": "2026-03-19",
    "confirmed_at": "2026-03-19",
    "site_name": "강남 현장",
    "department": { "id": 3, "name": "제조파트" },
    "order": { "id": 152, "order_number": "ORD-20260315-001", "site_name": "강남 현장" },
    "item": { "id": 45, "name": "알루미늄 프레임 50T" },
    "defect_quantity": 10.00,
    "unit": "EA",
    "defect_description": "V-CUT 불량 - 절단면 틀어짐",
    "cause_analysis": "절단기 날 마모로 인한 정밀도 저하",
    "corrective_action": "절단기 날 교체 및 정밀도 재검증",
    "action_completed_at": null,
    "action_manager": { "id": 8, "name": "이철수" },
    "related_employee": { "id": 12, "name": "박영희" },
    "material_cost": 150000,
    "shipping_cost": 30000,
    "construction_cost": 0,
    "other_cost": 0,
    "total_cost": 180000,
    "remarks": "",
    "drawing_location": "nas2dual/도면/2026/03",
    "items": [
      {
        "id": 1,
        "item_id": 45,
        "item_name": "알루미늄 프레임 50T",
        "specification": "50 x 3000mm",
        "quantity": 10.00,
        "unit_price": 15000,
        "amount": 150000,
        "sort_order": 0,
        "remarks": "전량 폐기"
      }
    ],
    "files": [],
    "approval": {
      "id": 10,
      "document_number": "AP-20260319-0001",
      "status": "pending",
      "completed_at": null,
      "steps": [
        {
          "id": 1,
          "step_order": 1,
          "step_type": "approval",
          "approver_id": 3,
          "status": "pending",
          "comment": null,
          "acted_at": null,
          "approver": { "id": 3, "name": "홍길동" }
        }
      ]
    },
    "creator": { "id": 5, "name": "김보곤" },
    "created_at": "2026-03-19",
    "updated_at": "2026-03-19"
  }
}

approval은 결재상신 전에는 null. 결재 진행 중이면 status: "pending", 승인 완료 시 "approved".


---

### 3.4 등록

POST /api/v1/material/nonconforming-reports


**요청 Body**:

```json
{
  "nc_type": "material",
  "occurred_at": "2026-03-19",
  "confirmed_at": "2026-03-19",
  "site_name": "강남 현장",
  "department_id": 3,
  "order_id": 152,
  "item_id": 45,
  "defect_quantity": 10,
  "unit": "EA",
  "defect_description": "V-CUT 불량 - 절단면 틀어짐",
  "cause_analysis": "",
  "corrective_action": "",
  "action_manager_id": 8,
  "related_employee_id": 12,
  "shipping_cost": 30000,
  "construction_cost": 0,
  "other_cost": 0,
  "drawing_location": "nas2dual/도면/2026/03",
  "remarks": "",
  "items": [
    {
      "item_id": 45,
      "item_name": "알루미늄 프레임 50T",
      "specification": "50 x 3000mm",
      "quantity": 10,
      "unit_price": 15000,
      "remarks": "전량 폐기"
    }
  ]
}
필드 타입 필수 설명
nc_type string 부적합유형 (2.1 참고)
occurred_at date 발생일
confirmed_at date - 불량확인일
site_name string - 현장명
department_id int - 부서 ID
order_id int - 관련 수주 ID
item_id int - 관련 품목 ID
defect_quantity number - 불량 수량
unit string - 단위 (EA, M, KG 등)
defect_description string - 불량 상세 설명
cause_analysis string - 원인 분석
corrective_action string - 처리 방안
action_manager_id int - 조치 담당자 ID
related_employee_id int - 관련 직원 ID
shipping_cost int - 운송 비용
construction_cost int - 시공 비용
other_cost int - 기타 비용
drawing_location string - 도면 저장 위치
remarks string - 비고
items array - 자재 상세 내역 (아래 참고)

items 배열 항목:

필드 타입 필수 설명
item_id int - 품목 마스터 ID (선택적)
item_name string 품목명
specification string - 규격/사양
quantity number - 수량
unit_price int - 단가
remarks string - 비고

material_cost는 전송하지 않아도 됨 — items의 quantity × unit_price 합계로 자동 계산 total_cost도 자동 계산: material_cost + shipping_cost + construction_cost + other_cost

응답: 단건 조회와 동일한 구조


3.5 수정

PUT /api/v1/material/nonconforming-reports/{id}

등록과 동일한 Body. 부분 수정 지원 (변경된 필드만 전송 가능).

items 배열이 포함되면 기존 items를 전체 교체 (삭제 후 재생성) items를 빈 배열 []로 보내면 기존 자재 내역 전부 삭제


3.6 삭제

DELETE /api/v1/material/nonconforming-reports/{id}

CLOSED 상태에서는 삭제 불가 (403)


3.7 상태 변경

PATCH /api/v1/material/nonconforming-reports/{id}/status

요청 Body:

{
  "status": "ANALYZING"
}

전이 규칙 위반 시 422 에러 반환 (섹션 2.3 참고)


3.8 결재상신

POST /api/v1/material/nonconforming-reports/{id}/submit-approval

RESOLVED 상태에서만 가능. 이미 결재가 연결된 경우 422 에러.

요청 Body:

{
  "title": "부적합 처리 결재 - NC-20260319-001",
  "form_id": 5,
  "steps": [
    { "approver_id": 3, "step_type": "approval" },
    { "approver_id": 7, "step_type": "approval" }
  ]
}
필드 타입 필수 설명
title string - 결재 제목 (미입력 시 자동 생성)
form_id int - 결재 양식 ID
steps array 결재선 (최소 1명)
steps[].approver_id int 결재자 사용자 ID
steps[].step_type string - approval(결재), agreement(합의), reference(참조) — 기본: approval

결재 결과에 따른 부적합 상태 변화:

결재 결과 부적합 상태 설명
승인 (approved) CLOSED 종결 (수정 불가)
반려 (rejected) RESOLVED 유지 approval_id 해제, 재상신 가능
회수 (cancelled) RESOLVED 유지 approval_id 해제, 재상신 가능

응답: 단건 조회와 동일 (approval 정보 포함)


4. 화면별 구현 가이드

4.1 목록 페이지

파일: react/src/components/material/nonconforming/NonconformingList.tsx
영역 내용
상단 통계 카드 stats API로 상태별 건수 표시 (4개 카드)
필터 기간(DateRangePicker), 상태(Select), 유형(Select), 검색(Input)
테이블 컬럼 No, 부적합번호, 유형, 현장명, 품목명, 발생일, 비용합계, 상태(Badge), 등록자

4.2 등록/수정 폼 — 5개 FormSection

파일: react/src/components/material/nonconforming/NonconformingForm.tsx
섹션 필드
1. 기본 정보 부적합번호(자동/읽기전용), 상태(Badge), 발생일, 확인일, 부적합유형(Select), 현장명, 부서, 관련 수주(검색), 관련 직원
2. 불량 내역 관련 품목(검색), 불량 수량, 단위, 불량 상세 설명(Textarea), 첨부파일
3. 원인 분석 및 처리 방안 원인 분석(Textarea), 처리 방안(Textarea), 조치 완료일, 조치 담당자
4. 자재 내역 및 비용 자재 상세 테이블(행 추가/삭제) + 비용 요약(4항목+합계)
5. 비고 비고(Textarea), 도면 저장 위치(Input)

4.3 자재 상세 테이블

파일: react/src/components/material/nonconforming/NonconformingItemTable.tsx
컬럼 타입 설명
No 자동 1부터 순번
품목명 Input (필수) item_name
규격 Input specification
수량 NumberInput quantity
단가 NumberInput unit_price
금액 읽기전용 quantity × unit_price (자동 계산)
비고 Input remarks
작업 Button 삭제 버튼
  • 하단에 "행 추가" 버튼
  • 금액 합계 = material_cost (비용 요약에 자동 반영)

4.4 비용 요약

파일: react/src/components/material/nonconforming/CostSummary.tsx
항목 입력 방식 설명
자재 비용 읽기 전용 자재 상세 테이블 금액 합계
운송 비용 NumberInput 수동 입력
시공 비용 NumberInput 수동 입력
기타 비용 NumberInput 수동 입력
비용 합계 읽기 전용 4개 항목 합계

4.5 상태별 UI 제어

상태 수정 가능 액션 버튼
RECEIVED "분석 시작" → ANALYZING
ANALYZING "조치 완료" → RESOLVED (원인분석+처리방안 필수)
RESOLVED "결재상신" → submit-approval API 호출
RESOLVED + 결재중 결재 진행 상태 표시 (기존 결재 시스템 UI)
CLOSED 읽기전용 없음 (결재 승인으로 자동 전환됨)

5. React 파일 구조

react/src/
├── app/[locale]/(protected)/material/
│   └── nonconforming-management/
│       ├── page.tsx                           ← mode 분기 (목록/등록)
│       └── [id]/page.tsx                      ← 상세/수정
├── components/material/nonconforming/
│   ├── NonconformingList.tsx                  ← 목록 + 통계 카드
│   ├── NonconformingForm.tsx                  ← 등록/수정 (5개 FormSection)
│   ├── NonconformingDetail.tsx                ← 상세 뷰 (읽기전용)
│   ├── NonconformingItemTable.tsx             ← 자재 상세 테이블
│   ├── CostSummary.tsx                        ← 비용 요약 (4항목+합계)
│   └── actions.ts                             ← Server Actions

6. Server Actions 예시

// actions.ts
'use server';

import { buildApiUrl } from '@/lib/api/query-params';
import { executeServerAction, executePaginatedAction } from '@/lib/api/server-action';

// 목록
export async function getNonconformingReports(params: Record<string, unknown>) {
  return executePaginatedAction({
    url: buildApiUrl('/api/v1/material/nonconforming-reports', params),
    method: 'GET',
  });
}

// 통계
export async function getNonconformingStats() {
  return executeServerAction({
    url: buildApiUrl('/api/v1/material/nonconforming-reports/stats'),
    method: 'GET',
  });
}

// 단건
export async function getNonconformingReport(id: number) {
  return executeServerAction({
    url: buildApiUrl(`/api/v1/material/nonconforming-reports/${id}`),
    method: 'GET',
  });
}

// 등록
export async function createNonconformingReport(data: Record<string, unknown>) {
  return executeServerAction({
    url: buildApiUrl('/api/v1/material/nonconforming-reports'),
    method: 'POST',
    body: data,
  });
}

// 수정
export async function updateNonconformingReport(id: number, data: Record<string, unknown>) {
  return executeServerAction({
    url: buildApiUrl(`/api/v1/material/nonconforming-reports/${id}`),
    method: 'PUT',
    body: data,
  });
}

// 삭제
export async function deleteNonconformingReport(id: number) {
  return executeServerAction({
    url: buildApiUrl(`/api/v1/material/nonconforming-reports/${id}`),
    method: 'DELETE',
  });
}

// 상태 변경
export async function changeNonconformingStatus(id: number, status: string) {
  return executeServerAction({
    url: buildApiUrl(`/api/v1/material/nonconforming-reports/${id}/status`),
    method: 'PATCH',
    body: { status },
  });
}

// 결재상신
export async function submitNonconformingApproval(
  id: number,
  data: { title?: string; form_id?: number; steps: Array<{ approver_id: number; step_type?: string }> }
) {
  return executeServerAction({
    url: buildApiUrl(`/api/v1/material/nonconforming-reports/${id}/submit-approval`),
    method: 'POST',
    body: data,
  });
}

관련 문서

  • dev/dev_plans/nonconforming-management-plan.md — 기획서 (전체 Phase 계획)
  • rules/item-policy.md — 품목 정책
  • features/quotes/README.md — 수주 시스템 (폼 구조 참고)

최종 업데이트: 2026-03-19