feat: Refactor work process and item inspection logic

- Updated SQL queries in `popProductionController` to separate work order process and result handling, ensuring batch_id is now managed in the work_order_process_result table.
- Enhanced `workInstructionController` to include id generation for work items and details, preventing NULL values during insertion.
- Implemented case-insensitive search functionality across various services, improving data retrieval accuracy.
- Added sorting functionality in the item inspection page, allowing users to sort by different columns with visual indicators for sort direction.

This refactor aims to improve data integrity and user experience across the production and inspection workflows.
This commit is contained in:
kjs
2026-04-23 17:36:04 +09:00
parent cfa6ee0869
commit c618283306
35 changed files with 9655 additions and 2868 deletions

View File

@@ -173,18 +173,21 @@ async function generateWorkProcessesForInstruction(
total_checklists: number;
} | null> {
// 중복 호출 방지: 이미 생성된 공정이 있는지 확인 (batch_id 기준 분리)
// ※ TASK:ERP-011 스키마 분리 반영 — batch_id는 실적 테이블(work_order_process_result)로 이동
if (batchId) {
// 다중 품목: 같은 wo_id + 같은 batch_id에 대해 이미 공정이 있으면 skip
// 기준정보(work_order_process) ⟷ 실적(work_order_process_result) JOIN으로 확인
const existCheck = await client.query(
`SELECT COUNT(*) as cnt FROM work_order_process
WHERE wo_id = $1 AND company_code = $2 AND batch_id = $3`,
`SELECT COUNT(*) as cnt FROM work_order_process p
JOIN work_order_process_result r ON r.wop_id = p.id
WHERE p.wo_id = $1 AND p.company_code = $2 AND r.batch_id = $3`,
[workInstructionId, companyCode, batchId],
);
if (parseInt(existCheck.rows[0].cnt, 10) > 0) {
return null; // 이미 존재
}
} else {
// 기존 동작: batch_id 없으면 wo_id 전체로 체크
// 기존 동작: batch_id 없으면 wo_id 전체로 체크 (기준정보 테이블만 조회)
const existCheck = await client.query(
`SELECT COUNT(*) as cnt FROM work_order_process
WHERE wo_id = $1 AND company_code = $2`,
@@ -221,13 +224,14 @@ async function generateWorkProcessesForInstruction(
let totalChecklists = 0;
for (const rd of routingDetails.rows) {
// 2. work_order_process INSERT (batch_id 포함)
// 2-A. work_order_process INSERT — 기준정보(TASK:ERP-011 분리 후)
// status/batch_id는 실적 테이블로 이동했으므로 여기에서는 제외
const wopResult = await client.query(
`INSERT INTO work_order_process (
id, company_code, wo_id, seq_no, process_code, process_name,
is_required, is_fixed_order, standard_time, plan_qty,
status, routing_detail_id, batch_id, writer
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
routing_detail_id, writer
) VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id`,
[
companyCode,
@@ -239,16 +243,26 @@ async function generateWorkProcessesForInstruction(
rd.is_fixed_order,
rd.standard_time,
planQty || null,
parseInt(rd.seq_no, 10) === 1 || rd.is_fixed_order === "Y"
? "acceptable"
: "waiting",
rd.id,
batchId || null,
userId,
],
);
const wopId = wopResult.rows[0].id;
// 2-B. work_order_process_result INSERT — 최초 실적 레코드 (seq=1)
// 동일 client로 실행 → 트랜잭션 보호 유지
// id는 wopId와 동일하게 부여 (초기 이관 정책 및 copyChecklistToSplit 호환 목적)
const initialStatus =
parseInt(rd.seq_no, 10) === 1 || rd.is_fixed_order === "Y"
? "acceptable"
: "waiting";
await client.query(
`INSERT INTO work_order_process_result (
id, wop_id, seq, company_code, status, batch_id, writer
) VALUES ($1, $2, 1, $3, $4, $5, $6)`,
[wopId, wopId, companyCode, initialStatus, batchId || null, userId],
);
// 3. process_work_result INSERT (공통 함수로 체크리스트 복사)
const checklistCount = await copyChecklistToSplit(
client,

View File

@@ -757,8 +757,8 @@ export async function copyWorkStandard(req: AuthenticatedRequest, res: Response)
for (const origItem of origItems.rows) {
const newItemResult = await client.query(
`INSERT INTO wi_process_work_item (company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
`INSERT INTO wi_process_work_item (id, company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
[companyCode, wiNo, rd.id, origItem.work_phase, origItem.title, origItem.is_required, origItem.sort_order, origItem.description, origItem.id, userId]
);
const newItemId = newItemResult.rows[0].id;
@@ -771,8 +771,8 @@ export async function copyWorkStandard(req: AuthenticatedRequest, res: Response)
for (const origDetail of origDetails.rows) {
await client.query(
`INSERT INTO wi_process_work_item_detail (company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
`INSERT INTO wi_process_work_item_detail (id, company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer)
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
[companyCode, newItemId, origDetail.detail_type, origDetail.content, origDetail.is_required, origDetail.sort_order, origDetail.remark, origDetail.inspection_code, origDetail.inspection_method, origDetail.unit, origDetail.lower_limit, origDetail.upper_limit, origDetail.duration_minutes, origDetail.input_type, origDetail.lookup_target, origDetail.display_fields, origDetail.process_inspection_apply || null, origDetail.equip_inspection_apply || null, userId]
);
}
@@ -824,10 +824,13 @@ export async function saveWorkStandard(req: AuthenticatedRequest, res: Response)
);
// 새 데이터 삽입
// NOTE: wi_process_work_item / wi_process_work_item_detail.id 컬럼에 DEFAULT(gen_random_uuid()) 누락
// → id를 명시하지 않으면 NULL 저장되어 재조회 시 wi_work_item_id 매칭 실패(0건 반환)로 이어짐.
// 원본 테이블(process_work_item) DEFAULT와 동기되지 않은 스키마 이슈. 여기서 명시 바인딩으로 회피.
for (const wi of workItems) {
const wiResult = await client.query(
`INSERT INTO wi_process_work_item (company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
`INSERT INTO wi_process_work_item (id, company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
[companyCode, wiNo, routingDetailId, wi.work_phase, wi.title, wi.is_required, wi.sort_order, wi.description || null, wi.source_work_item_id || null, userId]
);
const newId = wiResult.rows[0].id;
@@ -835,8 +838,8 @@ export async function saveWorkStandard(req: AuthenticatedRequest, res: Response)
if (wi.details && Array.isArray(wi.details)) {
for (const d of wi.details) {
await client.query(
`INSERT INTO wi_process_work_item_detail (company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
`INSERT INTO wi_process_work_item_detail (id, company_code, wi_work_item_id, detail_type, content, is_required, sort_order, remark, inspection_code, inspection_method, unit, lower_limit, upper_limit, duration_minutes, input_type, lookup_target, display_fields, process_inspection_apply, equip_inspection_apply, writer)
VALUES (gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
[companyCode, newId, d.detail_type, d.content, d.is_required, d.sort_order, d.remark || null, d.inspection_code || null, d.inspection_method || null, d.unit || null, d.lower_limit || null, d.upper_limit || null, d.duration_minutes || null, d.input_type || null, d.lookup_target || null, d.display_fields || null, d.process_inspection_apply || null, d.equip_inspection_apply || null, userId]
);
}

View File

@@ -415,13 +415,6 @@ export class AdminService {
let queryParams: any[] = [userLang];
let paramIndex = 2;
// [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시
// TODO: 권한 체크 다시 활성화 필요
logger.debug(`getUserMenuList 권한 그룹 체크 스킵 - ${userId}(${userType})`);
authFilter = "";
unionFilter = "";
/* [원본 코드 - getUserMenuList 권한 그룹 체크]
if (userType === "SUPER_ADMIN") {
// SUPER_ADMIN: 권한 그룹 체크 없이 해당 회사의 모든 메뉴 표시
logger.info(`✅ 좌측 사이드바 (SUPER_ADMIN): 회사 ${userCompanyCode}의 모든 메뉴 표시`);
@@ -481,7 +474,6 @@ export class AdminService {
return [];
}
}
*/
// 2. 회사별 필터링 조건 생성
let companyFilter = "";

View File

@@ -1421,9 +1421,9 @@ export class TableManagementService {
const paramBase = paramIndex + idx * 4;
conditions.push(`(
${columnName}::text = $${paramBase} OR
${columnName}::text LIKE $${paramBase + 1} OR
${columnName}::text LIKE $${paramBase + 2} OR
${columnName}::text LIKE $${paramBase + 3}
${columnName}::text ILIKE $${paramBase + 1} OR
${columnName}::text ILIKE $${paramBase + 2} OR
${columnName}::text ILIKE $${paramBase + 3}
)`);
values.push(
safeValue,
@@ -3466,17 +3466,17 @@ export class TableManagementService {
}
case "contains":
filterConditions.push(
`${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}%'`
`${safeColumn} ILIKE '%${String(value).replace(/'/g, "''")}%'`
);
break;
case "starts_with":
filterConditions.push(
`${safeColumn} LIKE '${String(value).replace(/'/g, "''")}%'`
`${safeColumn} ILIKE '${String(value).replace(/'/g, "''")}%'`
);
break;
case "ends_with":
filterConditions.push(
`${safeColumn} LIKE '%${String(value).replace(/'/g, "''")}'`
`${safeColumn} ILIKE '%${String(value).replace(/'/g, "''")}'`
);
break;
case "is_null":
@@ -3487,7 +3487,7 @@ export class TableManagementService {
break;
case "not_contains":
filterConditions.push(
`${safeColumn}::text NOT LIKE '%${String(value).replace(/'/g, "''")}%'`
`${safeColumn}::text NOT ILIKE '%${String(value).replace(/'/g, "''")}%'`
);
break;
case "greater_than":
@@ -3553,16 +3553,16 @@ export class TableManagementService {
conditions.push(`${safeCol}::text != '${String(value).replace(/'/g, "''")}'`);
break;
case "contains":
conditions.push(`${safeCol}::text LIKE '%${String(value).replace(/'/g, "''")}%'`);
conditions.push(`${safeCol}::text ILIKE '%${String(value).replace(/'/g, "''")}%'`);
break;
case "not_contains":
conditions.push(`${safeCol}::text NOT LIKE '%${String(value).replace(/'/g, "''")}%'`);
conditions.push(`${safeCol}::text NOT ILIKE '%${String(value).replace(/'/g, "''")}%'`);
break;
case "starts_with":
conditions.push(`${safeCol}::text LIKE '${String(value).replace(/'/g, "''")}%'`);
conditions.push(`${safeCol}::text ILIKE '${String(value).replace(/'/g, "''")}%'`);
break;
case "ends_with":
conditions.push(`${safeCol}::text LIKE '%${String(value).replace(/'/g, "''")}'`);
conditions.push(`${safeCol}::text ILIKE '%${String(value).replace(/'/g, "''")}'`);
break;
case "greater_than":
conditions.push(`(${safeCol})::numeric > ${parseFloat(String(value))}`);

View File

@@ -39,13 +39,13 @@ export async function getWorkHistories(filters?: WorkHistoryFilters): Promise<Wo
}
if (filters?.vehicle_number) {
query += ` AND vehicle_number LIKE $${paramIndex}`;
query += ` AND vehicle_number ILIKE $${paramIndex}`;
params.push(`%${filters.vehicle_number}%`);
paramIndex++;
}
if (filters?.driver_name) {
query += ` AND driver_name LIKE $${paramIndex}`;
query += ` AND driver_name ILIKE $${paramIndex}`;
params.push(`%${filters.driver_name}%`);
paramIndex++;
}
@@ -64,10 +64,10 @@ export async function getWorkHistories(filters?: WorkHistoryFilters): Promise<Wo
if (filters?.search) {
query += ` AND (
work_number LIKE $${paramIndex} OR
vehicle_number LIKE $${paramIndex} OR
driver_name LIKE $${paramIndex} OR
cargo_name LIKE $${paramIndex}
work_number ILIKE $${paramIndex} OR
vehicle_number ILIKE $${paramIndex} OR
driver_name ILIKE $${paramIndex} OR
cargo_name ILIKE $${paramIndex}
)`;
params.push(`%${filters.search}%`);
paramIndex++;

View File

@@ -135,19 +135,19 @@ export function buildDataFilterWhereClause(
}
case "contains":
conditions.push(`${columnRef} LIKE $${paramIndex}`);
conditions.push(`${columnRef} ILIKE $${paramIndex}`);
params.push(`%${value}%`);
paramIndex++;
break;
case "starts_with":
conditions.push(`${columnRef} LIKE $${paramIndex}`);
conditions.push(`${columnRef} ILIKE $${paramIndex}`);
params.push(`${value}%`);
paramIndex++;
break;
case "ends_with":
conditions.push(`${columnRef} LIKE $${paramIndex}`);
conditions.push(`${columnRef} ILIKE $${paramIndex}`);
params.push(`%${value}`);
paramIndex++;
break;