- 7개 엔드포인트 상세 (요청/응답 JSON 예시) - 코드 상수 (부적합유형 4종, 상태 4종, 전이 규칙) - 화면별 구현 가이드 (5개 FormSection, 자재 테이블, 비용 요약) - React 파일 구조 + Server Actions 예시 코드
14 KiB
부적합관리 API 명세
작성일: 2026-03-19 상태: Phase 1 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가지
- 불량 내역 — 무엇이, 언제, 어디서 발생했는지
- 처리 방법 — 원인 분석 + 시정/개선 조치
- 자재 내역과 비용 — 자재 상세 테이블 + 비용 요약 (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 (결재 승인 시 — Phase 2)
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": [],
"creator": { "id": 5, "name": "김보곤" },
"created_at": "2026-03-19",
"updated_at": "2026-03-19"
}
}
3.4 등록
POST /api/v1/material/nonconforming-reports
요청 Body:
{
"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 참고)
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 |
✅ | "결재상신" (Phase 2) |
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 },
});
}
관련 문서
dev/dev_plans/nonconforming-management-plan.md— 기획서 (전체 Phase 계획)rules/item-policy.md— 품목 정책features/quotes/README.md— 수주 시스템 (폼 구조 참고)
최종 업데이트: 2026-03-19