Merge branch 'mhkim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node
This commit is contained in:
@@ -6,6 +6,7 @@ import { AuthenticatedRequest } from "../types/auth";
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
import { numberingRuleService } from "../services/numberingRuleService";
|
||||
import { copyChecklistToSplit } from "./popProductionController";
|
||||
|
||||
// 자동 마이그레이션: work_instruction_detail에 routing_version_id + 품목별 일정/설비/작업조/작업자 컬럼 추가
|
||||
let _migrationDone = false;
|
||||
@@ -717,6 +718,80 @@ export async function getWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* wi_* 편집 시 마스터 체크리스트 스냅샷을 재투영한다.
|
||||
* 접수(work_order_process_result) 가 0건일 때만 동기화되며, 1건 이상이면 스냅샷 불변.
|
||||
* 트랜잭션 내에서 호출되어야 한다 (caller 가 BEGIN/COMMIT 관리).
|
||||
*
|
||||
* @param routingDetailId null 이면 해당 작업지시의 모든 routing detail 동기화
|
||||
* @returns synced: 실제 동기화 수행 여부, affectedProcesses: 재복사된 마스터 공정 수
|
||||
*/
|
||||
async function syncMasterChecklistFromWi(
|
||||
client: { query: (text: string, values?: any[]) => Promise<any> },
|
||||
workInstructionNo: string,
|
||||
routingDetailId: string | null,
|
||||
companyCode: string,
|
||||
userId: string,
|
||||
): Promise<{ synced: boolean; affectedProcesses: number; reason?: string }> {
|
||||
// 1. 작업지시 id 조회
|
||||
const wiRow = await client.query(
|
||||
`SELECT id FROM work_instruction WHERE work_instruction_no = $1 AND company_code = $2`,
|
||||
[workInstructionNo, companyCode],
|
||||
);
|
||||
if (wiRow.rowCount === 0) {
|
||||
return { synced: false, affectedProcesses: 0, reason: "work_instruction not found" };
|
||||
}
|
||||
const wiId = wiRow.rows[0].id as string;
|
||||
|
||||
// 2. advisory lock — 편집/접수 동시성 보호
|
||||
await client.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [
|
||||
`wi_snapshot:${companyCode}:${wiId}`,
|
||||
]);
|
||||
|
||||
// 3. 접수 건수 확인
|
||||
const acceptCount = await client.query(
|
||||
`SELECT COUNT(*)::int AS cnt FROM work_order_process_result wopr
|
||||
JOIN work_order_process wop ON wop.id = wopr.work_order_process_id
|
||||
WHERE wop.wo_id = $1 AND wop.company_code = $2 AND wopr.company_code = $2`,
|
||||
[wiId, companyCode],
|
||||
);
|
||||
if ((acceptCount.rows[0]?.cnt ?? 0) > 0) {
|
||||
return { synced: false, affectedProcesses: 0, reason: "accepted_count > 0" };
|
||||
}
|
||||
|
||||
// 4. 대상 마스터 공정 목록
|
||||
const masterQuery = routingDetailId
|
||||
? `SELECT id, routing_detail_id FROM work_order_process
|
||||
WHERE wo_id = $1 AND routing_detail_id = $2 AND company_code = $3`
|
||||
: `SELECT id, routing_detail_id FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2`;
|
||||
const masterParams = routingDetailId
|
||||
? [wiId, routingDetailId, companyCode]
|
||||
: [wiId, companyCode];
|
||||
const masters = await client.query(masterQuery, masterParams);
|
||||
|
||||
let affected = 0;
|
||||
for (const m of masters.rows) {
|
||||
// 5. 기존 마스터 스냅샷 삭제
|
||||
await client.query(
|
||||
`DELETE FROM process_work_result WHERE work_order_process_id = $1 AND company_code = $2`,
|
||||
[m.id, companyCode],
|
||||
);
|
||||
// 6. 재복사 — copyChecklistToSplit 재활용 (wi_* 우선, 없으면 원본 fallback)
|
||||
await copyChecklistToSplit(
|
||||
client,
|
||||
m.id,
|
||||
m.id,
|
||||
m.routing_detail_id,
|
||||
companyCode,
|
||||
userId,
|
||||
{ workInstructionNo },
|
||||
);
|
||||
affected++;
|
||||
}
|
||||
return { synced: true, affectedProcesses: affected };
|
||||
}
|
||||
|
||||
// ─── 원본 공정작업기준 -> 작업지시 전용 복사 ───
|
||||
export async function copyWorkStandard(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
@@ -783,6 +858,8 @@ export async function copyWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
}
|
||||
}
|
||||
|
||||
const sync = await syncMasterChecklistFromWi(client, wiNo, null, companyCode, userId);
|
||||
logger.info("[work-instruction] wi_* copy 후 마스터 스냅샷 동기화", { wiNo, ...sync });
|
||||
await client.query("COMMIT");
|
||||
logger.info("공정작업기준 복사 완료", { companyCode, wiNo, routingVersionId });
|
||||
return res.json({ success: true });
|
||||
@@ -850,6 +927,8 @@ export async function saveWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
}
|
||||
}
|
||||
|
||||
const sync = await syncMasterChecklistFromWi(client, wiNo, routingDetailId, companyCode, userId);
|
||||
logger.info("[work-instruction] wi_* save 후 마스터 스냅샷 동기화", { wiNo, routingDetailId, ...sync });
|
||||
await client.query("COMMIT");
|
||||
logger.info("작업지시 공정작업기준 저장 완료", { companyCode, wiNo, routingDetailId });
|
||||
return res.json({ success: true });
|
||||
@@ -869,6 +948,7 @@ export async function saveWorkStandard(req: AuthenticatedRequest, res: Response)
|
||||
export async function resetWorkStandard(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { wiNo } = req.params;
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
@@ -889,6 +969,8 @@ export async function resetWorkStandard(req: AuthenticatedRequest, res: Response
|
||||
`DELETE FROM wi_process_work_item WHERE work_instruction_no = $1 AND company_code = $2`,
|
||||
[wiNo, companyCode]
|
||||
);
|
||||
const sync = await syncMasterChecklistFromWi(client, wiNo, null, companyCode, userId);
|
||||
logger.info("[work-instruction] wi_* reset 후 마스터 스냅샷 원본 복원", { wiNo, ...sync });
|
||||
await client.query("COMMIT");
|
||||
logger.info("작업지시 공정작업기준 초기화", { companyCode, wiNo });
|
||||
return res.json({ success: true });
|
||||
|
||||
Reference in New Issue
Block a user