11 KiB
11 KiB
[계획서] 렉 구조 등록 - 층(floor) 필수 입력 해제
개요
탑씰 회사의 물류관리 > 창고정보 관리 > 렉 구조 등록 모달에서, "층" 필드를 필수 입력에서 선택 입력으로 변경합니다. 현재 "창고 코드 / 층 / 구역" 3개가 모두 필수로 하드코딩되어 있어, 층을 선택하지 않으면 미리보기 생성과 저장이 불가능합니다.
현재 동작
1. 필수 필드 경고 (RackStructureComponent.tsx:291~298)
층을 선택하지 않으면 빨간 경고가 표시됨:
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() 실행이 차단됨:
if (missingFields.length > 0) {
alert(`다음 필드를 먼저 입력해주세요: ${missingFields.join(", ")}`);
return;
}
3. 위치 코드 생성 (RackStructureComponent.tsx:497~513)
floor가 없으면 기본값 "1"을 사용하여 위치 코드를 생성:
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가 비어있으면 기존 데이터 조회 자체를 건너뜀 → 중복 체크 불가:
if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) {
setExistingLocations([]);
return;
}
5. 렉 구조 화면 감지 (buttonActions.ts:692~698)
floor가 비어있으면 렉 구조 화면으로 인식하지 않음 → 일반 저장으로 빠짐:
const isRackStructureScreen =
context.tableName === "warehouse_location" &&
context.formData?.floor && // ← floor 없으면 false
context.formData?.zone &&
!rackStructureLocations;
6. 저장 전 중복 체크 (buttonActions.ts:2085~2131)
floor가 없으면 중복 체크 전체를 건너뜀:
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 |
| 창고만 선택 | "구역을 먼저 입력해주세요" | 차단 | - | - |
| 아무것도 미선택 | "창고 코드, 구역을 먼저 입력해주세요" | 차단 | - | - |
아키텍처
데이터 흐름 (변경 전)
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 = 선택값]
데이터 흐름 (변경 후)
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]
컴포넌트 관계
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)
// 변경 전
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)
// 변경 전
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)
// 변경 전
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)
// 변경 전
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)
// 변경 전
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로 저장하여 "미입력"을 정확하게 표현 (프로젝트 표준 패턴)
- 특수 문자열("상관없음" 등) 사용하지 않음 (프로젝트 관행에 맞지 않으므로)