summaryrefslogtreecommitdiff
path: root/lib/sedp
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-09 12:19:05 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-09 12:19:05 +0000
commit6d654b1ba2c19e0bf1745b636908e3b00a0f02c7 (patch)
treef6d48c0d3a65b428a828acea5db65db8e7bf0db8 /lib/sedp
parent44794a8628997c0d979adb5bd6711cd848b3e397 (diff)
(대표님) 20250709 변경사항 (약 18시 30분까지)
Diffstat (limited to 'lib/sedp')
-rw-r--r--lib/sedp/sync-form copy.ts1037
-rw-r--r--lib/sedp/sync-form.ts503
2 files changed, 1343 insertions, 197 deletions
diff --git a/lib/sedp/sync-form copy.ts b/lib/sedp/sync-form copy.ts
new file mode 100644
index 00000000..2cb677b7
--- /dev/null
+++ b/lib/sedp/sync-form copy.ts
@@ -0,0 +1,1037 @@
+// src/lib/cron/syncTagFormMappings.ts
+import db from "@/db/db";
+import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items, contracts } from '@/db/schema';
+import { eq, and, inArray, ilike } from 'drizzle-orm';
+import { getSEDPToken } from "./sedp-token";
+
+// 환경 변수
+const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api';
+
+// 인터페이스 정의
+interface TagTypeClassFormMapping {
+ projectId: number;
+ tagTypeLabel: string;
+ classLabel: string;
+ formCode: string;
+ formName: string;
+ remark: string | null;
+ ep: string;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface FormMeta {
+ projectId: number;
+ formCode: string;
+ formName: string;
+ columns: string; // JSON 문자열
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+interface FormRecord {
+ contractItemId: number;
+ formCode: string;
+ formName: string;
+ eng: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+}
+interface Register {
+ PROJ_NO: string;
+ TYPE_ID: string;
+ EP_ID: string;
+ DESC: string;
+ REMARK: string | null;
+ NEW_TAG_YN: boolean;
+ ALL_TAG_YN: boolean;
+ VND_YN: boolean;
+ SEQ: number;
+ CMPLX_YN: boolean;
+ CMPL_SETT: any | null;
+ MAP_ATT: any[];
+ MAP_CLS_ID: string[];
+ MAP_OPER: any | null;
+ LNK_ATT: LinkAttribute[];
+ JOIN_TABLS: any[];
+ DELETED: boolean;
+ CRTER_NO: string;
+ CRTE_DTM: string;
+ CHGER_NO: string | null;
+ CHGE_DTM: string | null;
+ _id: string;
+}
+
+interface LinkAttribute {
+ ATT_ID: string;
+ CPY_DESC: string;
+ JOIN_KEY_ATT_ID: string | null;
+ JOIN_VAL_ATT_ID: string | null;
+ KEY_YN: boolean;
+ EDIT_YN: boolean;
+ PUB_YN: boolean;
+ VND_YN: boolean;
+ DEF_VAL: string | null;
+ UOM_ID: string | null;
+}
+
+interface Attribute {
+ PROJ_NO: string;
+ ATT_ID: string;
+ DESC: string;
+ GROUP: string | null;
+ REMARK: string | null;
+ VAL_TYPE: string;
+ IGN_LIST_VAL: boolean;
+ CL_ID: string | null;
+ UOM_ID: string | null;
+ DEF_VAL: string | null;
+ MIN_VAL: number;
+ MAX_VAL: number;
+ ESS_YN: boolean;
+ SEQ: number;
+ FORMAT: string | null;
+ REG_EXPS: string | null;
+ ATTRIBUTES: any[];
+ DELETED: boolean;
+ CRTER_NO: string;
+ CRTE_DTM: string;
+ CHGER_NO: string | null;
+ CHGE_DTM: string | null;
+ _id: string;
+}
+
+interface CodeList {
+ PROJ_NO: string;
+ CL_ID: string;
+ DESC: string;
+ REMARK: string | null;
+ PRNT_CD_ID: string | null;
+ REG_TYPE_ID: string | null;
+ VAL_ATT_ID: string | null;
+ VALUES: CodeValue[];
+ LNK_ATT: any[];
+ DELETED: boolean;
+ CRTER_NO: string;
+ CRTE_DTM: string;
+ CHGER_NO: string | null;
+ CHGE_DTM: string | null;
+ _id: string;
+}
+
+interface CodeValue {
+ PRNT_VALUE: string | null;
+ VALUE: string;
+ DESC: string;
+ REMARK: string;
+ USE_YN: boolean;
+ SEQ: number;
+ ATTRIBUTES: any[];
+}
+
+interface UOM {
+ PROJ_NO: string;
+ UOM_ID: string;
+ DESC: string;
+ SYMBOL: string;
+ CONV_RATE: number;
+ DELETED: boolean;
+ CRTER_NO: string;
+ CRTE_DTM: string;
+ CHGER_NO: string | null;
+ CHGE_DTM: string | null;
+ _id: string;
+}
+
+interface Project {
+ id: number;
+ code: string;
+ name: string;
+ type?: string;
+ createdAt?: Date;
+ updatedAt?: Date;
+}
+
+interface SyncResult {
+ project: string;
+ success: boolean;
+ count?: number;
+ error?: string;
+}
+
+interface FormColumn {
+ key: string;
+ label: string;
+ type: string;
+ options?: string[];
+ uom?: string;
+ uomId?: string;
+ shi?: Boolean;
+}
+
+// 아이템 코드 추출 함수
+function extractItemCodes(remark: string | null): string[] {
+ if (!remark) return [];
+
+ // 검색용으로만 소문자로 변환
+ const remarkLower = remark.toLowerCase();
+
+ // 'vd_' 접두사 확인
+ const hasVD_ = remarkLower.includes("vd_");
+
+ if (!hasVD_) return [];
+
+ let vdPart = "";
+
+ // 'vd_'가 있으면 원본 문자열에서 추출 (소문자 버전이 아님)
+ if (hasVD_) {
+ const vdIndex = remarkLower.indexOf("vd_");
+ vdPart = remark.substring(vdIndex + 3); // 원본 문자열에서 추출
+ }
+
+ if (!vdPart) return [];
+
+ // 쉼표로 구분된 여러 itemCode 처리
+ return vdPart.split(",").map(code => code.trim());
+}
+
+async function getDefaulTAttributes(): Promise<string[]> {
+ try {
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/Dictionary/GetByKey`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ },
+ body: JSON.stringify({
+ Key: "DefaultAttributesToCompare",
+ })
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.warn(`디폴트 속성 찾을 수 없음`);
+ return [];
+ }
+ throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+ // 데이터가 배열인지 확인하고 문자열 배열로 변환
+ if (Array.isArray(data)) {
+ return data as string[];
+ } else {
+ console.warn('응답이 배열 형식이 아닙니다');
+ return [];
+ }
+ } catch (parseError) {
+ console.error(`디폴트 속성 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return [];
+ }
+ } catch (error) {
+ console.error(`디폴트 어트리뷰트 가져오기 실패:`, error);
+ throw error;
+ }
+}
+
+// 레지스터 데이터 가져오기
+async function getRegisters(projectCode: string): Promise<Register[]> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/Register/Get`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`레지스터 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ let data;
+ try {
+ data = await response.json();
+ } catch (parseError) {
+ console.error(`프로젝트 ${projectCode}의 레지스터 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ throw new Error(`레지스터 응답 파싱 실패: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
+ }
+
+ // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑)
+ let registers: Register[] = Array.isArray(data) ? data : [data];
+
+ // MAP_CLS_ID가 비어있지 않고 REMARK가 vd, VD, vD, Vd 중 하나인 레지스터만 필터링
+ registers = registers.filter(register => {
+ // 삭제된 레지스터 제외
+ if (register.DELETED) return false;
+
+ // MAP_CLS_ID 배열이 존재하고 요소가 하나 이상 있는지 확인
+ const hasValidMapClsId = Array.isArray(register.MAP_CLS_ID) && register.MAP_CLS_ID.length > 0;
+
+ // REMARK가 'vd_' 또는 'vd' 포함 확인 (대소문자 구분 없이)
+ const remarkLower = register.REMARK && register.REMARK.toLowerCase();
+ const hasValidRemark = remarkLower && (remarkLower.includes('vd'));
+
+ // 두 조건 모두 충족해야 함
+ return hasValidMapClsId && hasValidRemark;
+ });
+
+ console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 유효한 레지스터를 가져왔습니다.`);
+ return registers;
+ } catch (error) {
+ console.error(`프로젝트 ${projectCode}의 레지스터 가져오기 실패:`, error);
+ throw error;
+ }
+}
+
+// 프로젝트의 모든 속성을 가져와 맵으로 반환
+async function getAttributes(projectCode: string): Promise<Map<string, Attribute>> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/Attributes/Get`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+
+ // 데이터가 배열인지 확인
+ const attributes: Attribute[] = Array.isArray(data) ? data : [data];
+
+ // ATT_ID로 효율적인 조회를 위한 맵 생성
+ const attributeMap = new Map<string, Attribute>();
+ for (const attribute of attributes) {
+ if (!attribute.DELETED) {
+ attributeMap.set(attribute.ATT_ID, attribute);
+ }
+ }
+
+ console.log(`프로젝트 ${projectCode}에서 ${attributeMap.size}개의 속성을 가져왔습니다`);
+ return attributeMap;
+
+ } catch (parseError) {
+ console.error(`프로젝트 ${projectCode}의 속성 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return new Map();
+ }
+ } catch (error) {
+ console.error(`프로젝트 ${projectCode}의 속성 가져오기 실패:`, error);
+ return new Map();
+ }
+}
+
+// 특정 속성 가져오기 (하위 호환성을 위해 유지)
+async function getAttributeById(projectCode: string, attributeId: string, register: string): Promise<Attribute | null> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/Attributes/GetByID`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ ATT_ID: attributeId,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.warn(`속성 ID ${attributeId}를 찾을 수 없음`);
+ return null;
+ }
+ throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+ return data;
+ } catch (parseError) {
+ console.error(`속성 ID ${attributeId} ${register} ${projectCode} 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return null;
+ }
+ } catch (error) {
+ console.error(`속성 ID ${attributeId} 가져오기 실패:`, error);
+ return null;
+ }
+}
+
+// 프로젝트의 모든 코드 리스트를 가져와 맵으로 반환
+async function getCodeLists(projectCode: string): Promise<Map<string, CodeList>> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/CodeList/Get`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+
+ // 데이터가 배열인지 확인
+ const codeLists: CodeList[] = Array.isArray(data) ? data : [data];
+
+ // CL_ID로 효율적인 조회를 위한 맵 생성
+ const codeListMap = new Map<string, CodeList>();
+ for (const codeList of codeLists) {
+ if (!codeList.DELETED) {
+ codeListMap.set(codeList.CL_ID, codeList);
+ }
+ }
+
+ console.log(`프로젝트 ${projectCode}에서 ${codeListMap.size}개의 코드 리스트를 가져왔습니다`);
+ return codeListMap;
+
+ } catch (parseError) {
+ console.error(`프로젝트 ${projectCode}의 코드 리스트 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return new Map();
+ }
+ } catch (error) {
+ console.error(`프로젝트 ${projectCode}의 코드 리스트 가져오기 실패:`, error);
+ return new Map();
+ }
+}
+
+// 특정 코드 리스트 가져오기 (하위 호환성을 위해 유지)
+async function getCodeListById(projectCode: string, codeListId: string): Promise<CodeList | null> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/CodeList/GetByID`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ CL_ID: codeListId,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.warn(`코드 리스트 ID ${codeListId}를 찾을 수 없음`);
+ return null;
+ }
+ throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+ return data;
+ } catch (parseError) {
+ console.error(`코드 리스트 ID ${codeListId} 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return null;
+ }
+ } catch (error) {
+ console.error(`코드 리스트 ID ${codeListId} 가져오기 실패:`, error);
+ return null;
+ }
+}
+
+// 프로젝트의 모든 UOM을 가져와 맵으로 반환
+async function getUOMs(projectCode: string): Promise<Map<string, UOM>> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/UOM/Get`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`UOM 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+
+ // 데이터가 배열인지 확인
+ const uoms: UOM[] = Array.isArray(data) ? data : [data];
+
+ // UOM_ID로 효율적인 조회를 위한 맵 생성
+ const uomMap = new Map<string, UOM>();
+ for (const uom of uoms) {
+ if (!uom.DELETED) {
+ uomMap.set(uom.UOM_ID, uom);
+ }
+ }
+
+ console.log(`프로젝트 ${projectCode}에서 ${uomMap.size}개의 UOM을 가져왔습니다`);
+ return uomMap;
+
+ } catch (parseError) {
+ console.error(`프로젝트 ${projectCode}의 UOM 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return new Map();
+ }
+ } catch (error) {
+ console.error(`프로젝트 ${projectCode}의 UOM 가져오기 실패:`, error);
+ return new Map();
+ }
+}
+
+// UOM 가져오기 (하위 호환성을 위해 유지)
+async function getUomById(projectCode: string, uomId: string): Promise<UOM | null> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/UOM/GetByID`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ UOMID: uomId, // API 명세서에 따라 UOMID 사용
+ ProjectNo: projectCode,
+ ContainDeleted: false
+ })
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.warn(`UOM ID ${uomId}를 찾을 수 없음`);
+ return null;
+ }
+ throw new Error(`UOM 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+ return data;
+ } catch (parseError) {
+ console.error(`UOM ID ${uomId} 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return null;
+ }
+ } catch (error) {
+ console.error(`UOM ID ${uomId} 가져오기 실패:`, error);
+ return null;
+ }
+}
+
+// contractItemId 조회 함수
+async function getContractItemsByItemCodes(itemCodes: string[], projectId: number): Promise<Map<string, number>> {
+ try {
+ if (!itemCodes.length) return new Map();
+
+ // 먼저 itemCodes에 해당하는 item 레코드를 조회
+ const itemRecords = await db.select({
+ id: items.id,
+ itemCode: items.itemCode
+ })
+ .from(items)
+ .where(inArray(items.itemCode, itemCodes));
+
+ if (!itemRecords.length) {
+ console.log(`No items found for itemCodes: ${itemCodes.join(', ')}`);
+ return new Map();
+ }
+
+ // item ID 목록 추출
+ const itemIds = itemRecords.map(item => item.id);
+
+ // contracts와 join하여 projectId로 필터링하면서 contractItems 조회
+ const contractItemRecords = await db.select({
+ id: contractItems.id,
+ itemId: contractItems.itemId
+ })
+ .from(contractItems)
+ .innerJoin(contracts, eq(contractItems.contractId, contracts.id))
+ .where(
+ and(
+ inArray(contractItems.itemId, itemIds),
+ eq(contracts.projectId, projectId)
+ )
+ );
+
+ // itemCode와 contractItemId의 매핑 생성
+ const itemCodeToContractItemId = new Map<string, number>();
+
+ for (const item of itemRecords) {
+ // itemCode가 null이 아닌 경우에만 처리
+ if (item.itemCode) {
+ const matchedContractItems = contractItemRecords.filter(ci => ci.itemId === item.id);
+ if (matchedContractItems.length > 0) {
+ // 일치하는 첫 번째 contractItem 사용
+ itemCodeToContractItemId.set(item.itemCode, matchedContractItems[0].id);
+ }
+ }
+ }
+
+ return itemCodeToContractItemId;
+ } catch (error) {
+ console.error('ContractItems 조회 중 오류 발생:', error);
+ return new Map();
+ }
+}
+
+// 데이터베이스에 태그 타입 클래스 폼 매핑 및 폼 메타 저장
+async function saveFormMappingsAndMetas(
+ projectId: number,
+ projectCode: string,
+ registers: Register[]
+): Promise<number> {
+ try {
+ // 프로젝트의 태그 타입과 클래스 가져오기
+ const tagTypeRecords = await db.select()
+ .from(tagTypes)
+ .where(eq(tagTypes.projectId, projectId));
+
+ const tagClassRecords = await db.select()
+ .from(tagClasses)
+ .where(eq(tagClasses.projectId, projectId));
+
+ // 태그 타입과 클래스 매핑
+ const tagTypeMap = new Map(tagTypeRecords.map(type => [type.code, type]));
+ const tagClassMap = new Map(tagClassRecords.map(cls => [cls.code, cls]));
+
+ // 모든 속성, 코드 리스트, UOM을 한 번에 가져와 반복 API 호출 방지
+ const attributeMap = await getAttributes(projectCode);
+ const codeListMap = await getCodeLists(projectCode);
+ const uomMap = await getUOMs(projectCode);
+
+ // 기본 속성 가져오기
+ const defaultAttributes = await getDefaulTAttributes();
+
+ // 모든 register에서 itemCode를 추출하여 한 번에 조회
+ const allItemCodes: string[] = [];
+ registers.forEach(register => {
+ if (register.REMARK) {
+ const itemCodes = extractItemCodes(register.REMARK);
+ allItemCodes.push(...itemCodes);
+ }
+ });
+
+ // 중복 제거
+ const uniqueItemCodes = [...new Set(allItemCodes)];
+
+ // 모든 itemCode에 대한 contractItemId 조회
+ const itemCodeToContractItemId = await getContractItemsByItemCodes(uniqueItemCodes , projectId);
+
+ console.log(`${uniqueItemCodes.length}개의 고유 itemCode 중 ${itemCodeToContractItemId.size}개의 contractItem을 찾았습니다`);
+
+ // 저장할 데이터 준비
+ const mappingsToSave: TagTypeClassFormMapping[] = [];
+ const formMetasToSave: FormMeta[] = [];
+ const formsToSave: FormRecord[] = [];
+
+ // 폼이 있는 contractItemId 트래킹
+ const contractItemIdsWithForms = new Set<number>();
+
+ // 각 register 처리
+ for (const register of registers) {
+ // 삭제된 register 건너뛰기
+ if (register.DELETED) continue;
+
+ // REMARK에서 itemCodes 추출
+
+
+ // 폼 메타용 columns 구성
+ const columns: FormColumn[] = [];
+
+ for (const linkAtt of register.LNK_ATT) {
+ let attribute = null;
+
+ // 기본 속성인지 확인
+ if (defaultAttributes && defaultAttributes.includes(linkAtt.ATT_ID)) {
+ // 기본 속성에 대한 기본 attribute 객체 생성
+ attribute = {
+ DESC: linkAtt.ATT_ID,
+ VAL_TYPE: 'STRING'
+ };
+ } else {
+ // 맵에서 속성 조회
+ attribute = attributeMap.get(linkAtt.ATT_ID);
+
+ // 속성을 찾지 못한 경우 다음으로 넘어감
+ if (!attribute) continue;
+ }
+
+ // 컬럼 정보 생성
+ const column: FormColumn = {
+ key: linkAtt.ATT_ID,
+ label: attribute.DESC,
+ type: (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST')
+ ? 'LIST'
+ : (attribute.VAL_TYPE || 'STRING'),
+ shi: attribute.REMARK?.toLocaleLowerCase() === "shi"
+ };
+
+ // 리스트 타입에 대한 옵션 추가 (기본 속성이 아닌 경우)
+ if (!defaultAttributes.includes(linkAtt.ATT_ID) &&
+ (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') &&
+ attribute.CL_ID) {
+
+ // 맵에서 코드 리스트 조회
+ const codeList = codeListMap.get(attribute.CL_ID);
+
+ if (codeList && codeList.VALUES) {
+ const options = [...new Set(
+ codeList.VALUES
+ .filter(value => value.USE_YN)
+ .map(value => value.VALUE)
+ )];
+
+ if (options.length > 0) {
+ column.options = options;
+ }
+ }
+ }
+
+ // UOM 정보 추가
+ if (linkAtt.UOM_ID) {
+ const uom = uomMap.get(linkAtt.UOM_ID);
+
+ if (uom) {
+ column.uom = uom.SYMBOL;
+ column.uomId = uom.UOM_ID;
+ }
+ }
+
+ columns.push(column);
+ }
+
+ // 컬럼이 없으면 건너뛰기
+ if (columns.length === 0) {
+ console.log(`폼 ${register.TYPE_ID} (${register.DESC})에 컬럼이 없어 건너뜁니다`);
+ continue;
+ }
+
+ // 폼 메타 데이터 준비
+ formMetasToSave.push({
+ projectId,
+ formCode: register.TYPE_ID,
+ formName: register.DESC,
+ columns: JSON.stringify(columns),
+ createdAt: new Date(),
+ updatedAt: new Date()
+ });
+
+ // 클래스 매핑 처리
+ for (const classId of register.MAP_CLS_ID) {
+ const tagClass = tagClassMap.get(classId);
+
+ if (!tagClass) {
+ console.warn(`프로젝트 ID ${projectId}에서 클래스 ID ${classId}를 찾을 수 없습니다`);
+ continue;
+ }
+
+ const tagTypeCode = tagClass.tagTypeCode;
+ const tagType = tagTypeMap.get(tagTypeCode);
+
+ if (!tagType) {
+ console.warn(`프로젝트 ID ${projectId}에서 태그 타입 ${tagTypeCode}를 찾을 수 없습니다`);
+ continue;
+ }
+
+ // 매핑 정보 저장
+ mappingsToSave.push({
+ projectId,
+ tagTypeLabel: tagType.description,
+ classLabel: tagClass.label,
+ formCode: register.TYPE_ID,
+ formName: register.DESC,
+ remark: register.REMARK,
+ ep: register.EP_ID,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ });
+ }
+
+ const itemCodes = extractItemCodes(register.REMARK || '');
+ if (!itemCodes.length) {
+ console.log(`Register ${register.TYPE_ID} (${register.DESC})의 REMARK에 유효한 itemCode가 없습니다`);
+ continue;
+ }
+ // 폼 레코드 준비
+ for (const itemCode of itemCodes) {
+ const contractItemId = itemCodeToContractItemId.get(itemCode);
+
+ if (!contractItemId) {
+ console.warn(`itemCode: ${itemCode}에 대한 contractItemId를 찾을 수 없습니다`);
+ continue;
+ }
+
+ // 폼이 있는 contractItemId 추적
+ contractItemIdsWithForms.add(contractItemId);
+
+ formsToSave.push({
+ contractItemId,
+ formCode: register.TYPE_ID,
+ formName: register.DESC,
+ eng: true,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ });
+ }
+ }
+
+ // 트랜잭션으로 모든 작업 처리
+ let totalSaved = 0;
+
+ await db.transaction(async (tx) => {
+ // 기존 데이터 삭제
+ await tx.delete(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId));
+ await tx.delete(formMetas).where(eq(formMetas.projectId, projectId));
+
+ // 해당 contractItemId에 대한 기존 폼 삭제
+ if (contractItemIdsWithForms.size > 0) {
+ await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms]));
+ }
+
+ // 매핑 저장
+ if (mappingsToSave.length > 0) {
+ await tx.insert(tagTypeClassFormMappings).values(mappingsToSave);
+ totalSaved += mappingsToSave.length;
+ console.log(`프로젝트 ID ${projectId}에 대해 ${mappingsToSave.length}개의 태그 타입-클래스-폼 매핑을 저장했습니다`);
+ }
+
+ // 폼 메타 저장
+ if (formMetasToSave.length > 0) {
+ await tx.insert(formMetas).values(formMetasToSave);
+ totalSaved += formMetasToSave.length;
+ console.log(`프로젝트 ID ${projectId}에 대해 ${formMetasToSave.length}개의 폼 메타 레코드를 저장했습니다`);
+ }
+
+ // 폼 레코드 저장
+ if (formsToSave.length > 0) {
+ await tx.insert(forms).values(formsToSave);
+ totalSaved += formsToSave.length;
+ console.log(`프로젝트 ID ${projectId}에 대해 ${formsToSave.length}개의 폼 레코드를 저장했습니다`);
+ }
+ });
+
+ return totalSaved;
+ } catch (error) {
+ console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID: ${projectId}):`, error);
+ throw error;
+ }
+}
+
+// 메인 동기화 함수
+export async function syncTagFormMappings() {
+ try {
+ console.log('태그 폼 매핑 동기화 시작:', new Date().toISOString());
+
+ // 모든 프로젝트 가져오기
+ const allProjects = await db.select().from(projects);
+
+ // 각 프로젝트에 대해 폼 매핑 동기화
+ const results = await Promise.allSettled(
+ allProjects.map(async (project: Project) => {
+ try {
+ // 레지스터 데이터 가져오기
+ const registers = await getRegisters(project.code);
+
+ // 데이터베이스에 저장
+ const count = await saveFormMappingsAndMetas(project.id, project.code, registers);
+ return {
+ project: project.code,
+ success: true,
+ count
+ } as SyncResult;
+ } catch (error) {
+ console.error(`프로젝트 ${project.code} 폼 매핑 동기화 실패:`, error);
+ return {
+ project: project.code,
+ success: false,
+ error: error instanceof Error ? error.message : String(error)
+ } as SyncResult;
+ }
+ })
+ );
+
+ // 결과 처리를 위한 배열 준비
+ const successfulResults: SyncResult[] = [];
+ const failedResults: SyncResult[] = [];
+
+ // 결과 분류
+ results.forEach((result) => {
+ if (result.status === 'fulfilled') {
+ if (result.value.success) {
+ successfulResults.push(result.value);
+ } else {
+ failedResults.push(result.value);
+ }
+ } else {
+ // 거부된 프로미스는 실패로 간주
+ failedResults.push({
+ project: 'unknown',
+ success: false,
+ error: result.reason?.toString() || 'Unknown error'
+ });
+ }
+ });
+
+ const successCount = successfulResults.length;
+ const failCount = failedResults.length;
+
+ // 이제 안전하게 count 속성에 접근 가능
+ const totalItems = successfulResults.reduce((sum, result) =>
+ sum + (result.count || 0), 0
+ );
+
+ console.log(`태그 폼 매핑 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`);
+
+ return {
+ success: successCount,
+ failed: failCount,
+ items: totalItems,
+ timestamp: new Date().toISOString()
+ };
+ } catch (error) {
+ console.error('태그 폼 매핑 동기화 중 오류 발생:', error);
+ throw error;
+ }
+} \ No newline at end of file
diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts
index 2cb677b7..c293c98e 100644
--- a/lib/sedp/sync-form.ts
+++ b/lib/sedp/sync-form.ts
@@ -1,6 +1,6 @@
// src/lib/cron/syncTagFormMappings.ts
import db from "@/db/db";
-import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items, contracts } from '@/db/schema';
+import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items, contracts, templateItems } from '@/db/schema';
import { eq, and, inArray, ilike } from 'drizzle-orm';
import { getSEDPToken } from "./sedp-token";
@@ -37,6 +37,51 @@ interface FormRecord {
createdAt: Date;
updatedAt: Date;
}
+
+interface TemplateItem {
+ TMPL_ID: string;
+ NAME: string;
+ TMPL_TYPE: string;
+ SPR_LST_SETUP: {
+ ACT_SHEET: string;
+ HIDN_SHEETS: Array<string>;
+ CONTENT?: string;
+ DATA_SHEETS: Array<{
+ SHEET_NAME: string;
+ REG_TYPE_ID: string;
+ MAP_CELL_ATT: Array<{
+ ATT_ID: string;
+ IN: string;
+ }>;
+ }>;
+ };
+ GRD_LST_SETUP: {
+ REG_TYPE_ID: string;
+ SPR_ITM_IDS: Array<string>;
+ ATTS: Array<{
+ ATT_ID: string;
+ ALIAS: string;
+ HEAD_TEXT: string;
+ HIDN_YN: boolean;
+ SEQ:number;
+ DFLT_YN:boolean;
+ }>;
+ };
+ SPR_ITM_LST_SETUP: {
+ ACT_SHEET: string;
+ HIDN_SHEETS: Array<string>;
+ CONTENT?: string;
+ DATA_SHEETS: Array<{
+ SHEET_NAME: string;
+ REG_TYPE_ID: string;
+ MAP_CELL_ATT: Array<{
+ ATT_ID: string;
+ IN: string;
+ }>;
+ }>;
+ };
+}
+
interface Register {
PROJ_NO: string;
TYPE_ID: string;
@@ -75,6 +120,42 @@ interface LinkAttribute {
UOM_ID: string | null;
}
+interface newRegister {
+ PROJ_NO: string;
+ MAP_ID: string;
+ EP_ID: string;
+ CATEGORY: string;
+ BYPASS: boolean;
+ REG_TYPE_ID: string;
+ TOOL_ID: string;
+ TOOL_TYPE: string;
+ MAP_CLS: {
+ TOOL_ATT_NAME: string;
+ ITEMS: ClassItmes[];
+ };
+ MAP_ATT: MapAttribute[];
+ MAP_TMPLS: string[];
+ CRTER_NO: string;
+ CRTE_DTM: string;
+ CHGER_NO: string;
+ _id: string;
+}
+
+
+
+interface ClassItmes {
+ SEDP_OBJ_CLS_ID: string;
+ TOOL_VALS: string;
+ ISDEFALUT: boolean;
+}
+
+interface MapAttribute {
+ SEDP_ATT_ID: string;
+ TOOL_ATT_NAME: string;
+ KEY_YN: boolean;
+ INOUT: string | null;
+}
+
interface Attribute {
PROJ_NO: string;
ATT_ID: string;
@@ -167,6 +248,9 @@ interface FormColumn {
uom?: string;
uomId?: string;
shi?: Boolean;
+ hidden?: boolean;
+ seq?: number;
+ head?: string;
}
// 아이템 코드 추출 함수
@@ -249,6 +333,63 @@ async function getDefaulTAttributes(): Promise<string[]> {
}
}
+async function fetchTemplateFromSEDP(projectCode: string, formCode: string): Promise<TemplateItem[]> {
+ try {
+ // Get the token
+ const apiKey = await getSEDPToken();
+
+ // Make the API call
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/Template/GetByRegisterID`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ WithContent: true,
+ ProjectNo: projectCode,
+ REG_TYPE_ID: formCode
+ })
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ console.warn(`템플릿을 찾을 수 없음: ${formCode}`);
+ return [];
+ }
+ const errorText = await response.text();
+ throw new Error(`SEDP Template API request failed: ${response.status} ${response.statusText} - ${errorText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ try {
+ const data = await response.json();
+ // 데이터가 배열인지 확인
+ const templates: TemplateItem[] = Array.isArray(data) ? data : [data];
+ return templates.filter(template => template && template.TMPL_ID);
+ } catch (parseError) {
+ console.error(`템플릿 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ try {
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ } catch (textError) {
+ console.error('응답 내용 로깅 실패:', textError);
+ }
+ return [];
+ }
+ } catch (error: any) {
+ console.error('Error calling SEDP Template API:', error);
+ console.warn(`템플릿 가져오기 실패 (${formCode}): ${error.message || 'Unknown error'}`);
+ return [];
+ }
+}
+
// 레지스터 데이터 가져오기
async function getRegisters(projectCode: string): Promise<Register[]> {
try {
@@ -315,6 +456,55 @@ async function getRegisters(projectCode: string): Promise<Register[]> {
}
}
+async function getNewRegisters(projectCode: string): Promise<newRegister[]> {
+ try {
+ // 토큰(API 키) 가져오기
+ const apiKey = await getSEDPToken();
+
+ const response = await fetch(
+ `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ "TOOL_ID": "eVCP"
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`새 레지스터 요청 실패: ${response.status} ${response.statusText}`);
+ }
+
+ // 안전하게 JSON 파싱
+ let data;
+ try {
+ data = await response.json();
+ } catch (parseError) {
+ console.error(`프로젝트 ${projectCode}의 새 레지스터 응답 파싱 실패:`, parseError);
+ // 응답 내용 로깅
+ const text = await response.clone().text();
+ console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`);
+ throw new Error(`새 레지스터 응답 파싱 실패: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
+ }
+
+ // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑)
+ let registers: newRegister[] = Array.isArray(data) ? data : [data];
+
+ console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 새 레지스터를 가져왔습니다.`);
+ return registers;
+ } catch (error) {
+ console.error(`프로젝트 ${projectCode}의 새 레지스터 가져오기 실패:`, error);
+ throw error;
+ }
+}
+
// 프로젝트의 모든 속성을 가져와 맵으로 반환
async function getAttributes(projectCode: string): Promise<Map<string, Attribute>> {
try {
@@ -716,249 +906,167 @@ async function getContractItemsByItemCodes(itemCodes: string[], projectId: numbe
}
}
-// 데이터베이스에 태그 타입 클래스 폼 매핑 및 폼 메타 저장
-async function saveFormMappingsAndMetas(
+// UPDATED: saveFormMappingsAndMetas()
+// ------------------------------------
+// Primary loop is **newRegisters**‑first; legacy **registers** are consulted
+// only for supplemental details.
+//
+// 2025‑07‑09 fix: newRegister.MAP_CLS is a **single object**, not an array.
+// Updated class‑ID extraction accordingly.
+//
+export async function saveFormMappingsAndMetas(
projectId: number,
projectCode: string,
- registers: Register[]
+ registers: Register[], // legacy SEDP Register list (supplemental)
+ newRegisters: newRegister[] // AdapterDataMapping list (primary)
): Promise<number> {
try {
- // 프로젝트의 태그 타입과 클래스 가져오기
- const tagTypeRecords = await db.select()
- .from(tagTypes)
- .where(eq(tagTypes.projectId, projectId));
+ /* ------------------------------------------------------------------ */
+ /* 1. Prepare look‑up structures & common data */
+ /* ------------------------------------------------------------------ */
- const tagClassRecords = await db.select()
- .from(tagClasses)
- .where(eq(tagClasses.projectId, projectId));
+ const tagTypeRecords = await db.select().from(tagTypes).where(eq(tagTypes.projectId, projectId));
+ const tagClassRecords = await db.select().from(tagClasses).where(eq(tagClasses.projectId, projectId));
+ const tagTypeMap = new Map(tagTypeRecords.map(t => [t.code, t]));
+ const tagClassMap = new Map(tagClassRecords.map(c => [c.code, c]));
- // 태그 타입과 클래스 매핑
- const tagTypeMap = new Map(tagTypeRecords.map(type => [type.code, type]));
- const tagClassMap = new Map(tagClassRecords.map(cls => [cls.code, cls]));
+ const registerMap = new Map(registers.map(r => [r.TYPE_ID, r]));
- // 모든 속성, 코드 리스트, UOM을 한 번에 가져와 반복 API 호출 방지
const attributeMap = await getAttributes(projectCode);
- const codeListMap = await getCodeLists(projectCode);
- const uomMap = await getUOMs(projectCode);
-
- // 기본 속성 가져오기
+ const codeListMap = await getCodeLists(projectCode);
+ const uomMap = await getUOMs(projectCode);
const defaultAttributes = await getDefaulTAttributes();
- // 모든 register에서 itemCode를 추출하여 한 번에 조회
- const allItemCodes: string[] = [];
- registers.forEach(register => {
- if (register.REMARK) {
- const itemCodes = extractItemCodes(register.REMARK);
- allItemCodes.push(...itemCodes);
- }
- });
-
- // 중복 제거
- const uniqueItemCodes = [...new Set(allItemCodes)];
-
- // 모든 itemCode에 대한 contractItemId 조회
- const itemCodeToContractItemId = await getContractItemsByItemCodes(uniqueItemCodes , projectId);
-
- console.log(`${uniqueItemCodes.length}개의 고유 itemCode 중 ${itemCodeToContractItemId.size}개의 contractItem을 찾았습니다`);
+ /* ------------------------------------------------------------------ */
+ /* 2. Contract‑item look‑up (TOOL_TYPE) */
+ /* ------------------------------------------------------------------ */
+ const uniqueItemCodes = [...new Set(newRegisters.filter(nr => nr.TOOL_TYPE).map(nr => nr.TOOL_TYPE as string))];
+ const itemCodeToContractItemId = await getContractItemsByItemCodes(uniqueItemCodes, projectId);
- // 저장할 데이터 준비
+ /* ------------------------------------------------------------------ */
+ /* 3. Buffers for bulk insert */
+ /* ------------------------------------------------------------------ */
const mappingsToSave: TagTypeClassFormMapping[] = [];
const formMetasToSave: FormMeta[] = [];
const formsToSave: FormRecord[] = [];
-
- // 폼이 있는 contractItemId 트래킹
const contractItemIdsWithForms = new Set<number>();
+ const templateDataByFormCode: Map<string, TemplateItem[]> = new Map();
- // 각 register 처리
- for (const register of registers) {
- // 삭제된 register 건너뛰기
- if (register.DELETED) continue;
+ /* ------------------------------------------------------------------ */
+ /* 4. Iterate over newRegisters */
+ /* ------------------------------------------------------------------ */
+ for (const newReg of newRegisters) {
+ const formCode = newReg.REG_TYPE_ID;
+ const legacy = registerMap.get(formCode);
- // REMARK에서 itemCodes 추출
+ /* ---------- 4‑a. templates ------------------------------------ */
+ let templates: TemplateItem[] = [];
+ try {
+ const fetched = await fetchTemplateFromSEDP(projectCode, formCode);
+ templates = fetched.filter(t => (newReg.MAP_TMPLS?.length ? newReg.MAP_TMPLS.includes(t.TMPL_ID) : true));
+ if (templates.length) templateDataByFormCode.set(formCode, templates);
+ } catch (e) {
+ console.warn(`템플릿 가져오기 실패 (${formCode})`, e);
+ }
+ const templateAttrMap = new Map<string, { hidden: boolean; seq: number; head: string }>();
+ templates.forEach(t => t.GRD_LST_SETUP?.ATTS?.forEach(att => {
+ if (!templateAttrMap.has(att.ATT_ID)) templateAttrMap.set(att.ATT_ID, { hidden: att.HIDN_YN, seq: att.SEQ, head: att.HEAD_TEXT });
+ }));
- // 폼 메타용 columns 구성
+ /* ---------- 4‑b. columns -------------------------------------- */
const columns: FormColumn[] = [];
-
- for (const linkAtt of register.LNK_ATT) {
- let attribute = null;
-
- // 기본 속성인지 확인
- if (defaultAttributes && defaultAttributes.includes(linkAtt.ATT_ID)) {
- // 기본 속성에 대한 기본 attribute 객체 생성
- attribute = {
- DESC: linkAtt.ATT_ID,
- VAL_TYPE: 'STRING'
- };
- } else {
- // 맵에서 속성 조회
- attribute = attributeMap.get(linkAtt.ATT_ID);
-
- // 속성을 찾지 못한 경우 다음으로 넘어감
- if (!attribute) continue;
+ for (const mapAtt of newReg.MAP_ATT) {
+ const attId = mapAtt.SEDP_ATT_ID;
+ const attribute = defaultAttributes.includes(attId) ? { DESC: attId, VAL_TYPE: "STRING" } as Partial<Attribute> : attributeMap.get(attId);
+ if (!attribute) continue;
+
+ const tmplMeta = templateAttrMap.get(attId);
+ const isShi = mapAtt.INOUT === "OUT";
+
+ let uomSymbol: string | undefined; let uomId: string | undefined;
+ if (legacy?.LNK_ATT) {
+ const l = legacy.LNK_ATT.find(a => a.ATT_ID === attId);
+ if (l?.UOM_ID) { const u = uomMap.get(l.UOM_ID); if (u) { uomSymbol = u.SYMBOL; uomId = u.UOM_ID; } }
}
- // 컬럼 정보 생성
- const column: FormColumn = {
- key: linkAtt.ATT_ID,
- label: attribute.DESC,
- type: (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST')
- ? 'LIST'
- : (attribute.VAL_TYPE || 'STRING'),
- shi: attribute.REMARK?.toLocaleLowerCase() === "shi"
+ const col: FormColumn = {
+ key: attId,
+ label: attribute.DESC as string,
+ type: (attribute.VAL_TYPE === "LIST" || attribute.VAL_TYPE === "DYNAMICLIST") ? "LIST" : (attribute.VAL_TYPE || "STRING"),
+ shi: isShi,
+ hidden: tmplMeta?.hidden ?? false,
+ seq: tmplMeta?.seq ?? 0,
+ head: tmplMeta?.head ?? "",
+ ...(uomSymbol ? { uom: uomSymbol, uomId } : {})
};
- // 리스트 타입에 대한 옵션 추가 (기본 속성이 아닌 경우)
- if (!defaultAttributes.includes(linkAtt.ATT_ID) &&
- (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') &&
- attribute.CL_ID) {
-
- // 맵에서 코드 리스트 조회
- const codeList = codeListMap.get(attribute.CL_ID);
-
- if (codeList && codeList.VALUES) {
- const options = [...new Set(
- codeList.VALUES
- .filter(value => value.USE_YN)
- .map(value => value.VALUE)
- )];
-
- if (options.length > 0) {
- column.options = options;
- }
- }
+ if (!defaultAttributes.includes(attId) && (attribute.VAL_TYPE === "LIST" || attribute.VAL_TYPE === "DYNAMICLIST") && attribute.CL_ID) {
+ const cl = codeListMap.get(attribute.CL_ID);
+ if (cl?.VALUES?.length) col.options = [...new Set(cl.VALUES.filter(v => v.USE_YN).map(v => v.VALUE))];
}
- // UOM 정보 추가
- if (linkAtt.UOM_ID) {
- const uom = uomMap.get(linkAtt.UOM_ID);
-
- if (uom) {
- column.uom = uom.SYMBOL;
- column.uomId = uom.UOM_ID;
- }
- }
-
- columns.push(column);
- }
-
- // 컬럼이 없으면 건너뛰기
- if (columns.length === 0) {
- console.log(`폼 ${register.TYPE_ID} (${register.DESC})에 컬럼이 없어 건너뜁니다`);
- continue;
+ columns.push(col);
}
+ if (!columns.length) { console.log(`폼 ${formCode} 건너뜀 (컬럼 없음)`); continue; }
+ columns.sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999));
- // 폼 메타 데이터 준비
- formMetasToSave.push({
- projectId,
- formCode: register.TYPE_ID,
- formName: register.DESC,
- columns: JSON.stringify(columns),
- createdAt: new Date(),
- updatedAt: new Date()
- });
-
- // 클래스 매핑 처리
- for (const classId of register.MAP_CLS_ID) {
- const tagClass = tagClassMap.get(classId);
-
- if (!tagClass) {
- console.warn(`프로젝트 ID ${projectId}에서 클래스 ID ${classId}를 찾을 수 없습니다`);
- continue;
- }
-
- const tagTypeCode = tagClass.tagTypeCode;
- const tagType = tagTypeMap.get(tagTypeCode);
-
- if (!tagType) {
- console.warn(`프로젝트 ID ${projectId}에서 태그 타입 ${tagTypeCode}를 찾을 수 없습니다`);
- continue;
- }
+ formMetasToSave.push({ projectId, formCode, formName: legacy?.DESC || formCode, columns: JSON.stringify(columns), createdAt: new Date(), updatedAt: new Date() });
- // 매핑 정보 저장
- mappingsToSave.push({
- projectId,
- tagTypeLabel: tagType.description,
- classLabel: tagClass.label,
- formCode: register.TYPE_ID,
- formName: register.DESC,
- remark: register.REMARK,
- ep: register.EP_ID,
- createdAt: new Date(),
- updatedAt: new Date()
- });
+ /* ---------- 4‑c. class mappings -------------------------------- */
+ const classIds = new Set<string>();
+ if (newReg.MAP_CLS?.ITEMS?.length) {
+ newReg.MAP_CLS.ITEMS.forEach(it => classIds.add(it.SEDP_OBJ_CLS_ID));
}
- const itemCodes = extractItemCodes(register.REMARK || '');
- if (!itemCodes.length) {
- console.log(`Register ${register.TYPE_ID} (${register.DESC})의 REMARK에 유효한 itemCode가 없습니다`);
- continue;
- }
- // 폼 레코드 준비
- for (const itemCode of itemCodes) {
- const contractItemId = itemCodeToContractItemId.get(itemCode);
-
- if (!contractItemId) {
- console.warn(`itemCode: ${itemCode}에 대한 contractItemId를 찾을 수 없습니다`);
- continue;
- }
-
- // 폼이 있는 contractItemId 추적
- contractItemIdsWithForms.add(contractItemId);
+ classIds.forEach(classId => {
+ const cls = tagClassMap.get(classId);
+ if (!cls) { console.warn(`클래스 ${classId} 없음`); return; }
+ const tp = tagTypeMap.get(cls.tagTypeCode);
+ if (!tp) { console.warn(`태그 타입 ${cls.tagTypeCode} 없음`); return; }
+ mappingsToSave.push({ projectId, tagTypeLabel: tp.description, classLabel: cls.label, formCode, formName: legacy?.DESC || formCode, remark: newReg.TOOL_TYPE || null, ep: newReg.EP_ID || legacy?.EP_ID || "", createdAt: new Date(), updatedAt: new Date() });
+ });
- formsToSave.push({
- contractItemId,
- formCode: register.TYPE_ID,
- formName: register.DESC,
- eng: true,
- createdAt: new Date(),
- updatedAt: new Date()
- });
+ /* ---------- 4‑d. contractItem ↔ form --------------------------- */
+ if (newReg.TOOL_TYPE) {
+ const cId = itemCodeToContractItemId.get(newReg.TOOL_TYPE);
+ if (cId) { contractItemIdsWithForms.add(cId); formsToSave.push({ contractItemId: cId, formCode, formName: legacy?.DESC || formCode, eng: true, createdAt: new Date(), updatedAt: new Date() }); }
+ else console.warn(`itemCode ${newReg.TOOL_TYPE} 의 contractItemId 없음`);
}
}
- // 트랜잭션으로 모든 작업 처리
+ /* ------------------------------------------------------------------ */
+ /* 5. DB transaction */
+ /* ------------------------------------------------------------------ */
let totalSaved = 0;
-
- await db.transaction(async (tx) => {
- // 기존 데이터 삭제
+ await db.transaction(async tx => {
+ const old = await tx.select({ id: tagTypeClassFormMappings.id }).from(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId));
+ if (old.length) await tx.delete(templateItems).where(inArray(templateItems.formMappingId, old.map(o => o.id)));
await tx.delete(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId));
await tx.delete(formMetas).where(eq(formMetas.projectId, projectId));
+ if (contractItemIdsWithForms.size) await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms]));
- // 해당 contractItemId에 대한 기존 폼 삭제
- if (contractItemIdsWithForms.size > 0) {
- await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms]));
- }
-
- // 매핑 저장
- if (mappingsToSave.length > 0) {
- await tx.insert(tagTypeClassFormMappings).values(mappingsToSave);
- totalSaved += mappingsToSave.length;
- console.log(`프로젝트 ID ${projectId}에 대해 ${mappingsToSave.length}개의 태그 타입-클래스-폼 매핑을 저장했습니다`);
- }
+ const savedMappings = mappingsToSave.length ? await tx.insert(tagTypeClassFormMappings).values(mappingsToSave).returning({ id: tagTypeClassFormMappings.id, formCode: tagTypeClassFormMappings.formCode }) : [];
+ totalSaved += mappingsToSave.length;
- // 폼 메타 저장
- if (formMetasToSave.length > 0) {
- await tx.insert(formMetas).values(formMetasToSave);
- totalSaved += formMetasToSave.length;
- console.log(`프로젝트 ID ${projectId}에 대해 ${formMetasToSave.length}개의 폼 메타 레코드를 저장했습니다`);
+ if (savedMappings.length) {
+ const rows: any[] = [];
+ savedMappings.forEach(m => (templateDataByFormCode.get(m.formCode) || []).forEach(t => rows.push({ formMappingId: m.id, tmplId: t.TMPL_ID, name: t.NAME, tmplType: t.TMPL_TYPE, sprLstSetup: t.SPR_LST_SETUP, grdLstSetup: t.GRD_LST_SETUP, sprItmLstSetup: t.SPR_ITM_LST_SETUP, description: `Template for form ${m.formCode}`, isActive: true, createdAt: new Date(), updatedAt: new Date() })));
+ if (rows.length) { await tx.insert(templateItems).values(rows); totalSaved += rows.length; }
}
- // 폼 레코드 저장
- if (formsToSave.length > 0) {
- await tx.insert(forms).values(formsToSave);
- totalSaved += formsToSave.length;
- console.log(`프로젝트 ID ${projectId}에 대해 ${formsToSave.length}개의 폼 레코드를 저장했습니다`);
- }
+ if (formMetasToSave.length) { await tx.insert(formMetas).values(formMetasToSave); totalSaved += formMetasToSave.length; }
+ if (formsToSave.length) { await tx.insert(forms).values(formsToSave); totalSaved += formsToSave.length; }
});
return totalSaved;
- } catch (error) {
- console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID: ${projectId}):`, error);
- throw error;
+ } catch (err) {
+ console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID:${projectId})`, err);
+ throw err;
}
}
+
// 메인 동기화 함수
export async function syncTagFormMappings() {
try {
@@ -973,9 +1081,10 @@ export async function syncTagFormMappings() {
try {
// 레지스터 데이터 가져오기
const registers = await getRegisters(project.code);
+ const newRegisters = await getNewRegisters(project.code);
// 데이터베이스에 저장
- const count = await saveFormMappingsAndMetas(project.id, project.code, registers);
+ const count = await saveFormMappingsAndMetas(project.id, project.code, registers, newRegisters);
return {
project: project.code,
success: true,