유튜브 자동 댓글 챗봇 만들기 (구글시트+Gemini 활용)
유튜브 챗봇 만들기 목차 바로가기
영상 강의
예제파일 다운로드
오빠두엑셀의 강의 예제파일은 여러분을 위해 자유롭게 제공하고 있습니다.
- [업무생산성] 유튜브 자동 댓글 챗봇 만들기보충파일
라이브 강의 전체영상도 함께 확인해보세요!
위캔두 회원이 되시면 매주 오빠두엑셀에서 진행하는 라이브강의 풀영상을 확인하실 수 있습니다.
관련 링크 모음
- Gemini API 발급
- 구글 클라우드 플랫폼
- 구글시트 oAuth 스크립트 ID
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
- Gemini 무료 API 요금 안내
- 유튜브 API 작업별 Cost 안내
Gemini 연동 마스터 코드
var scriptProperties = PropertiesService.getScriptProperties(); const geminiApiKey = scriptProperties.getProperty('GEMINI_API_KEY'); const openAi_ApiKey = scriptProperties.getProperty('OPENAI_API_KEY'); const geminiModel = 'gemini-1.5-flash'; // 다른 버전의 Gemini 모델 필요시, 여기서 수정하세요. const geminiEndpoint = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}-latest:generateContent?key=${geminiApiKey}`; /** ContetPrompt : 맥락, 상황을 작성합니다.(예: 채널 설명 등) tontPrompt : 어조를 지정합니다. (예: 부드럽게, 유머스럽게 등) instructionPrompt : 그 외 지시사항을 작성합니다. (예: 답글 마지막에는 '감사합니다!'로 끝낼 것 등) **/ const contextPrompt = `* 나는 대한민국에서 직장인을 위해 엑셀 및 다양한 오피스 프로그램의 활용법을 알려주는 유튜브 채널을 운영하고 있어. * 구독자가 작성한 댓글에 답변을 작성하는 작업에 도움이 필요해.`; const tonePrompt = `* 답변은 친절하면서 간결하고, 명료하고, 전문성있게 작성해. 항상 전문성을 잃지 않도록 노력해.`; const instructionPrompt = `* 마지막에는 "감사합니다! -오빠두봇드림✨"으로 마칠 것. * 댓글 내용은 반복하지 말고, 답글만 작성할 것. * 만약 댓글에 타임라인(예: 12:34)이 있으면 커리큘럼에 있는 타임라인을 참고해서 응원의 문장을 작성할 것.\n`; //-------------------------------------------- function 프롬프트미리보기 () { generatePrompt('영상 제목','영상 설명','감사합니다!','별말씀을요!'); } function YoutubeCommentAutoReply() { GetCommentsInSheet(); generateReplies(); postRepliesToComments(); } function generateReplies() { var sheet = SpreadsheetApp.openById(sheetId).getActiveSheet(); var dataRange = sheet.getDataRange(); var data = dataRange.getValues(); var headers = data[0]; var idIndex = headers.indexOf("ID"); var titleIndex = headers.indexOf("영상 제목"); var descriptionIndex = headers.indexOf("영상 설명"); var textIndex = headers.indexOf("댓글"); var replyIndex = headers.indexOf("답글"); var keywordIndex = headers.indexOf("초안"); var nameIndex = headers.indexOf("닉네임"); var statusIndex = headers.indexOf("확인"); for (var i = data.length - 1; i >= 1; i--) { if (data[i][statusIndex] === '') { if (data[i][nameIndex] === yHandle) { sheet.getRange(i + 1, replyIndex + 1).setValue('-'); sheet.getRange(i + 1, statusIndex + 1).setValue('X'); } else { var commentId = data[i][idIndex]; var youtubeTitle = data[i][titleIndex]; var youtubeDescription = data[i][descriptionIndex]; var comment = data[i][textIndex]; var keyword = data[i][keywordIndex]; sheet.getRange(i + 1, replyIndex + 1).setValue('✨ 답변 생성중...'); SpreadsheetApp.flush(); if (commentId.includes('.')) { var topLevelCommentId = commentId.split('.')[0]; var previousComments = ''; for (var j = 1; j < data.length - 1; j++) { var prevCommentId = data[j][idIndex]; if (prevCommentId === topLevelCommentId || prevCommentId.startsWith(topLevelCommentId + '.')) { var prevComment = data[j][textIndex]; var prevReply = data[j][replyIndex]; previousComments += 'Comment: ' + prevComment + '\nReply: ' + prevReply + '\n\n'; } } var prompt = generatePrompt(youtubeTitle, youtubeDescription, comment, keyword, previousComments); var reply = askGemini(prompt); // <- ChatGPT 사용 시, 이 부분을 askChatGPT로 변경하세요! sheet.getRange(i + 1, replyIndex + 1).setValue(reply); } else { var prompt = generatePrompt(youtubeTitle, youtubeDescription, comment, keyword); var reply = askGemini(prompt); // <- ChatGPT 사용 시, 이 부분을 askChatGPT로 변경하세요! sheet.getRange(i + 1, replyIndex + 1).setValue(reply); } } } else if (data[i][statusIndex] === 'O') { break; } else { sheet.getRange(i + 1, replyIndex + 1).setValue('-'); } SpreadsheetApp.flush(); } } function generatePrompt(youtubeTitle='', youtubeDescription='', comment='', keyword='', previousComments='') { var prompt = `###Context###\n${contextPrompt}\n\n`; prompt += `###Objective###\n* Information에 제공한 영상 제목과 설명을 참고하여, 댓글에 대한 답변을 작성해.\n`; if (keyword.length > 0) { prompt += `* 답변은 초안을 참고하여 작성할 것.\n`; } prompt += '\n'; prompt += `###Information###\n`; prompt += `* 영상 제목: ${youtubeTitle}\n* 영상 설명: ${youtubeDescription}\n`; if (previousComments) { prompt += `* 이전 댓글과 답변:\n${previousComments}\n`; } if (keyword.length > 0) { prompt += `* 댓글: ${comment}\n* 답변 초안: ${keyword}\n\n`; } else { prompt += `* 댓글: ${comment}\n\n`; } prompt += `###Tone###\n${tonePrompt}`; prompt += '\n\n'; prompt += `###Intruction###\n` + `${instructionPrompt}`; Logger.log(prompt); return prompt; } /** * Gemini API를 사용하여 응답을 생성합니다. * @param {string} prompt - Gemini 모델에 전달할 프롬프트 텍스트 * @param {number} temperature - 생성 텍스트의 무작위성을 제어하는 온도 값 (기본값: 1) * @return {string} Gemini 모델이 생성한 텍스트 응답 * @customfunction */ function askGemini(prompt, temperature=1) { const payload = { "contents": [ { "parts": [ { "text": prompt } ] } ], "generationConfig": { "temperature": temperature, }, }; const options = { 'method' : 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload) }; const response = UrlFetchApp.fetch(geminiEndpoint, options); const data = JSON.parse(response); const content = data["candidates"][0]["content"]["parts"][0]["text"]; return content; } function askChatGPT(prompt) { var url = 'https://api.openai.com/v1/chat/completions'; var options = { method: 'post', contentType: 'application/json', headers: { 'Authorization': 'Bearer ' + openAi_ApiKey }, payload: JSON.stringify({ model: 'gpt-4o-mini', // Use the correct model name messages: [ {role: 'system', content: '당신은 유튜브 채널에 남겨진 구독자 댓글에 대한 답변을 작성하는 대한민국 최고의 챗봇입니다.'}, {role: 'user', content: prompt} ] }), muteHttpExceptions: true }; var response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() !== 200) { Logger.log('Error: ' + response.getContentText()); throw new Error('Failed to fetch response from OpenAI API'); } var json = response.getContentText(); var reply = JSON.parse(json).choices[0].message.content.trim(); return reply; }
유튜브 데이터 API 연동 마스터 코드
const oAuthURL = 'https://accounts.google.com/o/oauth2/auth'; const tokenURL = 'https://accounts.google.com/o/oauth2/token'; const sslScope = 'https://www.googleapis.com/auth/youtube.force-ssl'; var scriptProperties = PropertiesService.getScriptProperties(); const clientId = scriptProperties.getProperty('CLIENT_ID'); const yHandle = scriptProperties.getProperty('YOUTUBE_HANDLE'); const channelId = scriptProperties.getProperty('YOUTUBE_CHANNEL_ID'); const sheetId = scriptProperties.getProperty('SHEET_ID'); function getYouTubeService() { return OAuth2.createService('YouTube') .setAuthorizationBaseUrl(oAuthURL) .setTokenUrl(tokenURL) .setClientId(clientId) .setCallbackFunction('authCallback') .setPropertyStore(PropertiesService.getUserProperties()) .setScope(sslScope) .setParam('access_type', 'offline') .setParam('approval_prompt', 'force') .setParam('login_hint', Session.getActiveUser().getEmail()); } function authCallback(request) { var youtubeService = getYouTubeService(); var isAuthorized = youtubeService.handleCallback(request); if (isAuthorized) { return HtmlService.createHtmlOutput('인증에 성공하였습니다. 이 창을 종료 후, 앱스크립트 편집기로 돌아가도 괜찮습니다.'); } else { return HtmlService.createHtmlOutput('인증 과정에 오류가 발생했습니다. 다시 시도하세요.'); } } function 답글생성하기() { generateReplies(); } function 댓글불러오기() { var youtubeService = YouTube.CommentThreads; var sheet = SpreadsheetApp.openById(sheetId).getActiveSheet(); var lastRow = sheet.getLastRow(); var lastTimestamp = lastRow > 1 ? new Date(sheet.getRange(lastRow, 6).getValue()) : null; var videoResponse = youtubeService.list('snippet,replies', { allThreadsRelatedToChannelId: channelId, maxResults: 100 }); var commentThreads = []; for (var i = 0; i <= videoResponse.items.length - 1 ; i++) { var item = videoResponse.items[i]; var videoId = item.snippet.videoId; if (!videoId) { Logger.log('잘못된 Video ID 입니다. : ' + videoId); continue; } var videoDetails = YouTube.Videos.list('snippet', { id: videoId }).items[0]; if (!videoDetails) { Logger.log('영상 설명을 찾을 수 없습니다. : ' + videoId); continue; } if (item.snippet.topLevelComment.snippet.authorDisplayName != yHandle) { commentThreads.unshift([ item.snippet.topLevelComment.id, videoDetails.snippet.title, videoDetails.snippet.description, item.snippet.topLevelComment.snippet.textDisplay.replace(/<[^>]*>/g, ''), item.snippet.topLevelComment.snippet.authorDisplayName, item.snippet.topLevelComment.snippet.publishedAt ]); } if (item.replies) { for (var j = 0; j <= item.replies.comments.length - 1; j++) { var reply = item.replies.comments[j]; if (reply.snippet.authorDisplayName != yHandle) { commentThreads.unshift([ reply.id, videoDetails.snippet.title, videoDetails.snippet.description, reply.snippet.textDisplay.replace(/<[^>]*>/g, ''), reply.snippet.authorDisplayName, reply.snippet.publishedAt ]); } } } } var newCommentThreads = lastTimestamp ? commentThreads.filter(function(comment) { var commentTimestamp = new Date(comment[5]); return commentTimestamp > lastTimestamp; }) : commentThreads; newCommentThreads.sort(function(a, b) { var timeA = new Date(a[5]); var timeB = new Date(b[5]); return timeA - timeB; }); if (newCommentThreads.length > 0) { sheet.getRange(lastRow + 1, 1, newCommentThreads.length, newCommentThreads[0].length).setValues(newCommentThreads); } } function 답글업로드하기() { var sheet = SpreadsheetApp.openById(sheetId).getActiveSheet(); var lastRow = sheet.getLastRow(); var idIndex = 1; var replyIndex = 9; var statusIndex = 7; var youtubeService = YouTube.Comments; for (var i = lastRow; i >= 2; i--) { var commentId = sheet.getRange(i, idIndex).getValue(); var reply = sheet.getRange(i, replyIndex).getValue(); var status = sheet.getRange(i, statusIndex).getValue(); if (status == 'O') { break; } Logger.log('Comment ID: ' + commentId); Logger.log('Original Reply: ' + reply); Logger.log('Status: ' + status); var topLevelCommentId = commentId.split('.')[0]; if (reply.trim().length > 0 && status == '') { var resource = { snippet: { parentId: topLevelCommentId, textOriginal: reply } }; try { youtubeService.insert(resource, 'snippet'); sheet.getRange(i, statusIndex).setValue('O'); Logger.log('ID의 답글을 성공적으로 게시했습니다. : ' + commentId); } catch (error) { Logger.log('해당 ID의 답글 게시 중 오류가 발생했습니다. : ' + commentId); Logger.log('오류 메시지 : ' + error.message); } } else { Logger.log('해당 ID의 답글이 비어있어 다음 답글로 넘어갑니다. : ' + commentId); } } }
ChatGPT 기반 챗봇 만드는 방법
Gemini 무료 API 대신 ChatGPT 기반의 챗봇을 사용하려면, 다음 단계에 따라 OpenAI에서 API키를 발급받은 후 코드를 수정합니다.
- 아래 링크를 클릭하여 OpenAI 개발자센터로 이동 후 API키를 발급받습니다.
https://platform.openai.com/api-keys
- Dashboard → API Keys → [Create New secret key] 버튼 클릭
- '만약 등록한 결제 수단이 없을 경우, API키 발급이 안될 수 있습니다. 그럴 경우 우측 상단의 [설정] → Billing 에서 결제수단을 먼저 등록합니다.
오빠두Tip : OpenAI API키를 발급받는 전체 과정 및 엑셀+ChatGPT 추가기능 사용법은 아래 영상 강의를 확인하세요!👇
- 앱스크립트 편집기로 이동한 후, 속성에서 새로운 속성값을 추가합니다.
Property : OPENAI_API_KEY
Value : OpenAI API키 - Gemini 연동 마스터 코드 중, askGemini를 askChatGPT로 변경하면 ChatGPT API를 활용한 유튜브 챗봇이 완성됩니다. (두 군데를 모두 바꿔주세요!)
변경 전 : var reply = askGemini(prompt);
변경 후 : var reply = askChatGPT(prompt);