feat: implement registered items management in process work standard
- 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
This commit is contained in:
@@ -30,26 +30,68 @@ export async function getItemsWithRouting(req: AuthenticatedRequest, res: Respon
|
||||
routingTable = "item_routing_version",
|
||||
routingFkColumn = "item_code",
|
||||
search = "",
|
||||
extraColumns = "",
|
||||
filterConditions = "",
|
||||
} = req.query as Record<string, string>;
|
||||
|
||||
const searchCondition = search
|
||||
? `AND (i.${nameColumn} ILIKE $2 OR i.${codeColumn} ILIKE $2)`
|
||||
: "";
|
||||
const params: any[] = [companyCode];
|
||||
if (search) params.push(`%${search}%`);
|
||||
let paramIndex = 2;
|
||||
|
||||
// 검색 조건
|
||||
let searchCondition = "";
|
||||
if (search) {
|
||||
searchCondition = `AND (i.${nameColumn} ILIKE $${paramIndex} OR i.${codeColumn} ILIKE $${paramIndex})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 추가 컬럼 SELECT
|
||||
const extraColumnNames: string[] = extraColumns
|
||||
? extraColumns.split(",").map((c: string) => c.trim()).filter(Boolean)
|
||||
: [];
|
||||
const extraSelect = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
const extraGroupBy = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
|
||||
// 사전 필터 조건
|
||||
let filterWhere = "";
|
||||
if (filterConditions) {
|
||||
try {
|
||||
const filters = JSON.parse(filterConditions) as Array<{
|
||||
column: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}>;
|
||||
for (const f of filters) {
|
||||
if (!f.column || !f.value) continue;
|
||||
if (f.operator === "equals") {
|
||||
filterWhere += ` AND i.${f.column} = $${paramIndex}`;
|
||||
params.push(f.value);
|
||||
} else if (f.operator === "contains") {
|
||||
filterWhere += ` AND i.${f.column} ILIKE $${paramIndex}`;
|
||||
params.push(`%${f.value}%`);
|
||||
} else if (f.operator === "not_equals") {
|
||||
filterWhere += ` AND i.${f.column} != $${paramIndex}`;
|
||||
params.push(f.value);
|
||||
}
|
||||
paramIndex++;
|
||||
}
|
||||
} catch { /* 파싱 실패 시 무시 */ }
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
i.id,
|
||||
i.${nameColumn} AS item_name,
|
||||
i.${codeColumn} AS item_code,
|
||||
i.${codeColumn} AS item_code
|
||||
${extraSelect ? ", " + extraSelect : ""},
|
||||
COUNT(rv.id) AS routing_count
|
||||
FROM ${tableName} i
|
||||
LEFT JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn}
|
||||
AND rv.company_code = i.company_code
|
||||
WHERE i.company_code = $1
|
||||
${searchCondition}
|
||||
GROUP BY i.id, i.${nameColumn}, i.${codeColumn}, i.created_date
|
||||
${filterWhere}
|
||||
GROUP BY i.id, i.${nameColumn}, i.${codeColumn}${extraGroupBy ? ", " + extraGroupBy : ""}, i.created_date
|
||||
ORDER BY i.created_date DESC NULLS LAST
|
||||
`;
|
||||
|
||||
@@ -711,3 +753,184 @@ export async function saveAll(req: AuthenticatedRequest, res: Response) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 등록 품목 관리 (item_routing_registered)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 화면별 등록된 품목 목록 조회
|
||||
*/
|
||||
export async function getRegisteredItems(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { screenCode } = req.params;
|
||||
const {
|
||||
tableName = "item_info",
|
||||
nameColumn = "item_name",
|
||||
codeColumn = "item_number",
|
||||
routingTable = "item_routing_version",
|
||||
routingFkColumn = "item_code",
|
||||
search = "",
|
||||
extraColumns = "",
|
||||
} = req.query as Record<string, string>;
|
||||
|
||||
const params: any[] = [companyCode, screenCode];
|
||||
let paramIndex = 3;
|
||||
|
||||
let searchCondition = "";
|
||||
if (search) {
|
||||
searchCondition = `AND (i.${nameColumn} ILIKE $${paramIndex} OR i.${codeColumn} ILIKE $${paramIndex})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const extraColumnNames: string[] = extraColumns
|
||||
? extraColumns.split(",").map((c: string) => c.trim()).filter(Boolean)
|
||||
: [];
|
||||
const extraSelect = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
const extraGroupBy = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
irr.id AS registered_id,
|
||||
irr.sort_order,
|
||||
i.id,
|
||||
i.${nameColumn} AS item_name,
|
||||
i.${codeColumn} AS item_code
|
||||
${extraSelect ? ", " + extraSelect : ""},
|
||||
COUNT(rv.id) AS routing_count
|
||||
FROM item_routing_registered irr
|
||||
JOIN ${tableName} i ON irr.item_id = i.id
|
||||
AND i.company_code = irr.company_code
|
||||
LEFT JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn}
|
||||
AND rv.company_code = i.company_code
|
||||
WHERE irr.company_code = $1
|
||||
AND irr.screen_code = $2
|
||||
${searchCondition}
|
||||
GROUP BY irr.id, irr.sort_order, i.id, i.${nameColumn}, i.${codeColumn}${extraGroupBy ? ", " + extraGroupBy : ""}
|
||||
ORDER BY CAST(irr.sort_order AS int) ASC, irr.created_date ASC
|
||||
`;
|
||||
|
||||
const result = await getPool().query(query, params);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("등록 품목 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 등록 (화면에 품목 추가)
|
||||
*/
|
||||
export async function registerItem(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { screenCode, itemId, itemCode } = req.body;
|
||||
if (!screenCode || !itemId) {
|
||||
return res.status(400).json({ success: false, message: "screenCode, itemId 필수" });
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO item_routing_registered (screen_code, item_id, item_code, company_code, writer)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (screen_code, item_id, company_code) DO NOTHING
|
||||
RETURNING *
|
||||
`;
|
||||
const result = await getPool().query(query, [
|
||||
screenCode, itemId, itemCode || null, companyCode, req.user?.userId || null,
|
||||
]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.json({ success: true, message: "이미 등록된 품목입니다", data: null });
|
||||
}
|
||||
|
||||
logger.info("품목 등록", { companyCode, screenCode, itemId });
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("품목 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 품목 일괄 등록
|
||||
*/
|
||||
export async function registerItemsBatch(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { screenCode, items } = req.body;
|
||||
if (!screenCode || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "screenCode, items[] 필수" });
|
||||
}
|
||||
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const inserted: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = await client.query(
|
||||
`INSERT INTO item_routing_registered (screen_code, item_id, item_code, company_code, writer)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (screen_code, item_id, company_code) DO NOTHING
|
||||
RETURNING *`,
|
||||
[screenCode, item.itemId, item.itemCode || null, companyCode, req.user?.userId || null]
|
||||
);
|
||||
if (result.rows[0]) inserted.push(result.rows[0]);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("품목 일괄 등록", { companyCode, screenCode, count: inserted.length });
|
||||
return res.json({ success: true, data: inserted });
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("품목 일괄 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록 품목 제거
|
||||
*/
|
||||
export async function unregisterItem(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const result = await getPool().query(
|
||||
`DELETE FROM item_routing_registered WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다" });
|
||||
}
|
||||
|
||||
logger.info("등록 품목 제거", { companyCode, id });
|
||||
return res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
logger.error("등록 품목 제거 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user