351 lines
11 KiB
Markdown
351 lines
11 KiB
Markdown
# [계획서] 렉 구조 등록 - 층(floor) 필수 입력 해제
|
|
|
|
> 관련 문서: [맥락노트](./RFO[맥락]-렉구조-층필수해제.md) | [체크리스트](./RFO[체크]-렉구조-층필수해제.md)
|
|
|
|
## 개요
|
|
|
|
탑씰 회사의 물류관리 > 창고정보 관리 > 렉 구조 등록 모달에서, "층" 필드를 필수 입력에서 선택 입력으로 변경합니다. 현재 "창고 코드 / 층 / 구역" 3개가 모두 필수로 하드코딩되어 있어, 층을 선택하지 않으면 미리보기 생성과 저장이 불가능합니다.
|
|
|
|
---
|
|
|
|
## 현재 동작
|
|
|
|
### 1. 필수 필드 경고 (RackStructureComponent.tsx:291~298)
|
|
|
|
층을 선택하지 않으면 빨간 경고가 표시됨:
|
|
|
|
```tsx
|
|
const missingFields = useMemo(() => {
|
|
const missing: string[] = [];
|
|
if (!context.warehouseCode) missing.push("창고 코드");
|
|
if (!context.floor) missing.push("층"); // ← 하드코딩 필수
|
|
if (!context.zone) missing.push("구역");
|
|
return missing;
|
|
}, [context]);
|
|
```
|
|
|
|
> "다음 필드를 먼저 입력해주세요: **층**"
|
|
|
|
### 2. 미리보기 생성 차단 (RackStructureComponent.tsx:517~521)
|
|
|
|
`missingFields`에 "층"이 포함되어 있으면 `generatePreview()` 실행이 차단됨:
|
|
|
|
```tsx
|
|
if (missingFields.length > 0) {
|
|
alert(`다음 필드를 먼저 입력해주세요: ${missingFields.join(", ")}`);
|
|
return;
|
|
}
|
|
```
|
|
|
|
### 3. 위치 코드 생성 (RackStructureComponent.tsx:497~513)
|
|
|
|
floor가 없으면 기본값 `"1"`을 사용하여 위치 코드를 생성:
|
|
|
|
```tsx
|
|
const floor = context?.floor || "1";
|
|
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
|
// 예: WH001-1층A구역-01-1
|
|
```
|
|
|
|
### 4. 기존 데이터 조회 (RackStructureComponent.tsx:378~432)
|
|
|
|
floor가 비어있으면 기존 데이터 조회 자체를 건너뜀 → 중복 체크 불가:
|
|
|
|
```tsx
|
|
if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) {
|
|
setExistingLocations([]);
|
|
return;
|
|
}
|
|
```
|
|
|
|
### 5. 렉 구조 화면 감지 (buttonActions.ts:692~698)
|
|
|
|
floor가 비어있으면 렉 구조 화면으로 인식하지 않음 → 일반 저장으로 빠짐:
|
|
|
|
```tsx
|
|
const isRackStructureScreen =
|
|
context.tableName === "warehouse_location" &&
|
|
context.formData?.floor && // ← floor 없으면 false
|
|
context.formData?.zone &&
|
|
!rackStructureLocations;
|
|
```
|
|
|
|
### 6. 저장 전 중복 체크 (buttonActions.ts:2085~2131)
|
|
|
|
floor가 없으면 중복 체크 전체를 건너뜀:
|
|
|
|
```tsx
|
|
if (warehouseCode && floor && zone) {
|
|
// 중복 체크 로직
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 변경 후 동작
|
|
|
|
### 1. 필수 필드에서 "층" 제거
|
|
|
|
- "창고 코드"와 "구역"만 필수
|
|
- 층을 선택하지 않아도 경고가 뜨지 않음
|
|
|
|
### 2. 미리보기 생성 정상 동작
|
|
|
|
- 층 없이도 미리보기 생성 가능
|
|
- 위치 코드에서 층 부분을 생략하여 깔끔하게 생성
|
|
|
|
### 3. 위치 코드 생성 규칙 변경
|
|
|
|
- 층 있을 때: `WH001-1층A구역-01-1` (기존과 동일)
|
|
- 층 없을 때: `WH001-A구역-01-1` (층 부분 생략)
|
|
|
|
### 4. 기존 데이터 조회 (중복 체크)
|
|
|
|
- 층 있을 때: `warehouse_code + floor + zone`으로 조회 (기존과 동일)
|
|
- 층 없을 때: `warehouse_code + zone`으로 조회 (floor 조건 제외)
|
|
|
|
### 5. 렉 구조 화면 감지
|
|
|
|
- floor 유무와 관계없이 `warehouse_location` 테이블 + zone 필드가 있으면 렉 구조 화면으로 인식
|
|
|
|
### 6. 저장 시 floor 값
|
|
|
|
- 층 선택함: `floor = "1층"` 등 선택한 값 저장
|
|
- 층 미선택: `floor = NULL`로 저장
|
|
|
|
---
|
|
|
|
## 시각적 예시
|
|
|
|
| 상태 | 경고 메시지 | 미리보기 | 위치 코드 | DB floor 값 |
|
|
|------|------------|---------|-----------|------------|
|
|
| 창고+층+구역 모두 선택 | 없음 | 생성 가능 | `WH001-1층A구역-01-1` | `"1층"` |
|
|
| 창고+구역만 선택 (층 미선택) | 없음 | 생성 가능 | `WH001-A구역-01-1` | `NULL` |
|
|
| 창고만 선택 | "구역을 먼저 입력해주세요" | 차단 | - | - |
|
|
| 아무것도 미선택 | "창고 코드, 구역을 먼저 입력해주세요" | 차단 | - | - |
|
|
|
|
---
|
|
|
|
## 아키텍처
|
|
|
|
### 데이터 흐름 (변경 전)
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[사용자: 창고/층/구역 입력] --> B{필수 필드 검증}
|
|
B -->|층 없음| C[경고: 층을 입력하세요]
|
|
B -->|3개 다 있음| D[기존 데이터 조회<br/>warehouse_code + floor + zone]
|
|
D --> E[미리보기 생성]
|
|
E --> F{저장 버튼}
|
|
F --> G[렉 구조 화면 감지<br/>floor && zone 필수]
|
|
G --> H[중복 체크<br/>warehouse_code + floor + zone]
|
|
H --> I[일괄 INSERT<br/>floor = 선택값]
|
|
```
|
|
|
|
### 데이터 흐름 (변경 후)
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[사용자: 창고/구역 입력<br/>층은 선택사항] --> B{필수 필드 검증}
|
|
B -->|창고 or 구역 없음| C[경고: 해당 필드를 입력하세요]
|
|
B -->|창고+구역 있음| D{floor 값 존재?}
|
|
D -->|있음| E1[기존 데이터 조회<br/>warehouse_code + floor + zone]
|
|
D -->|없음| E2[기존 데이터 조회<br/>warehouse_code + zone]
|
|
E1 --> F[미리보기 생성]
|
|
E2 --> F
|
|
F --> G{저장 버튼}
|
|
G --> H[렉 구조 화면 감지<br/>zone만 필수]
|
|
H --> I{floor 값 존재?}
|
|
I -->|있음| J1[중복 체크<br/>warehouse_code + floor + zone]
|
|
I -->|없음| J2[중복 체크<br/>warehouse_code + zone]
|
|
J1 --> K[일괄 INSERT<br/>floor = 선택값]
|
|
J2 --> K2[일괄 INSERT<br/>floor = NULL]
|
|
```
|
|
|
|
### 컴포넌트 관계
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph 프론트엔드
|
|
A[폼 필드<br/>창고/층/구역] -->|formData| B[RackStructureComponent<br/>필수 검증 + 미리보기]
|
|
B -->|locations 배열| C[buttonActions.ts<br/>화면 감지 + 중복 체크 + 저장]
|
|
end
|
|
subgraph 백엔드
|
|
C -->|POST /dynamic-form/save| D[DynamicFormApi<br/>데이터 저장]
|
|
D --> E[(warehouse_location<br/>floor: nullable)]
|
|
end
|
|
|
|
style B fill:#fff3cd,stroke:#ffc107
|
|
style C fill:#fff3cd,stroke:#ffc107
|
|
```
|
|
|
|
> 노란색 = 이번에 수정하는 부분
|
|
|
|
---
|
|
|
|
## 변경 대상 파일
|
|
|
|
| 파일 | 수정 내용 | 수정 규모 |
|
|
|------|----------|----------|
|
|
| `frontend/lib/registry/components/v2-rack-structure/RackStructureComponent.tsx` | 필수 검증에서 floor 제거, 위치 코드 생성 로직 수정, 기존 데이터 조회 로직 수정 | ~20줄 |
|
|
| `frontend/lib/utils/buttonActions.ts` | 렉 구조 화면 감지 조건 수정, 중복 체크 조건 수정 | ~10줄 |
|
|
|
|
### 사전 확인 필요
|
|
|
|
| 확인 항목 | 내용 |
|
|
|----------|------|
|
|
| DB 스키마 | `warehouse_location.floor` 컬럼이 `NULL` 허용인지 확인. NOT NULL이면 `ALTER TABLE` 필요 |
|
|
|
|
---
|
|
|
|
## 코드 설계
|
|
|
|
### 1. 필수 필드 검증 수정 (RackStructureComponent.tsx:291~298)
|
|
|
|
```tsx
|
|
// 변경 전
|
|
const missingFields = useMemo(() => {
|
|
const missing: string[] = [];
|
|
if (!context.warehouseCode) missing.push("창고 코드");
|
|
if (!context.floor) missing.push("층");
|
|
if (!context.zone) missing.push("구역");
|
|
return missing;
|
|
}, [context]);
|
|
|
|
// 변경 후
|
|
const missingFields = useMemo(() => {
|
|
const missing: string[] = [];
|
|
if (!context.warehouseCode) missing.push("창고 코드");
|
|
if (!context.zone) missing.push("구역");
|
|
return missing;
|
|
}, [context]);
|
|
```
|
|
|
|
### 2. 위치 코드 생성 수정 (RackStructureComponent.tsx:497~513)
|
|
|
|
```tsx
|
|
// 변경 전
|
|
const floor = context?.floor || "1";
|
|
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
|
|
|
// 변경 후
|
|
const floor = context?.floor;
|
|
const floorPrefix = floor ? `${floor}` : "";
|
|
const code = `${warehouseCode}-${floorPrefix}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
|
// 층 있을 때: WH001-1층A구역-01-1
|
|
// 층 없을 때: WH001-A구역-01-1
|
|
```
|
|
|
|
### 3. 기존 데이터 조회 수정 (RackStructureComponent.tsx:378~432)
|
|
|
|
```tsx
|
|
// 변경 전
|
|
if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) {
|
|
setExistingLocations([]);
|
|
return;
|
|
}
|
|
|
|
const searchParams = {
|
|
warehouse_code: { value: warehouseCodeForQuery, operator: "equals" },
|
|
floor: { value: floorForQuery, operator: "equals" },
|
|
zone: { value: zoneForQuery, operator: "equals" },
|
|
};
|
|
|
|
// 변경 후
|
|
if (!warehouseCodeForQuery || !zoneForQuery) {
|
|
setExistingLocations([]);
|
|
return;
|
|
}
|
|
|
|
const searchParams: Record<string, any> = {
|
|
warehouse_code: { value: warehouseCodeForQuery, operator: "equals" },
|
|
zone: { value: zoneForQuery, operator: "equals" },
|
|
};
|
|
if (floorForQuery) {
|
|
searchParams.floor = { value: floorForQuery, operator: "equals" };
|
|
}
|
|
```
|
|
|
|
### 4. 렉 구조 화면 감지 수정 (buttonActions.ts:692~698)
|
|
|
|
```tsx
|
|
// 변경 전
|
|
const isRackStructureScreen =
|
|
context.tableName === "warehouse_location" &&
|
|
context.formData?.floor &&
|
|
context.formData?.zone &&
|
|
!rackStructureLocations;
|
|
|
|
// 변경 후
|
|
const isRackStructureScreen =
|
|
context.tableName === "warehouse_location" &&
|
|
context.formData?.zone &&
|
|
!rackStructureLocations;
|
|
```
|
|
|
|
### 5. 저장 전 중복 체크 수정 (buttonActions.ts:2085~2131)
|
|
|
|
```tsx
|
|
// 변경 전
|
|
if (warehouseCode && floor && zone) {
|
|
const existingResponse = await DynamicFormApi.getTableData(tableName, {
|
|
search: {
|
|
warehouse_code: { value: warehouseCode, operator: "equals" },
|
|
floor: { value: floor, operator: "equals" },
|
|
zone: { value: zone, operator: "equals" },
|
|
},
|
|
// ...
|
|
});
|
|
}
|
|
|
|
// 변경 후
|
|
if (warehouseCode && zone) {
|
|
const searchParams: Record<string, any> = {
|
|
warehouse_code: { value: warehouseCode, operator: "equals" },
|
|
zone: { value: zone, operator: "equals" },
|
|
};
|
|
if (floor) {
|
|
searchParams.floor = { value: floor, operator: "equals" };
|
|
}
|
|
|
|
const existingResponse = await DynamicFormApi.getTableData(tableName, {
|
|
search: searchParams,
|
|
// ...
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 적용 범위 및 영향도
|
|
|
|
### 이번 변경은 전역 설정
|
|
|
|
방법 B는 렉 구조 컴포넌트 코드에서 직접 "층 필수"를 제거하는 방식이므로, 이 컴포넌트를 사용하는 **모든 회사**에 동일하게 적용됩니다.
|
|
|
|
| 회사 | 변경 후 |
|
|
|------|--------|
|
|
| 탑씰 | 층 안 골라도 됨 (요청 사항) |
|
|
| 다른 회사 | 층 안 골라도 됨 (동일하게 적용) |
|
|
|
|
### 기존 사용자에 대한 영향
|
|
|
|
- 층을 안 골라도 **되는** 것이지, 안 골라야 **하는** 것이 아님
|
|
- 기존처럼 층을 선택하면 **완전히 동일하게** 동작함 (하위 호환 보장)
|
|
- 즉, 기존 사용 패턴을 유지하는 회사에는 아무런 차이가 없음
|
|
|
|
### 회사별 독립 제어가 필요한 경우
|
|
|
|
만약 특정 회사는 층을 필수로 유지하고, 다른 회사는 선택으로 해야 하는 상황이 발생하면, 방법 A(설정 기능 추가)로 업그레이드가 필요합니다. 이번 방법 B의 변경은 향후 방법 A로 전환할 때 충돌 없이 확장 가능합니다.
|
|
|
|
---
|
|
|
|
## 설계 원칙
|
|
|
|
- "창고 코드"와 "구역"의 필수 검증은 기존과 동일하게 유지
|
|
- 층을 선택한 경우의 동작은 기존과 완전히 동일 (하위 호환)
|
|
- 층 미선택 시 위치 코드에서 층 부분을 깔끔하게 생략 (폴백값 "1" 사용하지 않음)
|
|
- 중복 체크는 가용한 필드 기준으로 수행 (floor 없으면 warehouse_code + zone 기준)
|
|
- DB에는 NULL로 저장하여 "미입력"을 정확하게 표현 (프로젝트 표준 패턴)
|
|
- 특수 문자열("상관없음" 등) 사용하지 않음 (프로젝트 관행에 맞지 않으므로)
|