summaryrefslogtreecommitdiff
path: root/lib/sedp/sync-form copy.ts
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/sync-form copy.ts
parent44794a8628997c0d979adb5bd6711cd848b3e397 (diff)
(대표님) 20250709 변경사항 (약 18시 30분까지)
Diffstat (limited to 'lib/sedp/sync-form copy.ts')
-rw-r--r--lib/sedp/sync-form copy.ts1037
1 files changed, 1037 insertions, 0 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