- Added new endpoints for managing registered items, including retrieval, registration, and batch registration. - Enhanced the existing processWorkStandardController to support filtering and additional columns in item queries. - Updated the processWorkStandardRoutes to include routes for registered items management. - Introduced a new documentation file detailing the design and structure of the POP 작업진행 관리 system. These changes aim to improve the management of registered items within the process work standard, enhancing usability and functionality. Made-with: Cursor
441 lines
15 KiB
TypeScript
441 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useMemo, useRef } from "react";
|
|
import { apiClient } from "@/lib/api/client";
|
|
import { ItemRoutingConfig, ItemData, RoutingVersionData, RoutingDetailData, ColumnDef } from "../types";
|
|
import { defaultConfig } from "../config";
|
|
|
|
const API_BASE = "/process-work-standard";
|
|
|
|
/** 표시 컬럼 목록에서 기본(item_name, item_code) 외 추가 컬럼만 추출 */
|
|
function getExtraColumnNames(columns?: ColumnDef[]): string {
|
|
if (!columns || columns.length === 0) return "";
|
|
return columns
|
|
.map((c) => c.name)
|
|
.filter((n) => n && n !== "item_name" && n !== "item_code")
|
|
.join(",");
|
|
}
|
|
|
|
export function useItemRouting(configPartial: Partial<ItemRoutingConfig>) {
|
|
const configKey = useMemo(
|
|
() => JSON.stringify(configPartial),
|
|
[configPartial]
|
|
);
|
|
|
|
const config: ItemRoutingConfig = useMemo(() => ({
|
|
...defaultConfig,
|
|
...configPartial,
|
|
dataSource: { ...defaultConfig.dataSource, ...configPartial?.dataSource },
|
|
modals: { ...defaultConfig.modals, ...configPartial?.modals },
|
|
processColumns: configPartial?.processColumns?.length
|
|
? configPartial.processColumns
|
|
: defaultConfig.processColumns,
|
|
}), [configKey]);
|
|
|
|
const configRef = useRef(config);
|
|
configRef.current = config;
|
|
|
|
const [items, setItems] = useState<ItemData[]>([]);
|
|
const [allItems, setAllItems] = useState<ItemData[]>([]);
|
|
const [versions, setVersions] = useState<RoutingVersionData[]>([]);
|
|
const [details, setDetails] = useState<RoutingDetailData[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [selectedItemCode, setSelectedItemCode] = useState<string | null>(null);
|
|
const [selectedItemName, setSelectedItemName] = useState<string | null>(null);
|
|
const [selectedVersionId, setSelectedVersionId] = useState<string | null>(null);
|
|
|
|
const isRegisteredMode = config.itemListMode === "registered";
|
|
|
|
/** API 기본 파라미터 생성 */
|
|
const buildBaseParams = useCallback((search?: string, columns?: ColumnDef[]) => {
|
|
const ds = configRef.current.dataSource;
|
|
const extra = getExtraColumnNames(columns);
|
|
const filters = configRef.current.itemFilterConditions;
|
|
const params: Record<string, string> = {
|
|
tableName: ds.itemTable,
|
|
nameColumn: ds.itemNameColumn,
|
|
codeColumn: ds.itemCodeColumn,
|
|
routingTable: ds.routingVersionTable,
|
|
routingFkColumn: ds.routingVersionFkColumn,
|
|
};
|
|
if (search) params.search = search;
|
|
if (extra) params.extraColumns = extra;
|
|
if (filters && filters.length > 0) {
|
|
params.filterConditions = JSON.stringify(filters);
|
|
}
|
|
return new URLSearchParams(params);
|
|
}, []);
|
|
|
|
// ────────────────────────────────────────
|
|
// 품목 목록 조회 (all 모드)
|
|
// ────────────────────────────────────────
|
|
const fetchItems = useCallback(
|
|
async (search?: string) => {
|
|
try {
|
|
setLoading(true);
|
|
const cols = configRef.current.itemDisplayColumns;
|
|
const params = buildBaseParams(search, cols);
|
|
const res = await apiClient.get(`${API_BASE}/items?${params}`);
|
|
if (res.data?.success) {
|
|
const data = res.data.data || [];
|
|
if (configRef.current.itemListMode !== "registered") {
|
|
setItems(data);
|
|
}
|
|
return data;
|
|
}
|
|
} catch (err) {
|
|
console.error("품목 조회 실패", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
return [];
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[configKey, buildBaseParams]
|
|
);
|
|
|
|
// ────────────────────────────────────────
|
|
// 등록 품목 조회 (registered 모드)
|
|
// ────────────────────────────────────────
|
|
const fetchRegisteredItems = useCallback(
|
|
async (search?: string) => {
|
|
const screenCode = configRef.current.screenCode;
|
|
if (!screenCode) {
|
|
console.warn("screenCode가 설정되지 않았습니다");
|
|
setItems([]);
|
|
return;
|
|
}
|
|
try {
|
|
setLoading(true);
|
|
const ds = configRef.current.dataSource;
|
|
const cols = configRef.current.itemDisplayColumns;
|
|
const extra = getExtraColumnNames(cols);
|
|
const params = new URLSearchParams({
|
|
tableName: ds.itemTable,
|
|
nameColumn: ds.itemNameColumn,
|
|
codeColumn: ds.itemCodeColumn,
|
|
routingTable: ds.routingVersionTable,
|
|
routingFkColumn: ds.routingVersionFkColumn,
|
|
...(search ? { search } : {}),
|
|
...(extra ? { extraColumns: extra } : {}),
|
|
});
|
|
const res = await apiClient.get(
|
|
`${API_BASE}/registered-items/${encodeURIComponent(screenCode)}?${params}`
|
|
);
|
|
if (res.data?.success) {
|
|
setItems(res.data.data || []);
|
|
}
|
|
} catch (err) {
|
|
console.error("등록 품목 조회 실패", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[configKey]
|
|
);
|
|
|
|
// ────────────────────────────────────────
|
|
// 전체 품목 조회 (등록 팝업용 - 필터+추가컬럼 적용)
|
|
// ────────────────────────────────────────
|
|
const fetchAllItems = useCallback(
|
|
async (search?: string) => {
|
|
try {
|
|
const cols = configRef.current.modalDisplayColumns;
|
|
const params = buildBaseParams(search, cols);
|
|
const res = await apiClient.get(`${API_BASE}/items?${params}`);
|
|
if (res.data?.success) {
|
|
setAllItems(res.data.data || []);
|
|
}
|
|
} catch (err) {
|
|
console.error("전체 품목 조회 실패", err);
|
|
}
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[configKey, buildBaseParams]
|
|
);
|
|
|
|
// ────────────────────────────────────────
|
|
// 품목 등록/제거 (registered 모드)
|
|
// ────────────────────────────────────────
|
|
const registerItem = useCallback(
|
|
async (itemId: string, itemCode: string) => {
|
|
const screenCode = configRef.current.screenCode;
|
|
if (!screenCode) return false;
|
|
try {
|
|
const res = await apiClient.post(`${API_BASE}/registered-items`, {
|
|
screenCode,
|
|
itemId,
|
|
itemCode,
|
|
});
|
|
if (res.data?.success) {
|
|
await fetchRegisteredItems();
|
|
return true;
|
|
}
|
|
} catch (err) {
|
|
console.error("품목 등록 실패", err);
|
|
}
|
|
return false;
|
|
},
|
|
[fetchRegisteredItems]
|
|
);
|
|
|
|
const registerItemsBatch = useCallback(
|
|
async (itemList: { itemId: string; itemCode: string }[]) => {
|
|
const screenCode = configRef.current.screenCode;
|
|
if (!screenCode) return false;
|
|
try {
|
|
const res = await apiClient.post(`${API_BASE}/registered-items/batch`, {
|
|
screenCode,
|
|
items: itemList,
|
|
});
|
|
if (res.data?.success) {
|
|
await fetchRegisteredItems();
|
|
return true;
|
|
}
|
|
} catch (err) {
|
|
console.error("품목 일괄 등록 실패", err);
|
|
}
|
|
return false;
|
|
},
|
|
[fetchRegisteredItems]
|
|
);
|
|
|
|
const unregisterItem = useCallback(
|
|
async (registeredId: string) => {
|
|
try {
|
|
const res = await apiClient.delete(`${API_BASE}/registered-items/${registeredId}`);
|
|
if (res.data?.success) {
|
|
if (selectedItemCode) {
|
|
const removedItem = items.find((i) => i.registered_id === registeredId);
|
|
if (removedItem) {
|
|
const removedCode = removedItem.item_code || removedItem[configRef.current.dataSource.itemCodeColumn];
|
|
if (selectedItemCode === removedCode) {
|
|
setSelectedItemCode(null);
|
|
setSelectedItemName(null);
|
|
setSelectedVersionId(null);
|
|
setVersions([]);
|
|
setDetails([]);
|
|
}
|
|
}
|
|
}
|
|
await fetchRegisteredItems();
|
|
return true;
|
|
}
|
|
} catch (err) {
|
|
console.error("등록 품목 제거 실패", err);
|
|
}
|
|
return false;
|
|
},
|
|
[selectedItemCode, items, fetchRegisteredItems]
|
|
);
|
|
|
|
// ────────────────────────────────────────
|
|
// 라우팅 버전/공정 관련 (기존 동일)
|
|
// ────────────────────────────────────────
|
|
const fetchVersions = useCallback(
|
|
async (itemCode: string) => {
|
|
try {
|
|
const ds = configRef.current.dataSource;
|
|
const params = new URLSearchParams({
|
|
routingVersionTable: ds.routingVersionTable,
|
|
routingDetailTable: ds.routingDetailTable,
|
|
routingFkColumn: ds.routingVersionFkColumn,
|
|
processTable: ds.processTable,
|
|
processNameColumn: ds.processNameColumn,
|
|
processCodeColumn: ds.processCodeColumn,
|
|
});
|
|
const res = await apiClient.get(
|
|
`${API_BASE}/items/${encodeURIComponent(itemCode)}/routings?${params}`
|
|
);
|
|
if (res.data?.success) {
|
|
const routingData = res.data.data || [];
|
|
setVersions(routingData);
|
|
return routingData;
|
|
}
|
|
} catch (err) {
|
|
console.error("라우팅 버전 조회 실패", err);
|
|
}
|
|
return [];
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[configKey]
|
|
);
|
|
|
|
const fetchDetails = useCallback(
|
|
async (versionId: string) => {
|
|
try {
|
|
setLoading(true);
|
|
const ds = configRef.current.dataSource;
|
|
const searchConditions = {
|
|
[ds.routingDetailFkColumn]: { value: versionId, operator: "equals" },
|
|
};
|
|
const params = new URLSearchParams({
|
|
page: "1",
|
|
size: "1000",
|
|
search: JSON.stringify(searchConditions),
|
|
sortBy: "seq_no",
|
|
sortOrder: "ASC",
|
|
enableEntityJoin: "true",
|
|
});
|
|
const res = await apiClient.get(
|
|
`/table-management/tables/${ds.routingDetailTable}/data-with-joins?${params}`
|
|
);
|
|
if (res.data?.success) {
|
|
const result = res.data.data;
|
|
setDetails(Array.isArray(result) ? result : result?.data || []);
|
|
}
|
|
} catch (err) {
|
|
console.error("공정 상세 조회 실패", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[configKey]
|
|
);
|
|
|
|
const selectItem = useCallback(
|
|
async (itemCode: string, itemName: string) => {
|
|
setSelectedItemCode(itemCode);
|
|
setSelectedItemName(itemName);
|
|
setSelectedVersionId(null);
|
|
setDetails([]);
|
|
const versionList = await fetchVersions(itemCode);
|
|
if (versionList.length > 0) {
|
|
const defaultVersion = versionList.find((v: RoutingVersionData) => v.is_default);
|
|
const targetVersion = defaultVersion || (configRef.current.autoSelectFirstVersion ? versionList[0] : null);
|
|
if (targetVersion) {
|
|
setSelectedVersionId(targetVersion.id);
|
|
await fetchDetails(targetVersion.id);
|
|
}
|
|
}
|
|
},
|
|
[fetchVersions, fetchDetails]
|
|
);
|
|
|
|
const selectVersion = useCallback(
|
|
async (versionId: string) => {
|
|
setSelectedVersionId(versionId);
|
|
await fetchDetails(versionId);
|
|
},
|
|
[fetchDetails]
|
|
);
|
|
|
|
const refreshVersions = useCallback(async () => {
|
|
if (selectedItemCode) {
|
|
const versionList = await fetchVersions(selectedItemCode);
|
|
if (selectedVersionId) {
|
|
await fetchDetails(selectedVersionId);
|
|
} else if (versionList.length > 0) {
|
|
const lastVersion = versionList[versionList.length - 1];
|
|
setSelectedVersionId(lastVersion.id);
|
|
await fetchDetails(lastVersion.id);
|
|
}
|
|
}
|
|
}, [selectedItemCode, selectedVersionId, fetchVersions, fetchDetails]);
|
|
|
|
const refreshDetails = useCallback(async () => {
|
|
if (selectedVersionId) {
|
|
await fetchDetails(selectedVersionId);
|
|
}
|
|
}, [selectedVersionId, fetchDetails]);
|
|
|
|
const deleteDetail = useCallback(
|
|
async (detailId: string) => {
|
|
try {
|
|
const ds = configRef.current.dataSource;
|
|
const res = await apiClient.delete(
|
|
`/table-management/tables/${ds.routingDetailTable}/delete`,
|
|
{ data: [{ id: detailId }] }
|
|
);
|
|
if (res.data?.success) { await refreshDetails(); return true; }
|
|
} catch (err) { console.error("공정 삭제 실패", err); }
|
|
return false;
|
|
},
|
|
[refreshDetails]
|
|
);
|
|
|
|
const deleteVersion = useCallback(
|
|
async (versionId: string) => {
|
|
try {
|
|
const ds = configRef.current.dataSource;
|
|
const res = await apiClient.delete(
|
|
`/table-management/tables/${ds.routingVersionTable}/delete`,
|
|
{ data: [{ id: versionId }] }
|
|
);
|
|
if (res.data?.success) {
|
|
if (selectedVersionId === versionId) { setSelectedVersionId(null); setDetails([]); }
|
|
await refreshVersions();
|
|
return true;
|
|
}
|
|
} catch (err) { console.error("버전 삭제 실패", err); }
|
|
return false;
|
|
},
|
|
[selectedVersionId, refreshVersions]
|
|
);
|
|
|
|
const setDefaultVersion = useCallback(
|
|
async (versionId: string) => {
|
|
try {
|
|
const ds = configRef.current.dataSource;
|
|
const res = await apiClient.put(`${API_BASE}/versions/${versionId}/set-default`, {
|
|
routingVersionTable: ds.routingVersionTable,
|
|
routingFkColumn: ds.routingVersionFkColumn,
|
|
});
|
|
if (res.data?.success) {
|
|
if (selectedItemCode) await fetchVersions(selectedItemCode);
|
|
return true;
|
|
}
|
|
} catch (err) { console.error("기본 버전 설정 실패", err); }
|
|
return false;
|
|
},
|
|
[selectedItemCode, fetchVersions]
|
|
);
|
|
|
|
const unsetDefaultVersion = useCallback(
|
|
async (versionId: string) => {
|
|
try {
|
|
const ds = configRef.current.dataSource;
|
|
const res = await apiClient.put(`${API_BASE}/versions/${versionId}/unset-default`, {
|
|
routingVersionTable: ds.routingVersionTable,
|
|
});
|
|
if (res.data?.success) {
|
|
if (selectedItemCode) await fetchVersions(selectedItemCode);
|
|
return true;
|
|
}
|
|
} catch (err) { console.error("기본 버전 해제 실패", err); }
|
|
return false;
|
|
},
|
|
[selectedItemCode, fetchVersions]
|
|
);
|
|
|
|
return {
|
|
config,
|
|
items,
|
|
allItems,
|
|
versions,
|
|
details,
|
|
loading,
|
|
selectedItemCode,
|
|
selectedItemName,
|
|
selectedVersionId,
|
|
isRegisteredMode,
|
|
fetchItems,
|
|
fetchRegisteredItems,
|
|
fetchAllItems,
|
|
registerItem,
|
|
registerItemsBatch,
|
|
unregisterItem,
|
|
selectItem,
|
|
selectVersion,
|
|
refreshVersions,
|
|
refreshDetails,
|
|
deleteDetail,
|
|
deleteVersion,
|
|
setDefaultVersion,
|
|
unsetDefaultVersion,
|
|
};
|
|
}
|