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

578 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 부적합관리 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`) |
**응답**:
```json
{
"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
```
**응답**:
```json
{
"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}
```
**응답**:
```json
{
"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**:
```json
{
"status": "ANALYZING"
}
```
> 전이 규칙 위반 시 422 에러 반환 (섹션 2.3 참고)
---
### 3.8 결재상신
```
POST /api/v1/material/nonconforming-reports/{id}/submit-approval
```
> `RESOLVED` 상태에서만 가능. 이미 결재가 연결된 경우 422 에러.
**요청 Body**:
```json
{
"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 예시
```typescript
// 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