Files
wace_rps/backend-node/src/services/ecrMngService.ts
T
chpark d7c645d24c
Build and Push Images / build-and-push (push) Has been cancelled
품질관리/고객CS/ECR — wace_plm 1:1 이식 + 견적관리 그리드 패턴 통일
신규 4개 메뉴 (PageHeader + CompactFilterBar + DataGrid 통일):
 - 품질관리/수입검사 요청 (/quality/incoming-request)
 - 품질관리/수입검사 관리 (/quality/incoming-mgmt)
 - 품질관리/공정검사 관리 (/quality/process-inspection)
 - 품질관리/반제품검사 관리 (/quality/semi-product-inspection)

DB 마이그레이션 (docs/migration/quality/):
 - 01_quality_tables_from_ilshin.sql — ilshin 운영 5개 테이블 vexplor_rps 정합
   (customer_service_mgmt/part/workingtime, inspection_mgmt, delivery_history_defect)
   + ecr_mng 7개 컬럼 동기화 (project_no, customer_cd, equip_name,
     design_dept, unit_cd, memo, check_result)
 - 02_wace_plm_quality_tables.sql — wace_plm quality.xml 매퍼 호환 신규 5개 테이블
   (incoming_inspection_detail/defect, process_inspection_master/detail,
    pms_quality_semi_product_inspection) + 인덱스 정의

