매일 아침 정책브리핑. (데이터 수집단계)

소개

‎​이번에는 중기부 보도자료 데이터를 매일 아침 구글 시트에 기록하는 스크립트를 작성함

-클로드, 구글시트, 앱스스크립트 사용

이후 작성된 목록을 자동으로 이메일로 발송하는 단계를 진행할 계획임.

진행 방법

지피티를 이용하다가 클로드가 코딩 성능이 더 낫다는 평가가 있어 클로드를 활용함(지피티로 하다가 지쳐서 결제했음 ㅠㅠ)

프롬프트에서 전체 코딩작성 단계와 내용을 정리 후 시작

한국어 앱 스크린샷
한국사이트 스크린샷
흰색 텍스트가 있는 검은색 ��화면

몇단계 오류를 거친 주요 내용을 정리하면,

-공공데이터포털 활용신청 현황에 명시된 endpoint, 디코딩 api키, 필수 파라미터 적용해야 함

-공공데이터포털에서 신청현황 상세보기 등 화면캡처와 가이드도 첨부해줌

// API 키와 엔드포인트 설정
const API_KEY = "api key";
const API_ENDPOINT = "http://apis.data.go.kr/1421000/mssPressService_v2/getPressList_v2";
const BASE_URL = "https://www.mss.go.kr/site/smba/ex/bbs/List.do?cbIdx=86";

function getYesterdayDate() {
  const today = new Date();
  const yesterday = new Date(today);
  
  if (today.getDay() === 1) {  // 월요일
    yesterday.setDate(today.getDate() - 3);
  } else {
    yesterday.setDate(today.getDate() - 1);
  }
  
  return Utilities.formatDate(yesterday, "Asia/Seoul", "yyyy-MM-dd");
}

function isWeekday() {
  const day = new Date().getDay();
  return day !== 0 && day !== 6;
}

function fetchNewsData() {
  if (!isWeekday()) return null;
  
  const yesterdayDate = getYesterdayDate();
  
  // 필수 파라미터 설정
  const params = {
    serviceKey: API_KEY,
    pageNo: '1',
    numOfRows: '100',
    startDate: yesterdayDate,
    endDate: yesterdayDate
  };
  
  // URL 파라미터 조합
  const queryString = Object.entries(params)
    .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
    .join('&');
  
  const url = `${API_ENDPOINT}?${queryString}`;
  
  try {
    const options = {
      'muteHttpExceptions': true,
      'method': 'GET'
    };
    
    Logger.log('요청 URL: ' + url);
    
    const response = UrlFetchApp.fetch(url, options);
    const responseCode = response.getResponseCode();
    const responseContent = response.getContentText();
    
    Logger.log('응답 코드: ' + responseCode);
    Logger.log('응답 내용: ' + responseContent);
    
    if (responseCode !== 200) {
      Logger.log('API 오류 - 응답 코드: ' + responseCode);
      return null;
    }
    
    const document = XmlService.parse(responseContent);
    const root = document.getRootElement();
    
    // 에러 체크
    const headerElement = root.getChild('cmmMsgHeader');
    if (headerElement) {
      const errMsg = headerElement.getChildText('errMsg');
      if (errMsg && errMsg !== 'NORMAL SERVICE.') {
        Logger.log('API 에러: ' + errMsg);
        return null;
      }
    }
    
    const items = [];
    const bodyElement = root.getChild('body');
    if (bodyElement) {
      const itemsElement = bodyElement.getChild('items');
      if (itemsElement) {
        const itemList = itemsElement.getChildren('item');
        
        itemList.forEach(item => {
          items.push({
            title: item.getChildText('title') || '',
            registrationDate: item.getChildText('regDate') || item.getChildText('registDt') || '',
            url: BASE_URL
          });
        });
      }
    }
    
    return items;
    
  } catch (error) {
    Logger.log('API 호출 오류: ' + error);
    Logger.log('오류 상세: ' + error.stack);
    return null;
  }
}

