Merge branch 'mhkim-node' of https://g.wace.me/jskim/vexplor_dev into jskim-node

This commit is contained in:
kjs
2026-04-24 17:59:00 +09:00
126 changed files with 27780 additions and 896 deletions

View File

@@ -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 });