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