백엔드:
 - qualityRoutes.ts — 4개 메뉴 list 엔드포인트 (실 테이블 조회)
 - ecrMngService SELECT_BASE 에 ilshin 신규 7컬럼 노출
 - app.ts 라우팅 등록 (/api/quality/*)

프론트:
 - DataGrid 4개 신규 페이지 + 그리드 툴바 (차트/엑셀/새로고침/컬럼설정/페이지사이즈)
 - customer-cs/cs, ecr/ecr — 견적관리와 동일한 PageHeader + CompactFilterBar
   + DataGrid 패턴으로 리팩토링 (다이얼로그/기존 API 유지)
 - ECR 그리드에 신규 6개 컬럼 추가 (설비명/프로젝트번호/고객사/설계부서/조치결과 등)
 - AdminPageRenderer 4개 라우트 등록

데이터 복사: ilshin → vexplor_rps (workingtime 5건, inspection_mgmt 1건,
ecr_mng 1건). 나머지 ilshin 운영 테이블은 0건이므로 스키마만 정합.
2026-05-14 19:08:15 +09:00

246 lines
7.9 KiB
TypeScript

/**
* ECR(Engineering Change Request) 관리 서비스
* - wace_plm 의 ECRController + ECRService + ecr.xml 을 Node 로 포팅
* - 동일 화면/동일 동작을 목표로 컬럼 명세 그대로 유지
* - PostgreSQL 기준 (원본도 PG)
*/
import { query, queryOne, transaction } from "../database/db";
import { logger } from "../utils/logger";
export interface EcrListParams {
year?: string;
productCode?: string;
requestCode?: string;
writer?: string;
statusCode?: string;
page?: number;
pageSize?: number;
}
export interface EcrUpsertParams {
objId?: string | number;
product_objid?: string | number | null;
part_objid?: string | number | null;
request_codeArr?: string; // 콤마 구분 다중 코드
title?: string;
before_contents?: string;
after_contents?: string;
writer: string;
}
const SELECT_BASE = `
SELECT
T.objid::text AS objid,
T.ecr_no,
(SELECT product_code FROM product_mgmt WHERE objid = T.product_objid) AS product_name,
T.product_objid::text AS product_objid,
T.upg_no,
T.part_objid::text AS part_objid,
(SELECT part_no FROM part_mng WHERE objid = T.part_objid) AS part_no,
(SELECT part_name FROM part_mng WHERE objid = T.part_objid) AS part_name,
T.request_cd,
T.title,
T.writer,
(SELECT user_name FROM user_info WHERE user_id = T.writer) AS writer_name,
T.status_cd,
-- wace_plm 의 code_name(varchar) 함수를 그대로 사용 (ecrTableMigration 에서 함수도 동일 정의)
code_name(T.status_cd) AS status_name,
(SELECT user_name FROM user_info WHERE user_id = T.check_user_id) AS check_name,
T.check_user_id,
T.before_contents,
T.after_contents,
TO_CHAR(T.reg_date, 'YYYY-MM-DD') AS reg_date,
TO_CHAR(T.check_date, 'YYYY-MM-DD') AS check_date,
-- 요청 코드 콤마 분리 → code_name 콤마 결합 (원본 자바와 동일)
(
SELECT array_to_string(
array_agg((SELECT code_name FROM comm_code cc WHERE cc.code_id = trim(req))),
', '
)
FROM unnest(string_to_array(coalesce(T.request_cd, ''), ',')) AS req
) AS request_name,
-- ilshin 운영 ecr_mng 와 동기화된 신규 컬럼 (docs/migration/quality/01_*.sql)
T.project_no,
T.customer_cd,
T.equip_name,
T.design_dept,
T.unit_cd,
T.memo,
T.check_result
FROM ecr_mng T
`;
export class EcrMngService {
/** 목록 (페이징 + 필터) */
static async list(p: EcrListParams) {
const page = Math.max(1, Number(p.page) || 1);
const pageSize = Math.max(1, Math.min(200, Number(p.pageSize) || 20));
const offset = (page - 1) * pageSize;
const where: string[] = ["1=1"];
const args: any[] = [];
let i = 1;
if (p.year) {
where.push(`TO_CHAR(T.reg_date, 'YYYY') = $${i++}`);
args.push(p.year);
}
if (p.productCode) {
where.push(`T.product_objid = $${i++}::bigint`);
args.push(p.productCode);
}
if (p.requestCode) {
where.push(`UPPER(T.request_cd) LIKE UPPER('%' || $${i++} || '%')`);
args.push(p.requestCode);
}
if (p.writer) {
where.push(`T.writer = $${i++}`);
args.push(p.writer);
}
if (p.statusCode) {
where.push(`T.status_cd = $${i++}`);
args.push(p.statusCode);
}
const whereSql = where.join(" AND ");
const cntRow = await queryOne<{ total: string }>(
`SELECT COUNT(1)::int AS total FROM ecr_mng T WHERE ${whereSql}`,
args,
);
const total = Number(cntRow?.total || 0);
const rows = await query<any>(
`${SELECT_BASE} WHERE ${whereSql} ORDER BY T.reg_date DESC, T.objid DESC LIMIT $${i++} OFFSET $${i++}`,
[...args, pageSize, offset],
);
return {
list: rows,
pagination: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) },
};
}
/** 단건 상세 */
static async detail(objId: string | number) {
const row = await queryOne<any>(
`${SELECT_BASE} WHERE T.objid = $1::bigint`,
[objId],
);
return row;
}
/**
* 등록/수정 (mergeECR)
* 원본: ON CONFLICT (OBJID) DO UPDATE …
* 신규 시 ECR_NO 자동 채번: ECR-YYYY-{seq_ecr_no:3자리}
*/
static async merge(p: EcrUpsertParams) {
const objId = p.objId ? String(p.objId) : `${Date.now()}`; // 신규 시 epoch 기반 OBJID
return await transaction(async (client) => {
const r = await client.query(
`
INSERT INTO ecr_mng
(objid, ecr_no, product_objid, part_objid, request_cd, title, writer,
status_cd, before_contents, after_contents, reg_date)
VALUES
($1::bigint,
'ECR-' || TO_CHAR(NOW(), 'YYYY') || '-' || LPAD(nextval('seq_ecr_no')::TEXT, 3, '0'),
NULLIF($2,'')::bigint, NULLIF($3,'')::bigint, $4, $5, $6,
'0000100', $7, $8, NOW())
ON CONFLICT (objid) DO UPDATE SET
title = EXCLUDED.title,
product_objid = EXCLUDED.product_objid,
part_objid = EXCLUDED.part_objid,
request_cd = EXCLUDED.request_cd,
before_contents = EXCLUDED.before_contents
RETURNING objid, ecr_no, status_cd
`,
[
objId,
p.product_objid ? String(p.product_objid) : "",
p.part_objid ? String(p.part_objid) : "",
p.request_codeArr || "",
p.title || "",
p.writer,
p.before_contents || "",
p.after_contents || "",
],
);
return r.rows[0];
});
}
/**
* 조치완료 (completeECR)
* - status_cd → '0000102'(조치완료), after_contents 갱신, check_user_id/check_date 기록
*/
static async complete(p: { objId: string | number; after_contents?: string; writer: string }) {
const r = await query<any>(
`
UPDATE ecr_mng
SET status_cd = '0000102',
after_contents = $1,
check_user_id = $2,
check_date = NOW()
WHERE objid = $3::bigint
RETURNING objid, status_cd
`,
[p.after_contents || "", p.writer, p.objId],
);
return r[0];
}
/** 삭제 (단건 또는 콤마 다중) */
static async deleteByIds(ids: Array<string | number>) {
if (!ids?.length) return { deleted: 0 };
return await transaction(async (client) => {
const r = await client.query(
`DELETE FROM ecr_mng WHERE objid = ANY($1::bigint[]) RETURNING objid`,
[ids.map((x) => String(x))],
);
return { deleted: r.rowCount || 0 };
});
}
/** 작성자 select 옵션 — 원본 common.getUserselect 유사 */
static async writerOptions() {
return await query<any>(
`SELECT user_id AS value, user_name AS label FROM user_info
WHERE status='active' AND user_id IS NOT NULL AND user_id <> ''
ORDER BY user_name`,
);
}
/** 제품 select 옵션 — 원본 common.getProductCodeselect 유사 */
static async productOptions() {
return await query<any>(
`SELECT objid::text AS value, COALESCE(product_code, product_name) AS label
FROM product_mgmt
WHERE COALESCE(status,'active') = 'active'
ORDER BY product_code, product_name`,
);
}
/** 부품 select 옵션 */
static async partOptions() {
return await query<any>(
`SELECT objid::text AS value,
COALESCE(part_no, part_name) || COALESCE(' (' || part_name || ')', '') AS label
FROM part_mng
WHERE COALESCE(status,'active') = 'active'
ORDER BY part_no`,
);
}
/** 공통코드 child 옵션 (parent_code_id 기준) */
static async commCodeOptions(parentCodeId: string) {
return await query<any>(
`SELECT code_id AS value, code_name AS label
FROM comm_code
WHERE parent_code_id = $1 AND COALESCE(status,'active') = 'active'
ORDER BY sort_order, code_name`,
[parentCodeId],
);
}
}