Files
wace_rps/backend-node/src/services/ecrMngService.ts
T
chpark 690b85805c ECR 기능/스키마 wace_plm 일치 + 공통코드·테이블타입 화면 정리
- ECR 관리: wace 의 ecrList/Form/Detail JSP 와 동일하게 5개 필터(연도/기종/요청/작성자/상태),
  변경전/후 2분할 모달, 작성중(0000100)만 삭제·수정 허용, 컬럼 순서/라벨 wace 일치
- ECR 스키마 wace 풀세트 동기화: ecr_mng 컬럼폭 확장, product_mgmt 16컬럼, part_mng 52컬럼,
  user_info 22컬럼, comm_code 보강(id/code_cd/ext_val), code_name(varchar) 함수,
  seq_ecr_no setval(33) 정렬
- wace_plm public.comm_code 733행 시드: src/seed/wace_comm_code.sql 추출 + 부팅 시 자동 적재
  (writer='system-seed' placeholder 자동 정리, 무중단 재적재 엔드포인트 /comm-code-seed)
- wace_plm 데이터 import 풀스키마: PRODUCT(7→16), PART(6→52), USER_INFO·COMM_CODE 신규
- 공통코드 관리 화면: 제목/설명 축소, 카테고리·코드 카드 → 컴팩트 리스트, 활성 토글 점,
  계층 배지 톤다운, hover 시 액션 노출
- 테이블 타입 관리 — 좌측: 한 줄 리스트 + 알파벳 인덱스 sticky 헤더
- 테이블 타입 관리 — 우측: 타입 카드 그리드 → 그룹 셀렉트(기본/참조/자동/첨부/표시변형),
  표시이름 제거 + 코멘트(description) Textarea 신설(화면관리에서 기본 라벨로 활용),
  시스템 자동 생성 컬럼(id/company_code/writer/created_date/updated_date) 잠금,
  표시옵션/고급설정(필수·읽기·기본값·최대길이) 제거 — 화면관리로 이관

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:58:54 +09:00

238 lines
7.7 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
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],
);
}
}