Enhance backend controllers, frontend pages, and V2 components

- Fix department, receiving, shippingOrder, shippingPlan controllers
- Update admin pages (company management, disk usage)
- Improve sales/logistics pages (order, shipping, outbound, receiving)
- Enhance V2 components (file-upload, split-panel-layout, table-list)
- Add SmartSelect common component
- Update DataGrid, FullscreenDialog common components
- Add gitignore rules for personal pipeline tools

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kmh
2026-03-30 11:52:03 +09:00
parent 348da95823
commit b97ca1a1c5
23 changed files with 1012 additions and 365 deletions

View File

@@ -33,6 +33,7 @@ import { formatNumber as centralFormatNumber } from "@/lib/formatting";
import { useToast } from "@/hooks/use-toast";
import { tableTypeApi } from "@/lib/api/screen";
import { apiClient, getFullImageUrl } from "@/lib/api/client";
import { codeCache } from "@/lib/caching/codeCache";
import { getFilePreviewUrl } from "@/lib/api/file";
import {
Dialog,
@@ -402,6 +403,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [isLoadingRight, setIsLoadingRight] = useState(false);
const [rightTableColumns, setRightTableColumns] = useState<any[]>([]); // 우측 테이블 컬럼 정보
const [columnInputTypes, setColumnInputTypes] = useState<Record<string, string>>({});
const [columnCodeCategories, setColumnCodeCategories] = useState<Record<string, string>>({}); // columnName → codeCategory
const [expandedItems, setExpandedItems] = useState<Set<any>>(new Set()); // 펼쳐진 항목들
// 🆕 페이징 상태
@@ -1124,6 +1126,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return <SplitPanelCellImage value={String(value)} />;
}
// code 타입: code_value → code_name 변환
if (colInputType === "code") {
const codeCategory = columnCodeCategories[columnName];
if (codeCategory && value) {
try {
const syncResult = codeCache.getCodeSync(codeCategory);
if (syncResult && Array.isArray(syncResult)) {
const foundCode = syncResult.find(
(item: any) => String(item.code_value).toUpperCase() === String(value).toUpperCase(),
);
if (foundCode) {
return foundCode.code_name;
}
} else {
// 캐시 미스: 비동기 로딩 트리거
codeCache.getCodeAsync(codeCategory).catch(() => {});
}
} catch {
// ignore
}
}
}
// 🆕 날짜 포맷 적용
if (format?.type === "date" || format?.dateFormat) {
return formatDateValue(value, format?.dateFormat || "YYYY-MM-DD");
@@ -1156,20 +1181,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (mapping && mapping[strValue]) {
const categoryData = mapping[strValue];
const displayLabel = categoryData.label || strValue;
const displayColor = categoryData.color || "#64748b";
return (
<Badge
style={{
backgroundColor: displayColor,
borderColor: displayColor,
}}
className="text-white"
>
{displayLabel}
</Badge>
);
return categoryData.label || strValue;
}
// 전역 폴백: 컬럼명으로 매핑을 못 찾았을 때, 전체 매핑에서 값 검색
@@ -1178,19 +1190,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const m = categoryMappings[key];
if (m && m[strValue]) {
const categoryData = m[strValue];
const displayLabel = categoryData.label || strValue;
const displayColor = categoryData.color || "#64748b";
return (
<Badge
style={{
backgroundColor: displayColor,
borderColor: displayColor,
}}
className="text-white"
>
{displayLabel}
</Badge>
);
return categoryData.label || strValue;
}
}
}
@@ -1216,7 +1216,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 일반 값
return String(value);
},
[formatDateValue, formatNumberValue, columnInputTypes],
[formatDateValue, formatNumberValue, columnInputTypes, columnCodeCategories],
);
// 🆕 패널 config의 columns에서 additionalJoinColumns 추출하는 헬퍼
@@ -2218,6 +2218,43 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
loadLeftColumnLabels();
}, [componentConfig.leftPanel?.tableName, isDesignMode]);
// 왼쪽 테이블 inputTypes + codeCategory 로드
useEffect(() => {
const loadLeftColumnInputTypes = async () => {
const leftTableName = componentConfig.leftPanel?.tableName;
if (!leftTableName || isDesignMode) return;
try {
const columnsResponse = await tableTypeApi.getColumns(leftTableName);
const inputTypes: Record<string, string> = {};
const codeCategories: Record<string, string> = {};
columnsResponse.forEach((col: any) => {
const colName = col.columnName || col.column_name;
if (colName) {
inputTypes[colName] = col.inputType || "text";
if (col.codeCategory) {
codeCategories[colName] = col.codeCategory;
}
}
});
setColumnInputTypes((prev) => ({ ...prev, ...inputTypes }));
setColumnCodeCategories((prev) => ({ ...prev, ...codeCategories }));
} catch {
// ignore
}
};
loadLeftColumnInputTypes();
}, [componentConfig.leftPanel?.tableName, isDesignMode]);
// codeCategory 프리로딩 (캐시 미스 방지)
useEffect(() => {
const categories = Object.values(columnCodeCategories).filter(Boolean);
if (categories.length > 0) {
codeCache.preloadCodes([...new Set(categories)]).catch(() => {});
}
}, [columnCodeCategories]);
// 우측 테이블 컬럼 정보 로드
useEffect(() => {
const loadRightTableColumns = async () => {
@@ -2247,20 +2284,25 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
});
const inputTypes: Record<string, string> = {};
const codeCategories: Record<string, string> = {};
for (const tbl of tablesToLoad) {
try {
const inputTypesResponse = await tableTypeApi.getColumnInputTypes(tbl);
inputTypesResponse.forEach((col: any) => {
const tblColumnsResponse = await tableTypeApi.getColumns(tbl);
tblColumnsResponse.forEach((col: any) => {
const colName = col.columnName || col.column_name;
if (colName) {
inputTypes[colName] = col.inputType || "text";
if (col.codeCategory) {
codeCategories[colName] = col.codeCategory;
}
}
});
} catch {
// ignore
}
}
setColumnInputTypes(inputTypes);
setColumnInputTypes((prev) => ({ ...prev, ...inputTypes }));
setColumnCodeCategories((prev) => ({ ...prev, ...codeCategories }));
} catch (error) {
console.error("우측 테이블 컬럼 정보 로드 실패:", error);
}
@@ -2304,12 +2346,18 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (response.data.success && response.data.data) {
const valueMap: Record<string, { label: string; color?: string }> = {};
response.data.data.forEach((item: any) => {
valueMap[item.value_code || item.valueCode] = {
label: item.value_label || item.valueLabel,
color: item.color,
};
});
const flattenCategories = (items: any[]) => {
items.forEach((item: any) => {
valueMap[item.value_code || item.valueCode] = {
label: item.value_label || item.valueLabel,
color: item.color,
};
if (item.children && item.children.length > 0) {
flattenCategories(item.children);
}
});
};
flattenCategories(response.data.data);
// 조인된 테이블은 "테이블명.컬럼명" 형태로도 저장
const mappingKey = tableName === leftTableName ? columnName : `${tableName}.${columnName}`;
@@ -2391,19 +2439,24 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (response.data.success && response.data.data) {
const valueMap: Record<string, { label: string; color?: string }> = {};
response.data.data.forEach((item: any) => {
valueMap[item.value_code || item.valueCode] = {
label: item.value_label || item.valueLabel,
color: item.color,
};
});
const flattenCategories = (items: any[]) => {
items.forEach((item: any) => {
valueMap[item.value_code || item.valueCode] = {
label: item.value_label || item.valueLabel,
color: item.color,
};
if (item.children && item.children.length > 0) {
flattenCategories(item.children);
}
});
};
flattenCategories(response.data.data);
// 조인된 테이블의 경우 "테이블명.컬럼명" 형태로 저장
const mappingKey = tableName === rightTableName ? columnName : `${tableName}.${columnName}`;
mappings[mappingKey] = valueMap;
// 🆕 컬럼명만으로도 접근할 수 있도록 추가 저장 (모든 테이블)
// 기존 매핑이 있으면 병합, 없으면 새로 생성
// 컬럼명만으로도 접근할 수 있도록 추가 저장 (모든 테이블)
mappings[columnName] = { ...(mappings[columnName] || {}), ...valueMap };
}
} catch (error) {