// --- [1. 상수 정의] --- // 시트 이름 const SHEET_YEAR_MAP = { // 년도별 상세 항목 시트 이름 매핑 "2022년": "22_3,4Q세부항목", "2023년": "23년_세부항목", "2024년": "24년_세부항목", // 현재 기본값 "2025년": "25년_세부항목", "2026년": "26년_세부항목" }; const SHEET_AUTO_PRODUCT_CODE = "auto_product code"; const SHEET_AUTO_WAREHOUSE_CODE = "auto_warehouse code"; const SHEET_AUTO_BUYER_CODE = "auto_buyer code"; const SHEET_AUTO_INTERNAL_TRANSACTION = "auto_내부거래 일괄 입력"; // "선택된 상세 항목 시트" 탭 컬럼 인덱스 (0부터 시작) - 모든 상세 항목 시트의 컬럼 구조가 동일하다고 가정 const COL_DETAIL_DELIVERY_DATE = 0; // A열 "전달일자" const COL_DETAIL_REQUEST_DEPT = 1; // B열 "요청부서" const COL_DETAIL_PRODUCT_NAME = 3; // D열 "품명" const COL_DETAIL_QUANTITY = 4; // E열 "수량" const COL_DETAIL_UNIT_COST = 5; // F열 "개당 생산 원가" const COL_DETAIL_TOTAL_COST = 6; // G열 "원가 합계" const COL_DETAIL_PURPOSE = 7; // H열 "사용 목적" const DETAIL_DATA_START_ROW_OFFSET = 2; // 상세 항목 시트에서 실제 데이터가 시작하는 행 (1 = slice(0), 2 = slice(1), 3 = slice(2)) // "auto_product code" 탭 컬럼 인덱스 const COL_PRODUCT_CODE_CODE = 0; // A열 "품목코드" const COL_PRODUCT_CODE_NAME = 1; // B열 "제품명" // "auto_warehouse code" 탭 컬럼 인덱스 const COL_WAREHOUSE_CODE_CODE = 0; // A열 "창고코드" const COL_WAREHOUSE_CODE_NAME = 1; // B열 "창고명" // "auto_buyer code" 탭 컬럼 인덱스 const COL_BUYER_CODE_CODE = 0; // A열 "거래처코드" const COL_BUYER_CODE_NAME = 1; // B열 "거래처" // "auto_내부거래 일괄 입력" 탭 컬럼 인덱스 (데이터가 작성될 위치, 0부터 시작) const COL_AUTO_INTERNAL_DATE = 0; // A열 "일자" const COL_AUTO_INTERNAL_BUYER_CODE = 2; // C열 "거래처코드" const COL_AUTO_INTERNAL_BUYER_NAME = 3; // D열 "거래처명" const COL_AUTO_INTERNAL_PERSON_IN_CHARGE = 4; // E열 "담당자" (이제 D1에서 읽은 값이 여기에 들어감) const COL_AUTO_INTERNAL_WAREHOUSE = 5; // F열 "출하창고" const COL_AUTO_INTERNAL_TRANSACTION_TYPE = 6; // G열 "거래유형" const COL_AUTO_INTERNAL_PRODUCT_CODE = 9; // J열 "품목코드" const COL_AUTO_INTERNAL_PRODUCT_NAME = 10; // K열 "품목명" const COL_AUTO_INTERNAL_QUANTITY = 11; // L열 "수량" const COL_AUTO_INTERNAL_UNIT_PRICE = 12; // M열 "단가" const COL_AUTO_INTERNAL_TOTAL_PRICE = 13; // N열 "단가(\)" const COL_AUTO_INTERNAL_SUPPLY_AMOUNT = 15; // P열 "공급가액" (새로 추가) const COL_AUTO_INTERNAL_PURPOSE = 18; // S열 "적요" // UI 요소 위치 상수 (auto_내부거래 일괄 입력 탭) const UI_YEAR_CELL = 'B1'; // 년도 선택 드롭다운 셀 const UI_PIC_CELL = 'D1'; // 담당자 지정 드롭다운 셀 const UI_EXECUTE_BUTTON_CELL = 'F1'; // 실행 버튼 위치 (레이블용) const UI_RESET_BUTTON_CELL = 'G1'; // 리셋 버튼 위치 (레이블용) const DATA_START_ROW_IN_TARGET_SHEET = 3; // UI 추가 후 실제 데이터가 시작될 행 // 유사성 비교를 위한 키워드 (품명용) const PRODUCT_KEYWORDS = [ "피규어", "충전기", "골프공", "인형", "후드", "티셔츠", "가방", "스티커", "폰케이스", "컵", "텀블러", "모자", "뱃지", "키링", "담요", "포스터", "마우스패드", "무선", "엔젤몬", "데빌몬", "선풍기", "장패드", "서머너즈워", "서머너즈 워", "솜인형", "아르타미엘", "브라우니", "해왕", "집업" ]; // 매칭 점수 임계값 const MIN_MATCH_SCORE_THRESHOLD = 5; // 거래처명에서 제거할 접두사들 (소문자) const BUYER_PREFIXES = ['c2s_', 'c2h_', 'c2v_', 'c2m_', 'c2p_']; // 요청부서명에서 제거할 접미사/부가 단어들 (소문자) const REQUEST_DEPT_SUFFIXES = ['팀', '실', '사업실', '기술총괄', 'ip관리팀', '정보보호팀', '스튜디오']; // 담당자 드롭다운 목록 const PIC_LIST = ["김수정", "김진영", "박지훈", "박현지", "배재선", "조동규"]; // --- [2. 메인 함수] --- function processProductCodesAndPopulateDetails() { const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); const targetSheet = spreadsheet.getSheetByName(SHEET_AUTO_INTERNAL_TRANSACTION); if (!targetSheet) { Browser.msgBox('오류: "auto_내부거래 일괄 입력" 시트를 찾을 수 없습니다.'); return; } // --- UI에서 년도 및 담당자 값 읽기 --- const selectedYear = targetSheet.getRange(UI_YEAR_CELL).getValue(); const selectedPIC = targetSheet.getRange(UI_PIC_CELL).getValue(); Logger.log(`UI에서 읽어온 선택 년도: "${selectedYear}"`); Logger.log(`UI에서 읽어온 담당자 이름: "${selectedPIC}"`); if (!selectedYear) { Browser.msgBox('경고', `B1 셀에 년도가 선택되지 않았습니다.`, Browser.Buttons.OK); return; } if (!SHEET_YEAR_MAP[selectedYear]) { Browser.msgBox('오류', `선택된 년도(${selectedYear})에 해당하는 상세 항목 시트 이름을 찾을 수 없습니다. SHEET_YEAR_MAP 상수를 확인하세요.`, Browser.Buttons.OK); return; } if (!selectedPIC) { Browser.msgBox('경고', `D1 셀에 담당자 이름이 입력되어 있지 않습니다. 담당자 없이 진행합니다.`, Browser.Buttons.OK); } const detailSheetName = SHEET_YEAR_MAP[selectedYear]; const detailSheet = spreadsheet.getSheetByName(detailSheetName); if (!detailSheet) { Browser.msgBox('오류', `'${detailSheetName}' 시트를 찾을 수 없습니다. 시트 이름을 확인하거나 해당 년도 데이터를 추가하세요.`); return; } // 다른 참조 시트들도 확인 const productCodeSheet = spreadsheet.getSheetByName(SHEET_AUTO_PRODUCT_CODE); const warehouseCodeSheet = spreadsheet.getSheetByName(SHEET_AUTO_WAREHOUSE_CODE); const buyerCodeSheet = spreadsheet.getSheetByName(SHEET_AUTO_BUYER_CODE); if (!productCodeSheet || !warehouseCodeSheet || !buyerCodeSheet) { Browser.msgBox('오류: 필요한 참조 시트 중 하나 이상을 찾을 수 없습니다 (auto_product code, auto_warehouse code, auto_buyer code). 시트 이름을 확인해주세요.'); return; } // --- 사전 데이터 로딩 및 맵 생성 --- const productCodeData = productCodeSheet.getDataRange().getValues().slice(1); const productMap = createProductLookupMap(productCodeData); const warehouseCodeData = warehouseCodeSheet.getDataRange().getValues().slice(1); const kimpoWarehouseCode = getWarehouseCodeByName(warehouseCodeData, "김포창고"); if (!kimpoWarehouseCode) { Browser.msgBox('오류: "김포창고"의 창고코드를 "auto_warehouse code" 시트에서 찾을 수 없습니다. 확인해주세요.'); return; } const buyerCodeData = buyerCodeSheet.getDataRange().getValues().slice(1); const buyerMap = createBuyerLookupMap(buyerCodeData); // --- 타겟 시트 (auto_내부거래 일괄 입력) 초기화 --- // E열을 제외하고 데이터를 지웁니다. const lastRowOfTarget = targetSheet.getLastRow(); if (lastRowOfTarget >= DATA_START_ROW_IN_TARGET_SHEET) { // A열 (0) 부터 D열 (3) 까지 targetSheet.getRange(DATA_START_ROW_IN_TARGET_SHEET, COL_AUTO_INTERNAL_DATE + 1, lastRowOfTarget - DATA_START_ROW_IN_TARGET_SHEET + 1, COL_AUTO_INTERNAL_BUYER_NAME + 1).clearContent(); // F열 (5) 부터 I열 (8) 까지 targetSheet.getRange(DATA_START_ROW_IN_TARGET_SHEET, COL_AUTO_INTERNAL_WAREHOUSE + 1, lastRowOfTarget - DATA_START_ROW_IN_TARGET_SHEET + 1, COL_AUTO_INTERNAL_PRODUCT_CODE - COL_AUTO_INTERNAL_WAREHOUSE).clearContent(); // J열 (9) 부터 N열 (13) 까지 targetSheet.getRange(DATA_START_ROW_IN_TARGET_SHEET, COL_AUTO_INTERNAL_PRODUCT_CODE + 1, lastRowOfTarget - DATA_START_ROW_IN_TARGET_SHEET + 1, COL_AUTO_INTERNAL_TOTAL_PRICE - COL_AUTO_INTERNAL_PRODUCT_CODE + 1).clearContent(); // P열 (15) (새로 추가된 공급가액 열) targetSheet.getRange(DATA_START_ROW_IN_TARGET_SHEET, COL_AUTO_INTERNAL_SUPPLY_AMOUNT + 1, lastRowOfTarget - DATA_START_ROW_IN_TARGET_SHEET + 1, 1).clearContent(); // <<< P열 클리어 추가 // S열 (18) targetSheet.getRange(DATA_START_ROW_IN_TARGET_SHEET, COL_AUTO_INTERNAL_PURPOSE + 1, lastRowOfTarget - DATA_START_ROW_IN_TARGET_SHEET + 1, 1).clearContent(); Logger.log(`'${SHEET_AUTO_INTERNAL_TRANSACTION}' 탭의 지정된 열들을 ${DATA_START_ROW_IN_TARGET_SHEET}행부터 초기화했습니다.`); } // --- 선택된 상세 항목 시트에서 데이터 읽기 및 타겟 데이터 구성 --- const detailData = detailSheet.getDataRange().getValues().slice(DETAIL_DATA_START_ROW_OFFSET); const dataToPopulateTarget = []; let matchedProductCodeCount = 0; let matchedBuyerCodeCount = 0; const unmatchedProducts = []; const unmatchedBuyers = []; detailData.forEach((row, index) => { // S열(18)까지 배열을 미리 생성합니다. const newRowForTarget = new Array(COL_AUTO_INTERNAL_PURPOSE + 1).fill(''); // --- 1. 상세 항목 시트에서 직접 복사 (매핑) --- // A열 "일자" - YYMMDD -> 20YYMMDD 형식으로 변환 let deliveryDateValue = row[COL_DETAIL_DELIVERY_DATE]; if (typeof deliveryDateValue === 'number') { deliveryDateValue = String(deliveryDateValue); } if (typeof deliveryDateValue === 'string' && deliveryDateValue.length === 6 && /^\d+$/.test(deliveryDateValue)) { newRowForTarget[COL_AUTO_INTERNAL_DATE] = "20" + deliveryDateValue; } else if (deliveryDateValue instanceof Date) { newRowForTarget[COL_AUTO_INTERNAL_DATE] = Utilities.formatDate(deliveryDateValue, SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(), "yyyyMMdd"); } else { newRowForTarget[COL_AUTO_INTERNAL_DATE] = deliveryDateValue; } newRowForTarget[COL_AUTO_INTERNAL_BUYER_NAME] = row[COL_DETAIL_REQUEST_DEPT]; newRowForTarget[COL_AUTO_INTERNAL_PRODUCT_NAME] = row[COL_DETAIL_PRODUCT_NAME]; newRowForTarget[COL_AUTO_INTERNAL_QUANTITY] = row[COL_DETAIL_QUANTITY]; newRowForTarget[COL_AUTO_INTERNAL_UNIT_PRICE] = row[COL_DETAIL_UNIT_COST]; newRowForTarget[COL_AUTO_INTERNAL_TOTAL_PRICE] = row[COL_DETAIL_TOTAL_COST]; newRowForTarget[COL_AUTO_INTERNAL_PURPOSE] = row[COL_DETAIL_PURPOSE]; // --- 2. 룩업 또는 고정값 입력 --- // E열 "담당자" (UI D1 셀에서 읽어온 값으로 일괄 입력) newRowForTarget[COL_AUTO_INTERNAL_PERSON_IN_CHARGE] = selectedPIC; // J열 "품목코드" const detailProductName = row[COL_DETAIL_PRODUCT_NAME]; const foundProductCode = findProductCode(detailProductName, productMap); if (foundProductCode) { newRowForTarget[COL_AUTO_INTERNAL_PRODUCT_CODE] = foundProductCode; matchedProductCodeCount++; } else { newRowForTarget[COL_AUTO_INTERNAL_PRODUCT_CODE] = '품목코드 없음'; unmatchedProducts.push({ rowIndex: index + (DETAIL_DATA_START_ROW_OFFSET + 1), productName: detailProductName }); } // F열 "출하창고" -> "김포창고" 창고코드 newRowForTarget[COL_AUTO_INTERNAL_WAREHOUSE] = kimpoWarehouseCode; // C열 "거래처코드" -> 요청부서(거래처명)에 해당하는 코드 const requestDeptName = row[COL_DETAIL_REQUEST_DEPT]; const foundBuyerCode = findBuyerCode(requestDeptName, buyerMap); if (foundBuyerCode) { newRowForTarget[COL_AUTO_INTERNAL_BUYER_CODE] = foundBuyerCode; matchedBuyerCodeCount++; } else { newRowForTarget[COL_AUTO_INTERNAL_BUYER_CODE] = '거래처코드 없음'; unmatchedBuyers.push({ rowIndex: index + (DETAIL_DATA_START_ROW_OFFSET + 1), buyerName: requestDeptName }); } // G열 "거래유형" -> 10으로 고정값 입력 newRowForTarget[COL_AUTO_INTERNAL_TRANSACTION_TYPE] = 10; // P열 "공급가액" -> N열 "단가(\)" 값 그대로 복사 (새로 추가된 기능) newRowForTarget[COL_AUTO_INTERNAL_SUPPLY_AMOUNT] = newRowForTarget[COL_AUTO_INTERNAL_TOTAL_PRICE]; // <<< P열에 N열 값 복사 dataToPopulateTarget.push(newRowForTarget); }); // --- 최종 데이터 작성 --- if (dataToPopulateTarget.length > 0) { const range = targetSheet.getRange( DATA_START_ROW_IN_TARGET_SHEET, 1, // 1열 (A열) dataToPopulateTarget.length, dataToPopulateTarget[0].length // 작성할 열의 수 (S열까지) ); range.setValues(dataToPopulateTarget); Logger.log(`'${SHEET_AUTO_INTERNAL_TRANSACTION}' 탭의 A열부터 S열까지, ${DATA_START_ROW_IN_TARGET_SHEET}행부터 ${dataToPopulateTarget.length}개의 데이터를 작성했습니다.`); } else { Logger.log('작성할 데이터가 없습니다.'); } // --- 결과 요약 및 로그 출력 --- Logger.log('--- 최종 매칭 및 작성 결과 ---'); Logger.log(`총 ${detailData.length}개 품목 중 ${matchedProductCodeCount}개 품목코드 매칭 성공.`); Logger.log(`총 ${detailData.length}개 요청부서 중 ${matchedBuyerCodeCount}개 거래처코드 매칭 성공.`); if (unmatchedProducts.length > 0) { Logger.log('\n--- 매칭되지 않은 품목 목록 ---'); unmatchedProducts.forEach(item => { Logger.log(`행 ${item.rowIndex}: "${item.productName}"`); }); } else { Logger.log('\n모든 품목이 성공적으로 매칭되었습니다!'); } if (unmatchedBuyers.length > 0) { Logger.log('\n--- 매칭되지 않은 거래처 목록 ---'); unmatchedBuyers.forEach(item => { Logger.log(`행 ${item.rowIndex}: "${item.buyerName}"`); }); } else { Logger.log('\n모든 거래처가 성공적으로 매칭되었습니다!'); } Browser.msgBox( "자동화 완료", `선택된 '${detailSheetName}' 탭의 데이터를 바탕으로\n` + `'${SHEET_AUTO_INTERNAL_TRANSACTION}' 탭의 A열부터 S열까지 데이터 작성을 완료했습니다.\n\n` + `총 ${detailData.length}개 품목 중 ${matchedProductCodeCount}개 품목코드 매칭 성공.\n` + `총 ${detailData.length}개 요청부서 중 ${matchedBuyerCodeCount}개 거래처코드 매칭 성공.\n` + `매칭 실패 품목: ${unmatchedProducts.length}개 (품목코드), ${unmatchedBuyers.length}개 (거래처코드).` + ` (자세한 내용은 실행 로그를 확인하세요.)`, Browser.Buttons.OK ); } // --- [3. 헬퍼 함수] --- /** * 문자열을 정규화합니다 (특수문자, 대괄호, 여러 공백 제거, 소문자 변환). * @param {string} str 원본 문자열. * @returns {string} 정규화된 문자열. */ function normalizeString(str) { if (typeof str !== 'string') return ''; let cleanedStr = str.replace(/\[.*?\]|\(|\)|\.|,|~|!|@|#|\$|%|\^&|\*|-|_|=|\+|\\|\||;|\:|\'|\"|<|>|\?|\/|\[|\]/g, ' '); cleanedStr = cleanedStr.replace(/\s+/g, ' ').trim().toLowerCase(); return cleanedStr; } /** * "auto_product code" 시트의 데이터를 바탕으로 품목코드 맵을 생성합니다. * @param {Array>} data "auto_product code" 시트에서 읽은 데이터 배열. * @returns {Object} 정규화된 제품명 -> {originalName: string, code: string, normalizedWords: Array} 맵. */ function createProductLookupMap(data) { const productMap = {}; data.forEach(row => { const productCode = String(row[COL_PRODUCT_CODE_CODE]); const productName = String(row[COL_PRODUCT_CODE_NAME]); const normalizedProductName = normalizeString(productName); const normalizedWords = normalizedProductName.split(' ').filter(word => word.length > 1); if (productMap[normalizedProductName]) { // 나중에 들어온 값으로 덮어씁니다. } productMap[normalizedProductName] = { originalName: productName, code: productCode, normalizedWords: normalizedWords }; }); return productMap; } /** * 주어진 품명에 대해 가장 적합한 품목코드를 찾습니다. (점수 기반 매칭) * @param {string} detailProductName "선택된 상세 항목 시트" 탭의 품명. * @param {Object} productMap `createProductLookupMap`으로 생성된 품목 맵. * @returns {string|null} 찾은 품목코드 또는 null (찾지 못했을 경우). */ function findProductCode(detailProductName, productMap) { if (typeof detailProductName !== 'string') return null; const normalizedDetailProductName = normalizeString(detailProductName); const detailWords = normalizedDetailProductName.split(' ').filter(word => word.length > 1); let bestMatchCode = null; let highestScore = 0; for (const normalizedProductCodeName in productMap) { const productInfo = productMap[normalizedProductCodeName]; const productWords = productInfo.normalizedWords; let currentScore = 0; if (normalizedDetailProductName === normalizedProductCodeName) { currentScore = 1000; } else if (normalizedDetailProductName.includes(normalizedProductCodeName)) { currentScore = Math.max(currentScore, normalizedProductCodeName.length * 10); } else if (normalizedProductCodeName.includes(normalizedDetailProductName)) { currentScore = Math.max(currentScore, normalizedDetailProductName.length * 10); } const commonWords = detailWords.filter(dWord => productWords.includes(dWord)); commonWords.forEach(commonWord => { currentScore += commonWord.length * 5; }); PRODUCT_KEYWORDS.forEach(keyword => { const normalizedKeyword = normalizeString(keyword); if (normalizedDetailProductName.includes(normalizedKeyword) && normalizedProductCodeName.includes(normalizedKeyword)) { currentScore += normalizedKeyword.length * 2; } }); const detailNoSpace = normalizedDetailProductName.replace(/\s/g, ''); const productNoSpace = normalizedProductCodeName.replace(/\s/g, ''); if (detailNoSpace === productNoSpace && detailNoSpace.length > 0) { currentScore += 500; } if (currentScore > highestScore) { highestScore = currentScore; bestMatchCode = productInfo.code; } } if (bestMatchCode && highestScore >= MIN_MATCH_SCORE_THRESHOLD) { return bestMatchCode; } return null; } /** * "auto_warehouse code" 시트 데이터에서 특정 창고명의 코드를 찾습니다. * @param {Array>} data "auto_warehouse code" 시트에서 읽은 데이터 배열. * @param {string} warehouseName 찾을 창고명. * @returns {string|null} 찾은 창고코드 또는 null. */ function getWarehouseCodeByName(data, warehouseName) { if (typeof warehouseName !== 'string') return null; const normalizedSearchName = normalizeString(warehouseName); for (let i = 0; i < data.length; i++) { const currentWarehouseName = String(data[i][COL_WAREHOUSE_CODE_NAME]); if (normalizeString(currentWarehouseName) === normalizedSearchName) { return String(data[i][COL_WAREHOUSE_CODE_CODE]); } } return null; } /** * 거래처명에서 특정 접두사 (C2S_, C2H_ 등)를 제거합니다. * @param {string} buyerName 원본 거래처명 (예: C2S_IP사업팀). * @returns {string} 접두사가 제거된 거래처명 (예: IP사업팀). */ function stripBuyerPrefix(buyerName) { if (typeof buyerName !== 'string') return ''; let cleanedName = buyerName.toLowerCase(); for (const prefix of BUYER_PREFIXES) { if (cleanedName.startsWith(prefix)) { cleanedName = cleanedName.substring(prefix.length); break; } } return cleanedName.trim(); } /** * 요청부서명에서 특정 부가적인 접미사/단어를 제거합니다. * @param {string} requestDeptName 원본 요청부서명 (예: 컴투스플랫폼 기술총괄). * @returns {string} 부가적인 단어가 제거된 요청부서명 (예: 플랫폼). */ function stripRequestDeptSuffix(requestDeptName) { if (typeof requestDeptName !== 'string') return ''; let cleanedName = requestDeptName.toLowerCase(); // 요청부서명에 존재하는 회사명 접두사를 먼저 처리 if (cleanedName.startsWith('컴투스플랫폼')) cleanedName = cleanedName.replace('컴투스플랫폼', '플랫폼'); else if (cleanedName.startsWith('컴투스홀딩스')) cleanedName = cleanedName.replace('컴투스홀딩스', ''); else if (cleanedName.startsWith('컴투스')) cleanedName = cleanedName.replace('컴투스', ''); else if (cleanedName.startsWith('컴투버스')) cleanedName = cleanedName.replace('컴투버스', ''); for (const suffix of REQUEST_DEPT_SUFFIXES) { cleanedName = cleanedName.replace(new RegExp(`\\s*${suffix}\\s*$`, 'g'), '').trim(); cleanedName = cleanedName.replace(new RegExp(`\\s*${suffix}\\s*`, 'g'), ' ').trim(); } cleanedName = cleanedName.replace(/\s+/g, ' ').trim(); return cleanedName; } /** * "auto_buyer code" 시트의 데이터를 바탕으로 거래처코드 맵을 생성합니다. * @param {Array>} data "auto_buyer code" 시트에서 읽은 데이터 배열. * @returns {Object} 정규화된 (접두사 제거된) 거래처명 -> {originalName: string, code: string, normalizedName: string, normalizedWords: Array} 맵. */ function createBuyerLookupMap(data) { const buyerMap = {}; data.forEach(row => { const buyerCode = String(row[COL_BUYER_CODE_CODE]); const buyerName = String(row[COL_BUYER_CODE_NAME]); const strippedAndNormalizedBuyerName = normalizeString(stripBuyerPrefix(buyerName)); const normalizedWords = strippedAndNormalizedBuyerName.split(' ').filter(word => word.length > 1); buyerMap[strippedAndNormalizedBuyerName] = { originalName: buyerName, code: buyerCode, normalizedName: strippedAndNormalizedBuyerName, normalizedWords: normalizedWords }; }); return buyerMap; } /** * 주어진 요청부서명에 대해 가장 적합한 거래처코드를 찾습니다. (점수 기반 매칭) * @param {string} requestDeptName "선택된 상세 항목 시트" 탭의 요청부서명. * @param {Object} buyerMap `createBuyerLookupMap`으로 생성된 거래처 맵. * @returns {string|null} 찾은 거래처코드 또는 null (찾지 못했을 경우). */ function findBuyerCode(requestDeptName, buyerMap) { if (typeof requestDeptName !== 'string') return null; // --- 명시적 매핑 규칙 (최우선) --- const normalizedRequestDeptForExactMatch = normalizeString(requestDeptName); switch (normalizedRequestDeptForExactMatch) { case '조직문화팀': return '00031'; case '세무회계팀': return '00127'; case '글로벌라이제이션1팀': return '00128'; case '컴투버스 사업실': return '00033'; case '인사기획팀': return '00130'; case '컴투스홀딩스 기획파트': return '00057'; case 'ip사업팀': return '00001'; case '플랫폼사업실': return '00078'; case '정보보호팀': return '00057'; case 'eco실': return '00126'; } // --- 명시적 매핑 끝 --- const normalizedRequestDept = normalizeString(requestDeptName); const strippedRequestDept = normalizeString(stripRequestDeptSuffix(requestDeptName)); const requestDeptWords = normalizedRequestDept.split(' ').filter(word => word.length > 1); const strippedRequestDeptWords = strippedRequestDept.split(' ').filter(word => word.length > 1); let bestMatchCode = null; let highestScore = 0; const BUYER_MATCH_THRESHOLD = 10; for (const buyerMapKey in buyerMap) { const buyerInfo = buyerMap[buyerMapKey]; const normalizedBuyerName = buyerInfo.normalizedName; const buyerWords = buyerInfo.normalizedWords; let currentScore = 0; // 1. 요청부서명(정규화)과 거래처명(정규화)의 완전 일치 if (normalizedRequestDept === normalizedBuyerName) { currentScore += 1000; } // 2. 요청부서명(부가단어제거)과 거래처명(정규화)의 완전 일치 else if (strippedRequestDept === normalizedBuyerName) { currentScore += 900; } // 3. 포함 관계 if (normalizedRequestDept.includes(normalizedBuyerName)) { currentScore += normalizedBuyerName.length * 10; } else if (normalizedBuyerName.includes(normalizedRequestDept)) { currentScore += normalizedRequestDept.length * 10; } // 4. 단어 기반 일치 (요청부서의 각 단어가 거래처명에 포함되는 경우) requestDeptWords.forEach(rWord => { if (buyerWords.includes(rWord)) { currentScore += rWord.length * 5; } }); strippedRequestDeptWords.forEach(sRWord => { if (buyerWords.includes(sRWord)) { currentScore += sRWord.length * 5; } }); if (currentScore > highestScore) { highestScore = currentScore; bestMatchCode = buyerInfo.code; } } if (bestMatchCode && highestScore >= BUYER_MATCH_THRESHOLD) { return bestMatchCode; } return null; } // --- [4. UI 설정 함수] --- function setupUIAndDropdowns() { const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); const targetSheet = spreadsheet.getSheetByName(SHEET_AUTO_INTERNAL_TRANSACTION); if (!targetSheet) { Browser.msgBox('오류: "auto_내부거래 일괄 입력" 시트를 찾을 수 없습니다.'); return; } // A1 셀이 "년도 선택"이 아니거나 B1에 드롭다운이 없으면 새 행 삽입 (UI가 아직 없는 경우) if (targetSheet.getRange('A1').getValue() !== '년도 선택' || targetSheet.getRange(UI_YEAR_CELL).getDataValidation() === null) { targetSheet.insertRows(1); // 1행에 새 행 삽입 } // 헤더 삽입 (새로운 1행) targetSheet.getRange('A1').setValue('년도 선택'); targetSheet.getRange('C1').setValue('담당자'); targetSheet.getRange(UI_EXECUTE_BUTTON_CELL).setValue('자동 기입 실행'); // F1에 실행 레이블 targetSheet.getRange(UI_RESET_BUTTON_CELL).setValue('리셋'); // G1에 리셋 레이블 // B1 년도 선택 드롭다운 생성 const years = Object.keys(SHEET_YEAR_MAP); const yearCell = targetSheet.getRange(UI_YEAR_CELL); const yearRule = SpreadsheetApp.newDataValidation() .requireValueInList(years, true) .setAllowInvalid(false) .setHelpText('데이터를 가져올 년도를 선택하세요.') .build(); yearCell.setDataValidation(yearRule); if (!yearCell.getValue()) { yearCell.setValue('2024년'); // 기본값 설정 (선택 사항) } // D1 담당자 지정 드롭다운 생성 const picCell = targetSheet.getRange(UI_PIC_CELL); const picRule = SpreadsheetApp.newDataValidation() .requireValueInList(PIC_LIST, true) .setAllowInvalid(false) .setHelpText('자동 입력될 담당자를 선택하세요.') .build(); picCell.setDataValidation(picRule); //picCell.setValue(''); // 초기에는 비워둘 수도 있습니다. Browser.msgBox("UI 및 드롭다운 설정 완료", "1행에 UI 요소와 드롭다운 메뉴가 생성되었습니다.\n" + "B1에서 년도를, D1에서 담당자를 선택한 후 F1/G1 위치에 버튼을 생성하고 스크립트를 연결하세요.", Browser.Buttons.OK); } // --- [5. 리셋 함수] --- function resetAutomationUIAndData() { const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); const targetSheet = spreadsheet.getSheetByName(SHEET_AUTO_INTERNAL_TRANSACTION); if (!targetSheet) { Browser.msgBox('오류: "auto_내부거래 일괄 입력" 시트를 찾을 수 없습니다.'); return; } // 1. 데이터 영역 삭제 (3행부터 마지막 행까지 모든 열) const lastRowOfTarget = targetSheet.getLastRow(); if (lastRowOfTarget >= DATA_START_ROW_IN_TARGET_SHEET) { targetSheet.getRange(DATA_START_ROW_IN_TARGET_SHEET, 1, lastRowOfTarget - DATA_START_ROW_IN_TARGET_SHEET + 1, targetSheet.getLastColumn()).clearContent(); Logger.log(`'${SHEET_AUTO_INTERNAL_TRANSACTION}' 탭의 ${DATA_START_ROW_IN_TARGET_SHEET}행부터 모든 데이터가 삭제되었습니다.`); } else { Logger.log('삭제할 데이터가 없습니다.'); } // 2. 년도/담당자 드롭다운 초기화 targetSheet.getRange(UI_YEAR_CELL).setValue('2024년'); // 년도 기본값으로 재설정 targetSheet.getRange(UI_PIC_CELL).clearContent(); // 담당자 선택 비우기 Browser.msgBox("리셋 완료", "데이터가 모두 삭제되었고 년도 및 담당자 선택이 초기화되었습니다.", Browser.Buttons.OK); } // --- [6. 커스텀 메뉴 생성 함수] --- // 스프레드시트를 열 때 자동으로 실행되어 커스텀 메뉴를 추가합니다. function onOpen() { const ui = SpreadsheetApp.getUi(); ui.createMenu('자동화 실행') // 최상위 메뉴 이름 .addItem('UI 설정 및 드롭다운 생성', 'setupUIAndDropdowns') .addSeparator() // 구분선 .addItem('데이터 자동 기입 실행', 'processProductCodesAndPopulateDetails') .addItem('데이터 및 UI 리셋', 'resetAutomationUIAndData') // 리셋 메뉴 추가 .addToUi(); }