창고 렉 구조 등록 컴포넌트 중복 방지기능 추가
This commit is contained in:
parent
76bad47bc7
commit
ae7c47ee5f
|
|
@ -527,6 +527,53 @@ export const deleteColumnMappingsByColumn = async (req: AuthenticatedRequest, re
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리 코드로 라벨 조회
|
||||||
|
*
|
||||||
|
* POST /api/table-categories/labels-by-codes
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - valueCodes: 카테고리 코드 배열 (예: ["CATEGORY_767659DCUF", "CATEGORY_8292565608"])
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* - { [code]: label } 형태의 매핑 객체
|
||||||
|
*/
|
||||||
|
export const getCategoryLabelsByCodes = async (req: AuthenticatedRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const companyCode = req.user!.companyCode;
|
||||||
|
const { valueCodes } = req.body;
|
||||||
|
|
||||||
|
if (!valueCodes || !Array.isArray(valueCodes) || valueCodes.length === 0) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("카테고리 코드로 라벨 조회", {
|
||||||
|
valueCodes,
|
||||||
|
companyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = await tableCategoryValueService.getCategoryLabelsByCodes(
|
||||||
|
valueCodes,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: labels,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`카테고리 라벨 조회 실패: ${error.message}`);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "카테고리 라벨 조회 중 오류가 발생했습니다",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2레벨 메뉴 목록 조회
|
* 2레벨 메뉴 목록 조회
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
deleteColumnMapping,
|
deleteColumnMapping,
|
||||||
deleteColumnMappingsByColumn,
|
deleteColumnMappingsByColumn,
|
||||||
getSecondLevelMenus,
|
getSecondLevelMenus,
|
||||||
|
getCategoryLabelsByCodes,
|
||||||
} from "../controllers/tableCategoryValueController";
|
} from "../controllers/tableCategoryValueController";
|
||||||
import { authenticateToken } from "../middleware/authMiddleware";
|
import { authenticateToken } from "../middleware/authMiddleware";
|
||||||
|
|
||||||
|
|
@ -42,6 +43,9 @@ router.post("/values/bulk-delete", bulkDeleteCategoryValues);
|
||||||
// 카테고리 값 순서 변경
|
// 카테고리 값 순서 변경
|
||||||
router.post("/values/reorder", reorderCategoryValues);
|
router.post("/values/reorder", reorderCategoryValues);
|
||||||
|
|
||||||
|
// 카테고리 코드로 라벨 조회
|
||||||
|
router.post("/labels-by-codes", getCategoryLabelsByCodes);
|
||||||
|
|
||||||
// ================================================
|
// ================================================
|
||||||
// 컬럼 매핑 관련 라우트 (논리명 ↔ 물리명)
|
// 컬럼 매핑 관련 라우트 (논리명 ↔ 물리명)
|
||||||
// ================================================
|
// ================================================
|
||||||
|
|
|
||||||
|
|
@ -907,8 +907,27 @@ class DataService {
|
||||||
return validation.error!;
|
return validation.error!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = Object.keys(data);
|
// 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외)
|
||||||
const values = Object.values(data);
|
const tableColumns = await this.getTableColumnsSimple(tableName);
|
||||||
|
const validColumnNames = new Set(tableColumns.map((col: any) => col.column_name));
|
||||||
|
|
||||||
|
const invalidColumns: string[] = [];
|
||||||
|
const filteredData = Object.fromEntries(
|
||||||
|
Object.entries(data).filter(([key]) => {
|
||||||
|
if (validColumnNames.has(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
invalidColumns.push(key);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invalidColumns.length > 0) {
|
||||||
|
console.log(`⚠️ [createRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = Object.keys(filteredData);
|
||||||
|
const values = Object.values(filteredData);
|
||||||
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
|
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
|
||||||
const columnNames = columns.map((col) => `"${col}"`).join(", ");
|
const columnNames = columns.map((col) => `"${col}"`).join(", ");
|
||||||
|
|
||||||
|
|
@ -951,9 +970,28 @@ class DataService {
|
||||||
|
|
||||||
// _relationInfo 추출 (조인 관계 업데이트용)
|
// _relationInfo 추출 (조인 관계 업데이트용)
|
||||||
const relationInfo = data._relationInfo;
|
const relationInfo = data._relationInfo;
|
||||||
const cleanData = { ...data };
|
let cleanData = { ...data };
|
||||||
delete cleanData._relationInfo;
|
delete cleanData._relationInfo;
|
||||||
|
|
||||||
|
// 🆕 테이블에 존재하는 컬럼만 필터링 (존재하지 않는 컬럼 제외)
|
||||||
|
const tableColumns = await this.getTableColumnsSimple(tableName);
|
||||||
|
const validColumnNames = new Set(tableColumns.map((col: any) => col.column_name));
|
||||||
|
|
||||||
|
const invalidColumns: string[] = [];
|
||||||
|
cleanData = Object.fromEntries(
|
||||||
|
Object.entries(cleanData).filter(([key]) => {
|
||||||
|
if (validColumnNames.has(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
invalidColumns.push(key);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invalidColumns.length > 0) {
|
||||||
|
console.log(`⚠️ [updateRecord] 테이블에 없는 컬럼 제외: ${invalidColumns.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Primary Key 컬럼 찾기
|
// Primary Key 컬럼 찾기
|
||||||
const pkResult = await query<{ attname: string }>(
|
const pkResult = await query<{ attname: string }>(
|
||||||
`SELECT a.attname
|
`SELECT a.attname
|
||||||
|
|
|
||||||
|
|
@ -1258,6 +1258,70 @@ class TableCategoryValueService {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리 코드로 라벨 조회
|
||||||
|
*
|
||||||
|
* @param valueCodes - 카테고리 코드 배열
|
||||||
|
* @param companyCode - 회사 코드
|
||||||
|
* @returns { [code]: label } 형태의 매핑 객체
|
||||||
|
*/
|
||||||
|
async getCategoryLabelsByCodes(
|
||||||
|
valueCodes: string[],
|
||||||
|
companyCode: string
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
if (!valueCodes || valueCodes.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("카테고리 코드로 라벨 조회", { valueCodes, companyCode });
|
||||||
|
|
||||||
|
const pool = getPool();
|
||||||
|
|
||||||
|
// 동적으로 파라미터 플레이스홀더 생성
|
||||||
|
const placeholders = valueCodes.map((_, i) => `$${i + 1}`).join(", ");
|
||||||
|
|
||||||
|
let query: string;
|
||||||
|
let params: any[];
|
||||||
|
|
||||||
|
if (companyCode === "*") {
|
||||||
|
// 최고 관리자: 모든 카테고리 값 조회
|
||||||
|
query = `
|
||||||
|
SELECT value_code, value_label
|
||||||
|
FROM table_column_category_values
|
||||||
|
WHERE value_code IN (${placeholders})
|
||||||
|
AND is_active = true
|
||||||
|
`;
|
||||||
|
params = valueCodes;
|
||||||
|
} else {
|
||||||
|
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
||||||
|
query = `
|
||||||
|
SELECT value_code, value_label
|
||||||
|
FROM table_column_category_values
|
||||||
|
WHERE value_code IN (${placeholders})
|
||||||
|
AND is_active = true
|
||||||
|
AND (company_code = $${valueCodes.length + 1} OR company_code = '*')
|
||||||
|
`;
|
||||||
|
params = [...valueCodes, companyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool.query(query, params);
|
||||||
|
|
||||||
|
// { [code]: label } 형태로 변환
|
||||||
|
const labels: Record<string, string> = {};
|
||||||
|
for (const row of result.rows) {
|
||||||
|
labels[row.value_code] = row.value_label;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`카테고리 라벨 ${Object.keys(labels).length}개 조회 완료`, { companyCode });
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`카테고리 코드로 라벨 조회 실패: ${error.message}`, { error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TableCategoryValueService();
|
export default new TableCategoryValueService();
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,39 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 렉 구조 컴포넌트 처리
|
||||||
|
if (comp.type === "component" && componentType === "rack-structure") {
|
||||||
|
const { RackStructureComponent } = require("@/lib/registry/components/rack-structure/RackStructureComponent");
|
||||||
|
const componentConfig = (comp as any).componentConfig || {};
|
||||||
|
// config가 중첩되어 있을 수 있음: componentConfig.config 또는 componentConfig 직접
|
||||||
|
const rackConfig = componentConfig.config || componentConfig;
|
||||||
|
|
||||||
|
console.log("🏗️ 렉 구조 컴포넌트 렌더링:", {
|
||||||
|
componentType,
|
||||||
|
componentConfig,
|
||||||
|
rackConfig,
|
||||||
|
fieldMapping: rackConfig.fieldMapping,
|
||||||
|
formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full overflow-auto">
|
||||||
|
<RackStructureComponent
|
||||||
|
config={rackConfig}
|
||||||
|
formData={formData}
|
||||||
|
tableName={tableName}
|
||||||
|
onChange={(locations: any[]) => {
|
||||||
|
console.log("📦 렉 구조 위치 데이터 변경:", locations.length, "개");
|
||||||
|
// 컴포넌트의 columnName을 키로 사용
|
||||||
|
const fieldKey = (comp as any).columnName || "_rackStructureLocations";
|
||||||
|
updateFormData(fieldKey, locations);
|
||||||
|
}}
|
||||||
|
isPreview={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
|
const { widgetType, label, placeholder, required, readonly, columnName } = comp;
|
||||||
const fieldName = columnName || comp.id;
|
const fieldName = columnName || comp.id;
|
||||||
const currentValue = formData[fieldName] || "";
|
const currentValue = formData[fieldName] || "";
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,29 @@ export async function reorderCategoryValues(orderedValueIds: number[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 카테고리 코드로 라벨 조회
|
||||||
|
*
|
||||||
|
* @param valueCodes - 카테고리 코드 배열 (예: ["CATEGORY_767659DCUF", "CATEGORY_8292565608"])
|
||||||
|
* @returns { [code]: label } 형태의 매핑 객체
|
||||||
|
*/
|
||||||
|
export async function getCategoryLabelsByCodes(valueCodes: string[]) {
|
||||||
|
try {
|
||||||
|
if (!valueCodes || valueCodes.length === 0) {
|
||||||
|
return { success: true, data: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiClient.post<{
|
||||||
|
success: boolean;
|
||||||
|
data: Record<string, string>;
|
||||||
|
}>("/table-categories/labels-by-codes", { valueCodes });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("카테고리 라벨 조회 실패:", error);
|
||||||
|
return { success: false, error: error.message, data: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================================================
|
// ================================================
|
||||||
// 컬럼 매핑 관련 API (논리명 ↔ 물리명)
|
// 컬럼 매핑 관련 API (논리명 ↔ 물리명)
|
||||||
// ================================================
|
// ================================================
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import {
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue";
|
||||||
|
import { DynamicFormApi } from "@/lib/api/dynamicForm";
|
||||||
import {
|
import {
|
||||||
RackStructureComponentProps,
|
RackStructureComponentProps,
|
||||||
RackLineCondition,
|
RackLineCondition,
|
||||||
|
|
@ -31,6 +33,13 @@ import {
|
||||||
RackStructureContext,
|
RackStructureContext,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
// 기존 위치 데이터 타입
|
||||||
|
interface ExistingLocation {
|
||||||
|
row_num: string;
|
||||||
|
level_num: string;
|
||||||
|
location_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 고유 ID 생성
|
// 고유 ID 생성
|
||||||
const generateId = () => `cond_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
const generateId = () => `cond_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
|
@ -185,6 +194,7 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
onConditionsChange,
|
onConditionsChange,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
|
tableName,
|
||||||
}) => {
|
}) => {
|
||||||
// 조건 목록
|
// 조건 목록
|
||||||
const [conditions, setConditions] = useState<RackLineCondition[]>(
|
const [conditions, setConditions] = useState<RackLineCondition[]>(
|
||||||
|
|
@ -200,6 +210,11 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
// 미리보기 데이터
|
// 미리보기 데이터
|
||||||
const [previewData, setPreviewData] = useState<GeneratedLocation[]>([]);
|
const [previewData, setPreviewData] = useState<GeneratedLocation[]>([]);
|
||||||
const [isPreviewGenerated, setIsPreviewGenerated] = useState(false);
|
const [isPreviewGenerated, setIsPreviewGenerated] = useState(false);
|
||||||
|
|
||||||
|
// 기존 데이터 중복 체크 관련 상태
|
||||||
|
const [existingLocations, setExistingLocations] = useState<ExistingLocation[]>([]);
|
||||||
|
const [isCheckingDuplicates, setIsCheckingDuplicates] = useState(false);
|
||||||
|
const [duplicateErrors, setDuplicateErrors] = useState<{ row: number; existingLevels: number[] }[]>([]);
|
||||||
|
|
||||||
// 설정값
|
// 설정값
|
||||||
const maxConditions = config.maxConditions || 10;
|
const maxConditions = config.maxConditions || 10;
|
||||||
|
|
@ -208,6 +223,60 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
const readonly = config.readonly || isPreview;
|
const readonly = config.readonly || isPreview;
|
||||||
const fieldMapping = config.fieldMapping || {};
|
const fieldMapping = config.fieldMapping || {};
|
||||||
|
|
||||||
|
// 카테고리 라벨 캐시 상태
|
||||||
|
const [categoryLabels, setCategoryLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 카테고리 코드인지 확인
|
||||||
|
const isCategoryCode = (value: string | undefined): boolean => {
|
||||||
|
return typeof value === "string" && value.startsWith("CATEGORY_");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 카테고리 라벨 조회 (비동기)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCategoryLabels = async () => {
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
// 카테고리 코드인 값들만 수집
|
||||||
|
const valuesToLookup: string[] = [];
|
||||||
|
const fieldsToCheck = [
|
||||||
|
fieldMapping.floorField ? formData[fieldMapping.floorField] : undefined,
|
||||||
|
fieldMapping.zoneField ? formData[fieldMapping.zoneField] : undefined,
|
||||||
|
fieldMapping.locationTypeField ? formData[fieldMapping.locationTypeField] : undefined,
|
||||||
|
fieldMapping.statusField ? formData[fieldMapping.statusField] : undefined,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const value of fieldsToCheck) {
|
||||||
|
if (value && isCategoryCode(value) && !categoryLabels[value]) {
|
||||||
|
valuesToLookup.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valuesToLookup.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 카테고리 코드로 라벨 일괄 조회
|
||||||
|
const response = await getCategoryLabelsByCodes(valuesToLookup);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
console.log("✅ 카테고리 라벨 조회 완료:", response.data);
|
||||||
|
setCategoryLabels((prev) => ({ ...prev, ...response.data }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("카테고리 라벨 조회 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCategoryLabels();
|
||||||
|
}, [formData, fieldMapping]);
|
||||||
|
|
||||||
|
// 카테고리 코드를 라벨로 변환하는 헬퍼 함수
|
||||||
|
const getCategoryLabel = useCallback((value: string | undefined): string | undefined => {
|
||||||
|
if (!value) return undefined;
|
||||||
|
if (isCategoryCode(value)) {
|
||||||
|
return categoryLabels[value] || value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}, [categoryLabels]);
|
||||||
|
|
||||||
// 필드 매핑을 통해 formData에서 컨텍스트 추출
|
// 필드 매핑을 통해 formData에서 컨텍스트 추출
|
||||||
const context: RackStructureContext = useMemo(() => {
|
const context: RackStructureContext = useMemo(() => {
|
||||||
// propContext가 있으면 우선 사용
|
// propContext가 있으면 우선 사용
|
||||||
|
|
@ -216,27 +285,33 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
// formData와 fieldMapping을 사용하여 컨텍스트 생성
|
// formData와 fieldMapping을 사용하여 컨텍스트 생성
|
||||||
if (!formData) return {};
|
if (!formData) return {};
|
||||||
|
|
||||||
return {
|
const rawFloor = fieldMapping.floorField ? formData[fieldMapping.floorField] : undefined;
|
||||||
|
const rawZone = fieldMapping.zoneField ? formData[fieldMapping.zoneField] : undefined;
|
||||||
|
const rawLocationType = fieldMapping.locationTypeField ? formData[fieldMapping.locationTypeField] : undefined;
|
||||||
|
const rawStatus = fieldMapping.statusField ? formData[fieldMapping.statusField] : undefined;
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
warehouseCode: fieldMapping.warehouseCodeField
|
warehouseCode: fieldMapping.warehouseCodeField
|
||||||
? formData[fieldMapping.warehouseCodeField]
|
? formData[fieldMapping.warehouseCodeField]
|
||||||
: undefined,
|
: undefined,
|
||||||
warehouseName: fieldMapping.warehouseNameField
|
warehouseName: fieldMapping.warehouseNameField
|
||||||
? formData[fieldMapping.warehouseNameField]
|
? formData[fieldMapping.warehouseNameField]
|
||||||
: undefined,
|
: undefined,
|
||||||
floor: fieldMapping.floorField
|
// 카테고리 값은 라벨로 변환
|
||||||
? formData[fieldMapping.floorField]?.toString()
|
floor: getCategoryLabel(rawFloor?.toString()),
|
||||||
: undefined,
|
zone: getCategoryLabel(rawZone),
|
||||||
zone: fieldMapping.zoneField
|
locationType: getCategoryLabel(rawLocationType),
|
||||||
? formData[fieldMapping.zoneField]
|
status: getCategoryLabel(rawStatus),
|
||||||
: undefined,
|
|
||||||
locationType: fieldMapping.locationTypeField
|
|
||||||
? formData[fieldMapping.locationTypeField]
|
|
||||||
: undefined,
|
|
||||||
status: fieldMapping.statusField
|
|
||||||
? formData[fieldMapping.statusField]
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}, [propContext, formData, fieldMapping]);
|
|
||||||
|
console.log("🏗️ [RackStructure] context 생성:", {
|
||||||
|
fieldMapping,
|
||||||
|
rawValues: { rawFloor, rawZone, rawLocationType, rawStatus },
|
||||||
|
context: ctx,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}, [propContext, formData, fieldMapping, getCategoryLabel]);
|
||||||
|
|
||||||
// 필수 필드 검증
|
// 필수 필드 검증
|
||||||
const missingFields = useMemo(() => {
|
const missingFields = useMemo(() => {
|
||||||
|
|
@ -283,6 +358,154 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
setConditions((prev) => prev.filter((cond) => cond.id !== id));
|
setConditions((prev) => prev.filter((cond) => cond.id !== id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 열 범위 중복 검사
|
||||||
|
const rowOverlapErrors = useMemo(() => {
|
||||||
|
const errors: { conditionIndex: number; overlappingWith: number; overlappingRows: number[] }[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < conditions.length; i++) {
|
||||||
|
const cond1 = conditions[i];
|
||||||
|
if (cond1.startRow <= 0 || cond1.endRow < cond1.startRow) continue;
|
||||||
|
|
||||||
|
for (let j = i + 1; j < conditions.length; j++) {
|
||||||
|
const cond2 = conditions[j];
|
||||||
|
if (cond2.startRow <= 0 || cond2.endRow < cond2.startRow) continue;
|
||||||
|
|
||||||
|
// 범위 겹침 확인
|
||||||
|
const overlapStart = Math.max(cond1.startRow, cond2.startRow);
|
||||||
|
const overlapEnd = Math.min(cond1.endRow, cond2.endRow);
|
||||||
|
|
||||||
|
if (overlapStart <= overlapEnd) {
|
||||||
|
// 겹치는 열 목록
|
||||||
|
const overlappingRows: number[] = [];
|
||||||
|
for (let r = overlapStart; r <= overlapEnd; r++) {
|
||||||
|
overlappingRows.push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
conditionIndex: i,
|
||||||
|
overlappingWith: j,
|
||||||
|
overlappingRows,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}, [conditions]);
|
||||||
|
|
||||||
|
// 중복 열이 있는지 확인
|
||||||
|
const hasRowOverlap = rowOverlapErrors.length > 0;
|
||||||
|
|
||||||
|
// 기존 데이터 조회를 위한 값 추출 (useMemo 객체 참조 문제 방지)
|
||||||
|
const warehouseCodeForQuery = context.warehouseCode;
|
||||||
|
const floorForQuery = context.floor;
|
||||||
|
const zoneForQuery = context.zone;
|
||||||
|
|
||||||
|
// 기존 데이터 조회 (창고/층/구역이 변경될 때마다)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadExistingLocations = async () => {
|
||||||
|
console.log("🏗️ [RackStructure] 기존 데이터 조회 체크:", {
|
||||||
|
warehouseCode: warehouseCodeForQuery,
|
||||||
|
floor: floorForQuery,
|
||||||
|
zone: zoneForQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 필수 조건이 충족되지 않으면 기존 데이터 초기화
|
||||||
|
if (!warehouseCodeForQuery || !floorForQuery || !zoneForQuery) {
|
||||||
|
console.log("⚠️ [RackStructure] 필수 조건 미충족 - 조회 스킵");
|
||||||
|
setExistingLocations([]);
|
||||||
|
setDuplicateErrors([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckingDuplicates(true);
|
||||||
|
try {
|
||||||
|
// warehouse_location 테이블에서 해당 창고/층/구역의 기존 데이터 조회
|
||||||
|
const filterParams = {
|
||||||
|
warehouse_id: warehouseCodeForQuery,
|
||||||
|
floor: floorForQuery,
|
||||||
|
zone: zoneForQuery,
|
||||||
|
};
|
||||||
|
console.log("🔍 기존 위치 데이터 조회 시작:", filterParams);
|
||||||
|
|
||||||
|
const response = await DynamicFormApi.getTableData("warehouse_location", {
|
||||||
|
filters: filterParams,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1000, // 충분히 큰 값
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🔍 기존 위치 데이터 응답:", response);
|
||||||
|
|
||||||
|
// API 응답 구조: { success: true, data: [...] } 또는 { success: true, data: { data: [...] } }
|
||||||
|
const dataArray = Array.isArray(response.data)
|
||||||
|
? response.data
|
||||||
|
: (response.data?.data || []);
|
||||||
|
|
||||||
|
if (response.success && dataArray.length > 0) {
|
||||||
|
const existing = dataArray.map((item: any) => ({
|
||||||
|
row_num: item.row_num,
|
||||||
|
level_num: item.level_num,
|
||||||
|
location_code: item.location_code,
|
||||||
|
}));
|
||||||
|
setExistingLocations(existing);
|
||||||
|
console.log("✅ 기존 위치 데이터 조회 완료:", existing.length, "개", existing);
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ 기존 위치 데이터 없음 또는 조회 실패");
|
||||||
|
setExistingLocations([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("기존 위치 데이터 조회 실패:", error);
|
||||||
|
setExistingLocations([]);
|
||||||
|
} finally {
|
||||||
|
setIsCheckingDuplicates(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadExistingLocations();
|
||||||
|
}, [warehouseCodeForQuery, floorForQuery, zoneForQuery]);
|
||||||
|
|
||||||
|
// 조건 변경 시 기존 데이터와 중복 체크
|
||||||
|
useEffect(() => {
|
||||||
|
if (existingLocations.length === 0) {
|
||||||
|
setDuplicateErrors([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 조건에서 생성될 열 목록
|
||||||
|
const plannedRows = new Map<number, number[]>(); // row -> levels
|
||||||
|
conditions.forEach((cond) => {
|
||||||
|
if (cond.startRow > 0 && cond.endRow >= cond.startRow && cond.levels > 0) {
|
||||||
|
for (let row = cond.startRow; row <= cond.endRow; row++) {
|
||||||
|
const levels: number[] = [];
|
||||||
|
for (let level = 1; level <= cond.levels; level++) {
|
||||||
|
levels.push(level);
|
||||||
|
}
|
||||||
|
plannedRows.set(row, levels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 기존 데이터와 중복 체크
|
||||||
|
const errors: { row: number; existingLevels: number[] }[] = [];
|
||||||
|
plannedRows.forEach((levels, row) => {
|
||||||
|
const existingForRow = existingLocations.filter(
|
||||||
|
(loc) => parseInt(loc.row_num) === row
|
||||||
|
);
|
||||||
|
if (existingForRow.length > 0) {
|
||||||
|
const existingLevels = existingForRow.map((loc) => parseInt(loc.level_num));
|
||||||
|
const duplicateLevels = levels.filter((l) => existingLevels.includes(l));
|
||||||
|
if (duplicateLevels.length > 0) {
|
||||||
|
errors.push({ row, existingLevels: duplicateLevels });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setDuplicateErrors(errors);
|
||||||
|
}, [conditions, existingLocations]);
|
||||||
|
|
||||||
|
// 기존 데이터와 중복이 있는지 확인
|
||||||
|
const hasDuplicateWithExisting = duplicateErrors.length > 0;
|
||||||
|
|
||||||
// 통계 계산
|
// 통계 계산
|
||||||
const statistics = useMemo(() => {
|
const statistics = useMemo(() => {
|
||||||
let totalLocations = 0;
|
let totalLocations = 0;
|
||||||
|
|
@ -312,11 +535,12 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
const floor = context?.floor || "1";
|
const floor = context?.floor || "1";
|
||||||
const zone = context?.zone || "A";
|
const zone = context?.zone || "A";
|
||||||
|
|
||||||
// 코드 생성 (예: WH001-1A-01-1)
|
// 코드 생성 (예: WH001-1층D구역-01-1)
|
||||||
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
const code = `${warehouseCode}-${floor}${zone}-${row.toString().padStart(2, "0")}-${level}`;
|
||||||
|
|
||||||
// 이름 생성 (예: A구역-01열-1단)
|
// 이름 생성 - zone에 이미 "구역"이 포함되어 있으면 그대로 사용
|
||||||
const name = `${zone}구역-${row.toString().padStart(2, "0")}열-${level}단`;
|
const zoneName = zone.includes("구역") ? zone : `${zone}구역`;
|
||||||
|
const name = `${zoneName}-${row.toString().padStart(2, "0")}열-${level}단`;
|
||||||
|
|
||||||
return { code, name };
|
return { code, name };
|
||||||
},
|
},
|
||||||
|
|
@ -325,12 +549,39 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
|
|
||||||
// 미리보기 생성
|
// 미리보기 생성
|
||||||
const generatePreview = useCallback(() => {
|
const generatePreview = useCallback(() => {
|
||||||
|
console.log("🔍 [generatePreview] 검증 시작:", {
|
||||||
|
missingFields,
|
||||||
|
hasRowOverlap,
|
||||||
|
hasDuplicateWithExisting,
|
||||||
|
duplicateErrorsCount: duplicateErrors.length,
|
||||||
|
existingLocationsCount: existingLocations.length,
|
||||||
|
});
|
||||||
|
|
||||||
// 필수 필드 검증
|
// 필수 필드 검증
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
alert(`다음 필드를 먼저 입력해주세요: ${missingFields.join(", ")}`);
|
alert(`다음 필드를 먼저 입력해주세요: ${missingFields.join(", ")}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 열 범위 중복 검증
|
||||||
|
if (hasRowOverlap) {
|
||||||
|
const overlapInfo = rowOverlapErrors.map((err) => {
|
||||||
|
const rows = err.overlappingRows.join(", ");
|
||||||
|
return `조건 ${err.conditionIndex + 1}과 조건 ${err.overlappingWith + 1}의 ${rows}열`;
|
||||||
|
}).join("\n");
|
||||||
|
alert(`열 범위가 중복됩니다:\n${overlapInfo}\n\n중복된 열을 수정해주세요.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 데이터와 중복 검증 - duplicateErrors 직접 체크
|
||||||
|
if (duplicateErrors.length > 0) {
|
||||||
|
const duplicateInfo = duplicateErrors.map((err) => {
|
||||||
|
return `${err.row}열 ${err.existingLevels.join(", ")}단`;
|
||||||
|
}).join(", ");
|
||||||
|
alert(`이미 등록된 위치가 있습니다:\n${duplicateInfo}\n\n해당 열/단을 제외하고 등록하거나, 기존 데이터를 삭제해주세요.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const locations: GeneratedLocation[] = [];
|
const locations: GeneratedLocation[] = [];
|
||||||
|
|
||||||
conditions.forEach((cond) => {
|
conditions.forEach((cond) => {
|
||||||
|
|
@ -338,15 +589,17 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
for (let row = cond.startRow; row <= cond.endRow; row++) {
|
for (let row = cond.startRow; row <= cond.endRow; row++) {
|
||||||
for (let level = 1; level <= cond.levels; level++) {
|
for (let level = 1; level <= cond.levels; level++) {
|
||||||
const { code, name } = generateLocationCode(row, level);
|
const { code, name } = generateLocationCode(row, level);
|
||||||
|
// 테이블 컬럼명과 동일하게 생성
|
||||||
locations.push({
|
locations.push({
|
||||||
rowNum: row,
|
row_num: String(row),
|
||||||
levelNum: level,
|
level_num: String(level),
|
||||||
locationCode: code,
|
location_code: code,
|
||||||
locationName: name,
|
location_name: name,
|
||||||
locationType: context?.locationType || "선반",
|
location_type: context?.locationType || "선반",
|
||||||
status: context?.status || "사용",
|
status: context?.status || "사용",
|
||||||
// 추가 필드
|
// 추가 필드 (테이블 컬럼명과 동일)
|
||||||
warehouseCode: context?.warehouseCode,
|
warehouse_id: context?.warehouseCode,
|
||||||
|
warehouse_name: context?.warehouseName,
|
||||||
floor: context?.floor,
|
floor: context?.floor,
|
||||||
zone: context?.zone,
|
zone: context?.zone,
|
||||||
});
|
});
|
||||||
|
|
@ -357,14 +610,14 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
|
|
||||||
// 정렬: 열 -> 단 순서
|
// 정렬: 열 -> 단 순서
|
||||||
locations.sort((a, b) => {
|
locations.sort((a, b) => {
|
||||||
if (a.rowNum !== b.rowNum) return a.rowNum - b.rowNum;
|
if (a.row_num !== b.row_num) return parseInt(a.row_num) - parseInt(b.row_num);
|
||||||
return a.levelNum - b.levelNum;
|
return parseInt(a.level_num) - parseInt(b.level_num);
|
||||||
});
|
});
|
||||||
|
|
||||||
setPreviewData(locations);
|
setPreviewData(locations);
|
||||||
setIsPreviewGenerated(true);
|
setIsPreviewGenerated(true);
|
||||||
onChange?.(locations);
|
onChange?.(locations);
|
||||||
}, [conditions, context, generateLocationCode, onChange, missingFields]);
|
}, [conditions, context, generateLocationCode, onChange, missingFields, hasRowOverlap, duplicateErrors, existingLocations, rowOverlapErrors]);
|
||||||
|
|
||||||
// 템플릿 저장
|
// 템플릿 저장
|
||||||
const saveTemplate = useCallback(() => {
|
const saveTemplate = useCallback(() => {
|
||||||
|
|
@ -448,6 +701,66 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 열 범위 중복 경고 */}
|
||||||
|
{hasRowOverlap && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<strong>열 범위가 중복됩니다!</strong>
|
||||||
|
<ul className="mt-1 list-inside list-disc text-xs">
|
||||||
|
{rowOverlapErrors.map((err, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
조건 {err.conditionIndex + 1}과 조건 {err.overlappingWith + 1}: {err.overlappingRows.join(", ")}열 중복
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<span className="mt-1 block text-xs">
|
||||||
|
중복된 열 범위를 수정해주세요.
|
||||||
|
</span>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 기존 데이터 중복 경고 */}
|
||||||
|
{hasDuplicateWithExisting && (
|
||||||
|
<Alert variant="destructive" className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<strong>이미 등록된 위치가 있습니다!</strong>
|
||||||
|
<ul className="mt-1 list-inside list-disc text-xs">
|
||||||
|
{duplicateErrors.map((err, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
{err.row}열: {err.existingLevels.join(", ")}단 (이미 등록됨)
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<span className="mt-1 block text-xs">
|
||||||
|
해당 열/단을 제외하거나 기존 데이터를 삭제해주세요.
|
||||||
|
</span>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 기존 데이터 로딩 중 표시 */}
|
||||||
|
{isCheckingDuplicates && (
|
||||||
|
<Alert className="mb-4">
|
||||||
|
<AlertCircle className="h-4 w-4 animate-spin" />
|
||||||
|
<AlertDescription>
|
||||||
|
기존 위치 데이터를 확인하는 중...
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 기존 데이터 존재 알림 */}
|
||||||
|
{!isCheckingDuplicates && existingLocations.length > 0 && !hasDuplicateWithExisting && (
|
||||||
|
<Alert className="mb-4 border-blue-200 bg-blue-50">
|
||||||
|
<AlertCircle className="h-4 w-4 text-blue-600" />
|
||||||
|
<AlertDescription className="text-blue-800">
|
||||||
|
해당 창고/층/구역에 <strong>{existingLocations.length}개</strong>의 위치가 이미 등록되어 있습니다.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 현재 매핑된 값 표시 */}
|
{/* 현재 매핑된 값 표시 */}
|
||||||
{(context.warehouseCode || context.warehouseName || context.floor || context.zone) && (
|
{(context.warehouseCode || context.warehouseName || context.floor || context.zone) && (
|
||||||
<div className="mb-4 flex flex-wrap gap-2 rounded-lg bg-gray-50 p-3">
|
<div className="mb-4 flex flex-wrap gap-2 rounded-lg bg-gray-50 p-3">
|
||||||
|
|
@ -548,10 +861,11 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={generatePreview}
|
onClick={generatePreview}
|
||||||
|
disabled={hasDuplicateWithExisting || hasRowOverlap || missingFields.length > 0 || isCheckingDuplicates}
|
||||||
className="h-8 gap-1"
|
className="h-8 gap-1"
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
미리보기 생성
|
{isCheckingDuplicates ? "확인 중..." : hasDuplicateWithExisting ? "중복 있음" : "미리보기 생성"}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
@ -595,15 +909,15 @@ export const RackStructureComponent: React.FC<RackStructureComponentProps> = ({
|
||||||
{previewData.map((loc, idx) => (
|
{previewData.map((loc, idx) => (
|
||||||
<TableRow key={idx}>
|
<TableRow key={idx}>
|
||||||
<TableCell className="text-center">{idx + 1}</TableCell>
|
<TableCell className="text-center">{idx + 1}</TableCell>
|
||||||
<TableCell className="font-mono">{loc.locationCode}</TableCell>
|
<TableCell className="font-mono">{loc.location_code}</TableCell>
|
||||||
<TableCell>{loc.locationName}</TableCell>
|
<TableCell>{loc.location_name}</TableCell>
|
||||||
<TableCell className="text-center">{context?.floor || "1"}</TableCell>
|
<TableCell className="text-center">{loc.floor || context?.floor || "1"}</TableCell>
|
||||||
<TableCell className="text-center">{context?.zone || "A"}</TableCell>
|
<TableCell className="text-center">{loc.zone || context?.zone || "A"}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{loc.rowNum.toString().padStart(2, "0")}
|
{loc.row_num.padStart(2, "0")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">{loc.levelNum}</TableCell>
|
<TableCell className="text-center">{loc.level_num}</TableCell>
|
||||||
<TableCell className="text-center">{loc.locationType}</TableCell>
|
<TableCell className="text-center">{loc.location_type}</TableCell>
|
||||||
<TableCell className="text-center">-</TableCell>
|
<TableCell className="text-center">-</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,40 @@ export class RackStructureRenderer extends AutoRegisteringComponentRenderer {
|
||||||
static componentDefinition = RackStructureDefinition;
|
static componentDefinition = RackStructureDefinition;
|
||||||
|
|
||||||
render(): React.ReactElement {
|
render(): React.ReactElement {
|
||||||
const { formData, isPreview, config } = this.props as any;
|
const { formData, isPreview, config, tableName, onFormDataChange } = this.props as Record<string, unknown>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RackStructureComponent
|
<RackStructureComponent
|
||||||
config={config || {}}
|
config={(config as object) || {}}
|
||||||
formData={formData} // formData 전달 (필드 매핑에서 사용)
|
formData={formData as Record<string, unknown>}
|
||||||
onChange={this.handleLocationsChange}
|
tableName={tableName as string}
|
||||||
isPreview={isPreview}
|
onChange={(locations) =>
|
||||||
|
this.handleLocationsChange(
|
||||||
|
locations,
|
||||||
|
onFormDataChange as ((fieldName: string, value: unknown) => void) | undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isPreview={isPreview as boolean}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 생성된 위치 데이터 변경 핸들러
|
* 생성된 위치 데이터 변경 핸들러
|
||||||
|
* formData에 _rackStructureLocations 키로 저장하여 저장 액션에서 감지
|
||||||
*/
|
*/
|
||||||
protected handleLocationsChange = (locations: GeneratedLocation[]) => {
|
protected handleLocationsChange = (
|
||||||
// 생성된 위치 데이터를 formData에 저장
|
locations: GeneratedLocation[],
|
||||||
|
onFormDataChange?: (fieldName: string, value: unknown) => void,
|
||||||
|
) => {
|
||||||
|
// 생성된 위치 데이터를 컴포넌트에 저장
|
||||||
this.updateComponent({ generatedLocations: locations });
|
this.updateComponent({ generatedLocations: locations });
|
||||||
|
|
||||||
|
// formData에도 저장하여 저장 액션에서 감지할 수 있도록 함
|
||||||
|
if (onFormDataChange) {
|
||||||
|
console.log("📦 [RackStructure] 미리보기 데이터를 formData에 저장:", locations.length, "개");
|
||||||
|
onFormDataChange("_rackStructureLocations", locations);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,19 @@ export interface RackStructureTemplate {
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 생성될 위치 데이터
|
// 생성될 위치 데이터 (테이블 컬럼명과 동일하게 매핑)
|
||||||
export interface GeneratedLocation {
|
export interface GeneratedLocation {
|
||||||
rowNum: number; // 열 번호
|
row_num: string; // 열 번호 (varchar)
|
||||||
levelNum: number; // 단 번호
|
level_num: string; // 단 번호 (varchar)
|
||||||
locationCode: string; // 위치 코드 (예: WH001-1A-01-1)
|
location_code: string; // 위치 코드 (예: WH001-1A-01-1)
|
||||||
locationName: string; // 위치명 (예: A구역-01열-1단)
|
location_name: string; // 위치명 (예: A구역-01열-1단)
|
||||||
locationType?: string; // 위치 유형
|
location_type?: string; // 위치 유형
|
||||||
status?: string; // 사용 여부
|
status?: string; // 사용 여부
|
||||||
// 추가 필드 (상위 폼에서 매핑된 값)
|
// 추가 필드 (상위 폼에서 매핑된 값)
|
||||||
warehouseCode?: string;
|
warehouse_id?: string; // 창고 ID/코드
|
||||||
floor?: string;
|
warehouse_name?: string; // 창고명
|
||||||
zone?: string;
|
floor?: string; // 층
|
||||||
|
zone?: string; // 구역
|
||||||
}
|
}
|
||||||
|
|
||||||
// 필드 매핑 설정 (상위 폼의 어떤 필드를 사용할지)
|
// 필드 매핑 설정 (상위 폼의 어떤 필드를 사용할지)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue