- 03-17 문서 섹션4 실제 구현에 맞게 수정 (근본원인, 코드경로 정정) - 변경이력 추가: 20260318_receiving_item_search_fix.md - INDEX.md 등록
395 lines
12 KiB
Markdown
395 lines
12 KiB
Markdown
# 입고관리 / 재고현황 변경사항 (2026-03-17)
|
|
|
|
**날짜:** 2026-03-17
|
|
**작업자:** R&D실 (백엔드)
|
|
**대상:** React 프론트엔드
|
|
**API 상태:** 구현 완료, 개발서버 배포 완료
|
|
|
|
---
|
|
|
|
## 변경 항목
|
|
|
|
| # | 항목 | 대상 화면 | 작업 유형 |
|
|
|---|------|----------|----------|
|
|
| 1 | 원자재로트번호 자동채번 | 입고등록 | UI 변경 |
|
|
| 2 | 재고 조정 위치 이동 | 입고관리 → 재고 상세 | UI 이동 + API 연동 |
|
|
| 3 | 재고 조정 모달 UI 개선 | InventoryAdjustmentDialog | UI 개선 |
|
|
| 4 | 입고 품목 검색 개선 | 입고등록 품목검색 모달 | 파라미터 변경 |
|
|
|
|
---
|
|
|
|
## 1. 원자재로트번호 자동채번
|
|
|
|
### 대상
|
|
|
|
```
|
|
URL: /material/receiving-management/new/mode-new
|
|
/material/receiving-management/[id]?mode=edit
|
|
컴포넌트: ReceivingDetail.tsx
|
|
```
|
|
|
|
### 변경 내용
|
|
|
|
| 항목 | 변경 전 | 변경 후 |
|
|
|------|--------|--------|
|
|
| `lot_no` 입력 | 사용자 직접 입력 | **readOnly** (API 자동 생성) |
|
|
| 신규 등록 시 | 프론트에서 `generateLotNo()` 호출 | `lot_no` 미전송 → API가 자동 채번 |
|
|
| 수정 모드 | 편집 가능 | **readOnly** (기존 값 표시) |
|
|
|
|
### 프론트 작업
|
|
|
|
```
|
|
✅ lot_no input에 readOnly 속성 적용
|
|
✅ 배경색 회색(비활성) 처리
|
|
✅ 신규 등록 시 placeholder: "저장 시 자동 생성"
|
|
✅ 저장 후 API 응답의 lot_no 값으로 화면 갱신
|
|
❌ generateLotNo() 함수 호출 제거
|
|
```
|
|
|
|
### API 동작
|
|
|
|
```
|
|
POST /api/v1/receivings (신규 등록)
|
|
→ lot_no 미전송 시 API가 채번규칙 기반 자동 생성
|
|
→ 채번 형식: YYMMDD-NN (예: 260317-01, 260317-02)
|
|
→ 응답: { "data": { "lot_no": "260317-01", ... } }
|
|
```
|
|
|
|
> 채번 형식은 MNG 채번규칙 관리에서 테넌트별 변경 가능. 프론트는 형식에 의존하지 않고 API 응답값을 그대로 표시.
|
|
|
|
---
|
|
|
|
## 2. 재고 조정 위치 이동
|
|
|
|
### AS-IS → TO-BE
|
|
|
|
```
|
|
AS-IS:
|
|
입고관리 목록 (ReceivingList.tsx)
|
|
→ 헤더에 [재고 조정] 버튼 → InventoryAdjustmentDialog 팝업
|
|
입고관리 상세 (ReceivingDetail.tsx)
|
|
→ 하단에 재고 조정 이력 섹션
|
|
|
|
TO-BE:
|
|
입고관리 목록 → [재고 조정] 버튼 제거
|
|
입고관리 상세 → 재고 조정 섹션 제거
|
|
재고현황 > 재고 상세 > 기본 정보 아래 > 재고 조정 섹션 (이력 + 추가)
|
|
```
|
|
|
|
### 대상 컴포넌트
|
|
|
|
| 작업 | 파일 | 내용 |
|
|
|------|------|------|
|
|
| 제거 | `ReceivingManagement/ReceivingList.tsx` | 헤더의 "재고 조정" 버튼 + `isAdjustmentOpen` 상태 + `InventoryAdjustmentDialog` 제거 |
|
|
| 제거 | `ReceivingManagement/ReceivingDetail.tsx` | 재고 조정 이력 섹션 렌더링 제거 |
|
|
| 추가 | `StockStatus/StockStatusDetail.tsx` | 기본 정보 카드 아래에 재고 조정 섹션 추가 |
|
|
| 이동 | `ReceivingManagement/InventoryAdjustmentDialog.tsx` | 공통 위치로 추출 권장 |
|
|
|
|
### 재고 조정 섹션 UI
|
|
|
|
```
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ 재고 조정 [+ 추가] │
|
|
├──────┬─────────────────────┬────────────┬─────────┬────────────┤
|
|
│ No │ 조정일시 │ 증감 수량 │ 사유 │ 검수자 │
|
|
├──────┼─────────────────────┼────────────┼─────────┼────────────┤
|
|
│ 1 │ 2026-03-17 14:30 │ +100 │ 실사조정│ 김보곤 │
|
|
│ 2 │ 2026-03-16 10:15 │ -50 │ 파손폐기│ 홍길동 │
|
|
├──────────────────────────────────────────────────────────────────┤
|
|
│ 재고 조정 이력이 없습니다. │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### + 추가 버튼 동작
|
|
|
|
1. 클릭 시 입력 폼/모달 오픈
|
|
2. **증감 수량** (필수, 0 제외) 및 **사유** (선택) 입력
|
|
3. 저장 시 `POST /api/v1/stocks/{stockId}/adjustments` 호출
|
|
4. 성공 시 이력 테이블 새로고침 + 기본 정보의 재고량 갱신
|
|
|
|
---
|
|
|
|
## API 스펙
|
|
|
|
### 2-1. 재고 조정 이력 조회
|
|
|
|
```
|
|
GET /api/v1/stocks/{stockId}/adjustments
|
|
```
|
|
|
|
**응답:**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "데이터를 조회했습니다.",
|
|
"data": [
|
|
{
|
|
"id": 1,
|
|
"adjusted_at": "2026-03-17 14:30",
|
|
"quantity": 100,
|
|
"balance_qty": 196,
|
|
"remark": "실사 조정",
|
|
"inspector": "김보곤"
|
|
},
|
|
{
|
|
"id": 2,
|
|
"adjusted_at": "2026-03-16 10:15",
|
|
"quantity": -50,
|
|
"balance_qty": 96,
|
|
"remark": "파손 폐기",
|
|
"inspector": "홍길동"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 2-2. 재고 조정 등록
|
|
|
|
```
|
|
POST /api/v1/stocks/{stockId}/adjustments
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**요청:**
|
|
|
|
```json
|
|
{
|
|
"quantity": 100,
|
|
"remark": "실사 조정"
|
|
}
|
|
```
|
|
|
|
| 필드 | 타입 | 필수 | 설명 |
|
|
|------|------|:----:|------|
|
|
| `quantity` | number | O | 증감 수량 (양수: 증가, 음수: 감소, 0 불가) |
|
|
| `remark` | string | X | 조정 사유 (최대 500자) |
|
|
|
|
**응답:**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "데이터를 생성했습니다.",
|
|
"data": {
|
|
"id": 3,
|
|
"adjusted_at": "2026-03-17 15:00",
|
|
"quantity": 100,
|
|
"balance_qty": 296,
|
|
"remark": "실사 조정",
|
|
"inspector": "김보곤"
|
|
}
|
|
}
|
|
```
|
|
|
|
### stockId 확인 방법
|
|
|
|
재고 상세 조회 응답에서 `stock` 관계의 `id`를 사용:
|
|
|
|
```
|
|
GET /api/v1/stocks/{itemId}
|
|
→ response.data.stock.id ← 이것이 stockId
|
|
```
|
|
|
|
---
|
|
|
|
## TypeScript 타입
|
|
|
|
```typescript
|
|
// 재고 조정 이력
|
|
interface StockAdjustmentRecord {
|
|
id: number;
|
|
adjusted_at: string; // "2026-03-17 14:30"
|
|
quantity: number; // 양수: 증가, 음수: 감소
|
|
balance_qty: number; // 조정 후 잔량
|
|
remark: string | null;
|
|
inspector: string;
|
|
}
|
|
|
|
// 재고 조정 입력 폼
|
|
interface StockAdjustmentForm {
|
|
quantity: number; // 필수, 0 불가
|
|
remark?: string; // 선택, 최대 500자
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Server Action 예시
|
|
|
|
```typescript
|
|
// StockStatus/actions.ts에 추가
|
|
|
|
export async function getStockAdjustments(stockId: number) {
|
|
return fetchApi(`/stocks/${stockId}/adjustments`);
|
|
}
|
|
|
|
export async function createStockAdjustment(
|
|
stockId: number,
|
|
data: StockAdjustmentForm
|
|
) {
|
|
return fetchApi(`/stocks/${stockId}/adjustments`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 재고 조정 모달 UI 개선
|
|
|
|
### 현재 문제
|
|
|
|
- 모달 `max-width: 700px`로 테이블 컬럼(로트번호, 품목코드, 품목유형, 품목명, 규격, 단위, 재고량, 증감 수량)이 잘림
|
|
- 규격 컬럼 이후가 보이지 않아 좌우 스크롤이 필요하지만, 스크롤바가 작고 마우스로 조작이 어려움
|
|
- 모바일/태블릿 환경에서 더 심각
|
|
|
|
### 개선 방향
|
|
|
|
```
|
|
AS-IS:
|
|
Dialog max-width: 700px → 컬럼 잘림, 스크롤 조작 어려움
|
|
|
|
TO-BE:
|
|
화면 폭에 꽉 찬 풀스크린 모달 (또는 max-width: 95vw)
|
|
테이블 영역에 명확한 가로 스크롤바 표시
|
|
```
|
|
|
|
### 구체적 개선사항
|
|
|
|
```
|
|
✅ 모달 너비: max-width 제거 또는 95vw로 확대 (풀스크린 모달 권장)
|
|
✅ 테이블 컨테이너: overflow-x: auto + 명시적 스크롤바 스타일
|
|
✅ 스크롤바 두께: 최소 8px 이상 (얇은 기본 스크롤바 금지)
|
|
✅ 스크롤바 항상 표시: overflow-x: scroll 또는 CSS scrollbar-gutter
|
|
✅ 고정 컬럼(선택): 로트번호/품목코드를 sticky left로 고정하면 더 좋음
|
|
```
|
|
|
|
### CSS 참고
|
|
|
|
```css
|
|
/* 테이블 컨테이너 */
|
|
.adjustment-table-wrapper {
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
/* 스크롤바 항상 표시 + 두께 확보 */
|
|
.adjustment-table-wrapper::-webkit-scrollbar {
|
|
height: 10px;
|
|
}
|
|
.adjustment-table-wrapper::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 5px;
|
|
}
|
|
.adjustment-table-wrapper::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 입고 품목 검색 개선
|
|
|
|
> **상태**: ✅ 구현 완료 (2026-03-18)
|
|
|
|
### 현재 문제
|
|
|
|
1. **검색 결과 20건 제한**: 품목이 많은 테넌트에서 원하는 품목을 찾기 어려움
|
|
2. **모든 품목 유형 표시**: 가공품, 반제품 등 입고와 무관한 품목도 검색됨
|
|
3. **원자재 입고에 맞지 않는 결과**: 철판, 모터 등 미가공 원자재만 검색되어야 함
|
|
|
|
### 근본 원인 (2026-03-18 확인)
|
|
|
|
API와 React 간 **파라미터 이름 불일치**로 인해 필터가 동작하지 않았다:
|
|
|
|
| React가 전송 | API Controller가 읽음 | 결과 |
|
|
|-------------|---------------------|------|
|
|
| `per_page=50` | `size` (기본값 20) | **per_page 무시, 항상 20건** |
|
|
| `itemType=RM,SM,CS` | `type` 또는 `item_type` | **itemType(camelCase) 무시, 전체 품목 반환** |
|
|
|
|
### 수정 내용
|
|
|
|
#### 1) API 수정 — `ItemsController.php`
|
|
|
|
```php
|
|
// AS-IS
|
|
'size' => $request->input('size', 20),
|
|
'item_type' => $request->input('type') ?? $request->input('item_type'),
|
|
|
|
// TO-BE (per_page, itemType 파라미터도 수용)
|
|
'size' => $request->input('size') ?? $request->input('per_page', 20),
|
|
'item_type' => $request->input('type') ?? $request->input('item_type') ?? $request->input('itemType'),
|
|
```
|
|
|
|
#### 2) React 수정 — `ReceivingDetail.tsx`
|
|
|
|
실제 품목검색은 `ItemSearchModal` 컴포넌트를 통해 `fetchItems()` (`lib/api/items.ts`)를 호출한다.
|
|
(`actions.ts`의 `searchItems()`는 미사용 dead code)
|
|
|
|
```typescript
|
|
// AS-IS — itemType 미전달
|
|
<ItemSearchModal
|
|
open={isItemSearchOpen}
|
|
onOpenChange={setIsItemSearchOpen}
|
|
onSelectItem={handleSelectItem}
|
|
/>
|
|
|
|
// TO-BE — 원자재/부자재/소모품만 필터링
|
|
<ItemSearchModal
|
|
open={isItemSearchOpen}
|
|
onOpenChange={setIsItemSearchOpen}
|
|
onSelectItem={handleSelectItem}
|
|
itemType="RM,SM,CS"
|
|
/>
|
|
```
|
|
|
|
### 제외되는 품목 유형
|
|
|
|
| item_type | 한글 | 제외 이유 |
|
|
|-----------|------|----------|
|
|
| `PT` | 부품 | 가공/조립된 형태 |
|
|
| `SF` | 반제품 | 중간 가공품 |
|
|
| `FG` | 완제품 | 최종 생산품 |
|
|
| `BN` | 절곡품 | 절곡 가공품 |
|
|
|
|
> 입고(수입검사)는 미가공 원자재(철판, 모터, 볼트 등)만 대상이므로 `RM,SM,CS`로 제한한다.
|
|
|
|
---
|
|
|
|
## 작업 체크리스트
|
|
|
|
### 입고 목록 (ReceivingList.tsx)
|
|
|
|
- [ ] 헤더의 "재고 조정" 버튼 제거
|
|
- [ ] `isAdjustmentOpen` 상태 변수 제거
|
|
- [ ] `InventoryAdjustmentDialog` import 및 렌더링 제거
|
|
|
|
### 입고 상세 (ReceivingDetail.tsx)
|
|
|
|
- [ ] `lot_no` input → readOnly 변경
|
|
- [ ] placeholder: "저장 시 자동 생성"
|
|
- [ ] `generateLotNo()` 호출 제거
|
|
- [ ] 재고 조정 이력 섹션 렌더링 제거
|
|
|
|
### 재고 상세 (StockStatusDetail.tsx)
|
|
|
|
- [ ] 기본 정보 카드 아래에 재고 조정 섹션 추가
|
|
- [ ] `GET /stocks/{stockId}/adjustments` 연동 → 이력 테이블 렌더링
|
|
- [ ] + 추가 버튼 → 입력 폼/모달 → `POST /stocks/{stockId}/adjustments` 연동
|
|
- [ ] 조정 성공 시 재고량 갱신 (기본 정보 리로드)
|
|
|
|
### 품목 검색 (ItemSearchModal + API)
|
|
|
|
- [x] API `ItemsController`: `per_page`, `itemType`(camelCase) 파라미터 수용 (2026-03-18)
|
|
- [x] `ReceivingDetail.tsx`: `ItemSearchModal`에 `itemType="RM,SM,CS"` prop 전달 (2026-03-18)
|
|
- [ ] (정리) `actions.ts`의 `searchItems()` dead code 제거 (미사용 확인됨)
|
|
|
|
### 재고 조정 모달 (InventoryAdjustmentDialog.tsx)
|
|
|
|
- [ ] 모달 너비: 풀스크린 또는 `max-width: 95vw`로 확대
|
|
- [ ] 테이블 컨테이너에 `overflow-x: auto` + 명시적 스크롤바 스타일
|
|
- [ ] 스크롤바 두께 최소 8px 이상, 항상 표시
|
|
- [ ] (선택) 로트번호/품목코드 컬럼 sticky left 고정
|