import db from "@/db/db"; import { projects, tagClassAttributes, tagClasses, tagTypes } from '@/db/schema'; import { eq, and } 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 SubclassInfo { id: string; desc: string; } // ObjectClass 인터페이스 정의 interface ObjectClass { PROJ_NO: string; CLS_ID: string; DESC: string; TAG_TYPE_ID: string | null; PRT_CLS_ID: string | null; LNK_ATT: LinkAttribute[]; DELETED: boolean; DEL_USER: string | null; DEL_DTM: string | null; 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 TagType { TYPE_ID: string; DESC: string; PROJ_NO: string; // 기타 필드들... } interface LinkAttribute { ATT_ID: string; DEF_VAL: string; UOM_ID: string; SEQ: number; } interface SubClassCodeValue { PRNT_VALUE: string; VALUE: string; DESC: string; REMARK: string; USE_YN: boolean; SEQ: number; ATTRIBUTES: Array<{ ATT_ID: string; VALUE: string; }>; } /** * 프로젝트별 CodeList(클래스 코드 값) 조회 */ export async function getCodeListsByID(projectCode: string): Promise { try { // 1) 토큰(API 키) 발급 const apiKey = await getSEDPToken(); // 2) API 호출 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: 'EVCP_TAG_FUNC', ContainDeleted: false, }), }); if (!response.ok) { // 네트워크·인증 오류 등 throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); } // 3) 응답 파싱 const { VALUES = [] } = (await response.json()) as { VALUES?: SubClassCodeValue[] }; return VALUES; // 정상 반환 } catch (err) { // 4) 파싱 오류·기타 예외 console.error(`프로젝트 ${projectCode}의 코드 리스트 가져오기 실패:`, err); return []; } } function collectInheritedAttributes( classId: string, allClasses: ObjectClass[] ): LinkAttribute[] { const classMap = new Map(allClasses.map(cls => [cls.CLS_ID, cls])); const collectedAttributes: LinkAttribute[] = []; const seenAttributeIds = new Set(); // 현재 클래스부터 시작해서 부모를 따라 올라가며 속성 수집 function collectFromClass(currentClassId: string | null): void { if (!currentClassId) return; const currentClass = classMap.get(currentClassId); if (!currentClass) return; // 먼저 부모 클래스의 속성을 수집 (상위부터 하위 순서로) if (currentClass.PRT_CLS_ID) { collectFromClass(currentClass.PRT_CLS_ID); } // 현재 클래스의 LNK_ATT 추가 (중복 제거) if (currentClass.LNK_ATT && Array.isArray(currentClass.LNK_ATT)) { for (const attr of currentClass.LNK_ATT) { if (!seenAttributeIds.has(attr.ATT_ID)) { seenAttributeIds.add(attr.ATT_ID); collectedAttributes.push(attr); } } } } collectFromClass(classId); // SEQ 순서로 정렬 return collectedAttributes.sort((a, b) => (a.SEQ || 0) - (b.SEQ || 0)); } // 태그 클래스 속성 저장 함수 async function saveTagClassAttributes( tagClassId: number, attributes: LinkAttribute[] ): Promise { try { if (attributes.length === 0) { console.log(`태그 클래스 ID ${tagClassId}에 저장할 속성이 없습니다.`); return; } // 현재 태그 클래스의 모든 속성 조회 const existingAttributes = await db.select() .from(tagClassAttributes) .where(eq(tagClassAttributes.tagClassId, tagClassId)); // 속성 ID 기준으로 맵 생성 const existingAttributeMap = new Map( existingAttributes.map(attr => [attr.attId, attr]) ); // API에 있는 속성 ID 목록 const apiAttributeIds = new Set(attributes.map(attr => attr.ATT_ID)); // 삭제할 속성 ID 목록 const attributeIdsToDelete = existingAttributes .map(attr => attr.attId) .filter(attId => !apiAttributeIds.has(attId)); // 새로 추가할 항목과 업데이트할 항목 분리 const toInsert = []; const toUpdate = []; for (const attr of attributes) { const record = { tagClassId: tagClassId, attId: attr.ATT_ID, defVal: attr.DEF_VAL, uomId: attr.UOM_ID, seq: attr.SEQ, updatedAt: new Date() }; if (existingAttributeMap.has(attr.ATT_ID)) { // 업데이트 항목 toUpdate.push(record); } else { // 새로 추가할 항목 toInsert.push({ ...record, createdAt: new Date() }); } } // 1. 새 항목 삽입 if (toInsert.length > 0) { await db.insert(tagClassAttributes).values(toInsert); console.log(`태그 클래스 ID ${tagClassId}에 ${toInsert.length}개의 새 속성 추가 완료`); } // 2. 기존 항목 업데이트 for (const item of toUpdate) { await db.update(tagClassAttributes) .set({ defVal: item.defVal, uomId: item.uomId, seq: item.seq, updatedAt: item.updatedAt }) .where( and( eq(tagClassAttributes.tagClassId, item.tagClassId), eq(tagClassAttributes.attId, item.attId) ) ); } if (toUpdate.length > 0) { console.log(`태그 클래스 ID ${tagClassId}의 ${toUpdate.length}개 속성 업데이트 완료`); } // 3. 더 이상 존재하지 않는 항목 삭제 if (attributeIdsToDelete.length > 0) { for (const attId of attributeIdsToDelete) { await db.delete(tagClassAttributes) .where( and( eq(tagClassAttributes.tagClassId, tagClassId), eq(tagClassAttributes.attId, attId) ) ); } console.log(`태그 클래스 ID ${tagClassId}에서 ${attributeIdsToDelete.length}개의 속성 삭제 완료`); } } catch (error) { console.error(`태그 클래스 속성 저장 실패 (태그 클래스 ID: ${tagClassId}):`, error); throw error; } } // 오브젝트 클래스 데이터 가져오기 async function getObjectClasses(projectCode: string, token:string): Promise { try { const response = await fetch( `${SEDP_API_BASE_URL}/ObjectClass/Get`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': token, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo:projectCode, ContainDeleted: false }) } ); if (!response.ok) { throw new Error(`오브젝트 클래스 요청 실패: ${response.status} ${response.statusText}`); } const data = await response.json(); // 결과가 배열인지 확인 if (Array.isArray(data)) { return data; } else { // 단일 객체인 경우 배열로 변환 return [data]; } } catch (error) { console.error(`프로젝트 ${projectCode}의 오브젝트 클래스 가져오기 실패:`, error); throw error; } } // 태그 타입 존재 확인 async function verifyTagTypes(projectId: number, tagTypeCodes: string[]): Promise> { try { // 프로젝트에 있는 태그 타입 코드 조회 const existingTagTypes = await db.select({ code: tagTypes.code }) .from(tagTypes) .where(eq(tagTypes.projectId, projectId)); // 존재하는 태그 타입 코드 Set으로 반환 return new Set(existingTagTypes.map(type => type.code)); } catch (error) { console.error(`프로젝트 ID ${projectId}의 태그 타입 확인 실패:`, error); throw error; } } async function saveTagTypesToDatabase(allTagTypes: TagType[], projectCode: string): Promise { try { if (allTagTypes.length === 0) { console.log(`프로젝트 ${projectCode}에 저장할 태그 타입이 없습니다.`); return; } // 프로젝트 코드로 프로젝트 ID 조회 const projectResult = await db.select({ id: projects.id }) .from(projects) .where(eq(projects.code, projectCode)) .limit(1); if (projectResult.length === 0) { throw new Error(`프로젝트 코드 ${projectCode}에 해당하는 프로젝트를 찾을 수 없습니다.`); } const projectId = projectResult[0].id; // 현재 프로젝트의 모든 태그 타입 조회 const existingTagTypes = await db.select() .from(tagTypes) .where(eq(tagTypes.projectId, projectId)); // 코드 기준으로 맵 생성 const existingTagTypeMap = new Map( existingTagTypes.map(type => [type.code, type]) ); // API에 있는 코드 목록 const apiTagTypeCodes = new Set(allTagTypes.map(type => type.TYPE_ID)); // 삭제할 코드 목록 const codesToDelete = existingTagTypes .map(type => type.code) .filter(code => !apiTagTypeCodes.has(code)); // 새로 추가할 항목 const toInsert = []; // 업데이트할 항목 const toUpdate = []; // 태그 타입 데이터 처리 for (const tagType of allTagTypes) { // 데이터베이스 레코드 준비 const record = { code: tagType.TYPE_ID, projectId: projectId, description: tagType.DESC, updatedAt: new Date() }; // 이미 존재하는 코드인지 확인 if (existingTagTypeMap.has(tagType.TYPE_ID)) { // 업데이트 항목에 추가 toUpdate.push(record); } else { // 새로 추가할 항목에 추가 (createdAt 필드 추가) toInsert.push({ ...record, createdAt: new Date() }); } } // 트랜잭션 실행 // 1. 새 항목 삽입 if (toInsert.length > 0) { await db.insert(tagTypes).values(toInsert); console.log(`프로젝트 ID ${projectId}에 ${toInsert.length}개의 새 태그 타입 추가 완료`); } // 2. 기존 항목 업데이트 for (const item of toUpdate) { await db.update(tagTypes) .set({ description: item.description, updatedAt: item.updatedAt }) .where( and( eq(tagTypes.code, item.code), eq(tagTypes.projectId, item.projectId) ) ); } if (toUpdate.length > 0) { console.log(`프로젝트 ID ${projectId}의 ${toUpdate.length}개 태그 타입 업데이트 완료`); } // 3. 더 이상 존재하지 않는 항목 삭제 if (codesToDelete.length > 0) { for (const code of codesToDelete) { await db.delete(tagTypes) .where( and( eq(tagTypes.code, code), eq(tagTypes.projectId, projectId) ) ); } console.log(`프로젝트 ID ${projectId}에서 ${codesToDelete.length}개의 태그 타입 삭제 완료`); } console.log(`프로젝트 ${projectCode}(ID: ${projectId})의 태그 타입 동기화 완료`); } catch (error) { console.error(`태그 타입 저장 실패 (프로젝트: ${projectCode}):`, error); throw error; } } async function getAllTagTypes(projectCode: string, token: string): Promise { try { console.log(`프로젝트 ${projectCode}의 모든 태그 타입 가져오기 시작`); const response = await fetch( `${SEDP_API_BASE_URL}/TagType/Get`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': token, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, ContainDeleted: false }) } ); if (!response.ok) { throw new Error(`태그 타입 요청 실패: ${response.status} ${response.statusText}`); } const data = await response.json(); // 결과가 배열인지 확인 if (Array.isArray(data)) { return data; } else { // 단일 객체인 경우 배열로 변환 return [data]; } } catch (error) { console.error('태그 타입 목록 가져오기 실패:', error); throw error; } } // 수정된 함수: ID와 DESC을 함께 반환 function findSubclasses(parentCode: string, allClasses: ObjectClass[]): SubclassInfo[] { return allClasses .filter(cls => cls.PRT_CLS_ID === parentCode) .map(cls => ({ id: cls.CLS_ID, desc: cls.DESC })); } // 서브클래스별 리마크 가져오기 (수정됨) async function getSubclassRemarks( subclasses: SubclassInfo[], projectCode: string ): Promise> { try { if (subclasses.length === 0) { return {}; } // getCodeListsByID로 코드 리스트 가져오기 const codeValues = await getCodeListsByID(projectCode); if (!Array.isArray(codeValues)) { console.log(`프로젝트 ${projectCode}의 코드 리스트가 배열이 아닙니다.`); return {}; } // 서브클래스별 리마크 매핑 const remarkMap: Record = {}; for (const subclass of subclasses) { // VALUE가 서브클래스 ID와 일치하는 항목 찾기 const matchedValue = codeValues.find(value => value.VALUE === subclass.id); if (matchedValue && matchedValue.REMARK) { remarkMap[subclass.id] = matchedValue.REMARK; } else { // REMARK가 없는 경우 빈 문자열 또는 기본값 remarkMap[subclass.id] = ''; } } return remarkMap; } catch (error) { console.error(`서브클래스 리마크 가져오기 실패 (프로젝트: ${projectCode}):`, error); return {}; } } // LNK_ATT 속성 처리가 포함된 오브젝트 클래스 저장 함수 (수정됨) async function saveObjectClassesToDatabase( projectId: number, classes: ObjectClass[], projectCode: string, token: string, skipTagTypeSync: boolean = false ): Promise { try { // null이 아닌 TAG_TYPE_ID만 필터링 const validClasses = classes.filter(cls => cls.TAG_TYPE_ID !== null && cls.TAG_TYPE_ID !== ""); if (validClasses.length === 0) { console.log(`프로젝트 ID ${projectId}에 저장할 유효한 오브젝트 클래스가 없습니다.`); return 0; } // 태그 타입 동기화 (기존 로직 유지) if (!skipTagTypeSync) { console.log(`프로젝트 ID ${projectId}의 태그 타입 동기화 시작...`); try { const allTagTypes = await getAllTagTypes(projectCode, token); await saveTagTypesToDatabase(allTagTypes, projectCode); } catch (error) { console.error(`프로젝트 ${projectCode}의 태그 타입 동기화 실패:`, error); } console.log(`프로젝트 ID ${projectId}의 태그 타입 동기화 완료`); } // 존재하는 태그 타입 확인 const tagTypeCodes = validClasses.map(cls => cls.TAG_TYPE_ID!); const existingTagTypeCodes = await verifyTagTypes(projectId, tagTypeCodes); // 태그 타입이 존재하는 오브젝트 클래스만 필터링 const classesToSave = validClasses.filter(cls => cls.TAG_TYPE_ID !== null && existingTagTypeCodes.has(cls.TAG_TYPE_ID) ); if (classesToSave.length === 0) { console.log(`프로젝트 ID ${projectId}에 저장할 유효한 오브젝트 클래스가 없습니다.`); return 0; } // 현재 프로젝트의 모든 오브젝트 클래스 가져오기 const existingClasses = await db.select() .from(tagClasses) .where(eq(tagClasses.projectId, projectId)); // 코드 기준으로 맵 생성 const existingClassMap = new Map( existingClasses.map(cls => [cls.code, cls]) ); // 새로 추가할 항목과 업데이트할 항목 분리 const toInsert = []; const toUpdate = []; // API에 있는 코드 목록 const apiClassCodes = new Set(classesToSave.map(cls => cls.CLS_ID)); // 삭제할 코드 목록 const codesToDelete = existingClasses .map(cls => cls.code) .filter(code => !apiClassCodes.has(code)); // 각 클래스별로 서브클래스와 리마크 처리 for (const cls of classesToSave) { // 서브클래스 찾기 (이제 {id, desc} 형태로 반환) const subclasses = findSubclasses(cls.CLS_ID, classes); // 서브클래스별 리마크 가져오기 const subclassRemark = await getSubclassRemarks(subclasses, projectCode); const record = { code: cls.CLS_ID, projectId: projectId, label: cls.DESC, tagTypeCode: cls.TAG_TYPE_ID!, subclasses: subclasses, // 이제 {id, desc}[] 형태 subclassRemark: subclassRemark, updatedAt: new Date() }; if (existingClassMap.has(cls.CLS_ID)) { toUpdate.push(record); } else { toInsert.push({ ...record, createdAt: new Date() }); } } let totalChanged = 0; // 새 항목 삽입 if (toInsert.length > 0) { const insertedClasses = await db.insert(tagClasses) .values(toInsert) .returning({ id: tagClasses.id, code: tagClasses.code }); totalChanged += toInsert.length; console.log(`프로젝트 ID ${projectId}에 ${toInsert.length}개의 새 오브젝트 클래스 추가 완료`); // 새로 삽입된 각 클래스의 상속된 LNK_ATT 속성 처리 for (const insertedClass of insertedClasses) { try { // 🔥 수정된 부분: 상속된 속성 수집 const inheritedAttributes = collectInheritedAttributes(insertedClass.code, classes); if (inheritedAttributes.length > 0) { await saveTagClassAttributes(insertedClass.id, inheritedAttributes); console.log(`태그 클래스 ${insertedClass.code}에 ${inheritedAttributes.length}개의 상속된 속성 저장 완료`); } } catch (error) { console.error(`태그 클래스 ${insertedClass.code}의 속성 저장 실패:`, error); } } } // 기존 항목 업데이트 for (const item of toUpdate) { await db.update(tagClasses) .set({ label: item.label, tagTypeCode: item.tagTypeCode, subclasses: item.subclasses, subclassRemark: item.subclassRemark, updatedAt: item.updatedAt }) .where( and( eq(tagClasses.code, item.code), eq(tagClasses.projectId, item.projectId) ) ); // 업데이트된 클래스의 속성 처리 const updatedClass = await db.select({ id: tagClasses.id }) .from(tagClasses) .where( and( eq(tagClasses.code, item.code), eq(tagClasses.projectId, item.projectId) ) ) .limit(1); if (updatedClass.length > 0) { try { // 🔥 수정된 부분: 상속된 속성 수집 const inheritedAttributes = collectInheritedAttributes(item.code, classes); await saveTagClassAttributes(updatedClass[0].id, inheritedAttributes); console.log(`태그 클래스 ${item.code}에 ${inheritedAttributes.length}개의 상속된 속성 업데이트 완료`); } catch (error) { console.error(`태그 클래스 ${item.code}의 속성 업데이트 실패:`, error); } } totalChanged += 1; } if (toUpdate.length > 0) { console.log(`프로젝트 ID ${projectId}의 ${toUpdate.length}개 오브젝트 클래스 업데이트 완료`); } // 더 이상 존재하지 않는 항목 삭제 if (codesToDelete.length > 0) { for (const code of codesToDelete) { await db.delete(tagClasses) .where( and( eq(tagClasses.code, code), eq(tagClasses.projectId, projectId) ) ); } console.log(`프로젝트 ID ${projectId}에서 ${codesToDelete.length}개의 오브젝트 클래스 삭제 완료`); totalChanged += codesToDelete.length; } return totalChanged; } catch (error) { console.error(`오브젝트 클래스 저장 실패 (프로젝트 ID: ${projectId}):`, error); throw error; } } // 메인 동기화 함수 export async function syncObjectClasses() { try { console.log('오브젝트 클래스 동기화 시작:', new Date().toISOString()); // 1. 토큰 가져오기 const token = await getSEDPToken(); // 2. 모든 프로젝트 가져오기 const allProjects = await db.select().from(projects); // 3. 모든 프로젝트에 대해 먼저 태그 타입 동기화 console.log('모든 프로젝트의 태그 타입 동기화 시작...'); const tagTypeResults = await Promise.allSettled( allProjects.map(async (project: Project) => { try { console.log(`프로젝트 ${project.code}의 태그 타입 동기화 시작...`); // 프로젝트의 모든 태그 타입 가져오기 const allTagTypes = await getAllTagTypes(project.code, token); // 태그 타입 저장 await saveTagTypesToDatabase(allTagTypes, project.code); console.log(`프로젝트 ${project.code}의 태그 타입 동기화 완료`); return { project: project.code, success: true, count: allTagTypes.length }; } catch (error) { console.error(`프로젝트 ${project.code}의 태그 타입 동기화 실패:`, error); return { project: project.code, success: false, error: error instanceof Error ? error.message : String(error) }; } }) ); // 태그 타입 동기화 결과 집계 const tagTypeSuccessCount = tagTypeResults.filter( result => result.status === 'fulfilled' && result.value.success ).length; const tagTypeFailCount = tagTypeResults.length - tagTypeSuccessCount; console.log(`모든 프로젝트의 태그 타입 동기화 완료: ${tagTypeSuccessCount}개 성공, ${tagTypeFailCount}개 실패`); // 4. 각 프로젝트에 대해 오브젝트 클래스 동기화 (태그 타입 동기화는 건너뜀) const results = await Promise.allSettled( allProjects.map(async (project: Project) => { try { // 오브젝트 클래스 데이터 가져오기 const objectClasses = await getObjectClasses(project.code, token); // 데이터베이스에 저장 (skipTagTypeSync를 true로 설정하여 태그 타입 동기화 건너뜀) // 이 과정에서 LNK_ATT 속성도 함께 처리됨 const count = await saveObjectClassesToDatabase(project.id, objectClasses, project.code, token, true); 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 { tagTypeSync: { success: tagTypeSuccessCount, failed: tagTypeFailCount }, objectClassSync: { success: successCount, failed: failCount, items: totalItems }, timestamp: new Date().toISOString() }; } catch (error) { console.error('오브젝트 클래스 동기화 중 오류 발생:', error); throw error; } } // 유틸리티 함수들 (필요시 사용) export async function getTagClassWithAttributes(tagClassId: number) { const tagClass = await db.select() .from(tagClasses) .where(eq(tagClasses.id, tagClassId)) .limit(1); if (tagClass.length === 0) { return null; } const attributes = await db.select() .from(tagClassAttributes) .where(eq(tagClassAttributes.tagClassId, tagClassId)) .orderBy(tagClassAttributes.seq); return { ...tagClass[0], attributes }; } export async function getProjectTagClassesWithAttributes(projectId: number) { const result = await db.select({ // 태그 클래스 정보 id: tagClasses.id, code: tagClasses.code, label: tagClasses.label, tagTypeCode: tagClasses.tagTypeCode, subclasses: tagClasses.subclasses, subclassRemark: tagClasses.subclassRemark, createdAt: tagClasses.createdAt, updatedAt: tagClasses.updatedAt, // 속성 정보 attributeId: tagClassAttributes.id, attId: tagClassAttributes.attId, defVal: tagClassAttributes.defVal, uomId: tagClassAttributes.uomId, seq: tagClassAttributes.seq }) .from(tagClasses) .leftJoin(tagClassAttributes, eq(tagClasses.id, tagClassAttributes.tagClassId)) .where(eq(tagClasses.projectId, projectId)) .orderBy(tagClasses.code, tagClassAttributes.seq); // 결과를 태그 클래스별로 그룹화 const groupedResult = result.reduce((acc, row) => { const tagClassId = row.id; if (!acc[tagClassId]) { acc[tagClassId] = { id: row.id, code: row.code, label: row.label, tagTypeCode: row.tagTypeCode, subclasses: row.subclasses, // 이제 {id, desc}[] 형태 subclassRemark: row.subclassRemark, createdAt: row.createdAt, updatedAt: row.updatedAt, attributes: [] }; } // 속성이 있는 경우에만 추가 if (row.attributeId) { acc[tagClassId].attributes.push({ id: row.attributeId, attId: row.attId, defVal: row.defVal, uomId: row.uomId, seq: row.seq }); } return acc; }, {} as Record); return Object.values(groupedResult); }