function formatSheet(sheet) {
  // 헤더 스타일 설정
  const headerRange = sheet.getRange(1, 1, 1, 4);
  headerRange.setBackground('#f3f3f3')
             .setFontWeight('bold')
             .setHorizontalAlignment('center')
             .setVerticalAlignment('middle');
  
  // 데이터 영역 스타일 설정
  const dataRange = sheet.getDataRange();
  if (dataRange.getNumRows() > 1) {
    const dataStyle = sheet.getRange(2, 1, dataRange.getNumRows() - 1, 4);
    dataStyle.setVerticalAlignment('middle');
    
    // 각 열별 정렬 설정
    sheet.getRange(2, 1, dataRange.getNumRows() - 1, 1).setHorizontalAlignment('center'); // 기록시각
    sheet.getRange(2, 2, dataRange.getNumRows() - 1, 1).setHorizontalAlignment('center'); // 부처명
    sheet.getRange(2, 3, dataRange.getNumRows() - 1, 1).setHorizontalAlignment('left');   // 제목
    sheet.getRange(2, 4, dataRange.getNumRows() - 1, 1).setHorizontalAlignment('center'); // 등록일
  }
  
  // 열 너비 자동 조정
  sheet.autoResizeColumns(1, 4);
  
  // 최소/최대 열 너비 설정
  const columnWidths = [
    { col: 1, min: 150, max: 200 },  // 기록시각
    { col: 2, min: 100, max: 150 },  // 부처명
    { col: 3, min: 400, max: 800 },  // 제목
    { col: 4, min: 100, max: 150 }   // 등록일
  ];
  
  columnWidths.forEach(({col, min, max}) => {
    const currentWidth = sheet.getColumnWidth(col);
    if (currentWidth < min) {
      sheet.setColumnWidth(col, min);
    } else if (currentWidth > max) {
      sheet.setColumnWidth(col, max);
    }
  });
}

function writeToSheet(newsItems) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName("보도자료");
    
    if (!sheet) {
      sheet = ss.insertSheet("보도자료");
    }
    
    if (sheet.getLastRow() === 0) {
      sheet.appendRow(['기록시각', '부처명', '제목', '등록일']);
    }
    
    const currentTime = Utilities.formatDate(new Date(), "Asia/Seoul", "yyyy-MM-dd HH:mm:ss");
    
    if (!newsItems || newsItems.length === 0) {
      sheet.appendRow([
        currentTime,
        '중소벤처기업부',
        '게시물 등록 없음',
        getYesterdayDate()
      ]);
    } else {
      newsItems.forEach(item => {
        const title = `=HYPERLINK("${item.url}", "${item.title}")`;
        sheet.appendRow([
          currentTime,
          '중소벤처기업부',
          title,
          item.registrationDate || '-'
        ]);
      });
    }
    
    // 시트 서식 적용
    formatSheet(sheet);
    
  } catch (error) {
    Logger.log('시트 작성 오류: ' + error);
    Logger.log('오류 상세: ' + error.stack);
  }
}

function main() {
  try {
    const newsItems = fetchNewsData();
    writeToSheet(newsItems);
  } catch (error) {
    Logger.log('실행 오류: ' + error);
    Logger.log('오류 상세: ' + error.stack);
  }
}

수정과정을 거쳐 구글시트에 제대로 된 데이트가 기록되기 시작

사전에 매일 6시 실행을 프롬프트에 삽입해서 트리거 실행함수가 별도 생성

-createDailyTrigger 함수 실행하면 트리거가 저장됨


function createDailyTrigger() {
  try {
    const triggers = ScriptApp.getProjectTriggers();
    triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));
    
    ScriptApp.newTrigger('main')
      .timeBased()
      .atHour(6)
      .everyDays(1)
      .create();
      
    Logger.log('트리거가 성공적으로 설정되었습니다.');
  } catch (error) {
    Logger.log('트리거 설정 오류: ' + error);
    Logger.log('오류 상세: ' + error.stack);
  }
}‎​

결과와 배운 점

‎​앱스 스크립트는 클로드가 더 나은 편의성과 성능이 좋은 것으로 판단됨

---

이전 사례글 댓글로 지적해주신 공공포털 api가 제공되지 않는 부처 처리방안을 며칠에 걸쳐 시도하였습니다. 웹페이지 스크래핑을 위해 다양한 방법으로 시도하였으나, 모두 실패하였습니다. 추측컨대 표로 표현된 웹데이터를 가져와서 정리하는 부분이 쉽지 않아 보입니다.

api를 이용한 사이트 사례를 먼저 완성한 이후 다시 방법을 찾아서 해결하고 글 올리도록 하겠습니다

4
2개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요