(보수공사)나의 전세물건 좀 살려줘~

소개

시도하고자 했던 것과 그 이유를 알려주세요.

말 그대로다... 분양받은 집을 입주기간때문에 전세가 빠지지 않아서 머리털(쿨록 ㅠㅠ) 아니 머리가 아퍼서 지끈거린다. ㅜㅜ

그래서 나의 괴로움을 구글한테 던져보기로 하였다.

진행 방법

어떤 도구를 사용했고, 어떻게 활용하셨나요?

** 사용도구 : Gemini AI 2.5 , 구글앱스스크립트

  1. 제미나이한테 프롬프트를 던져주고 설계구상을 해보았다.

## 시나리오배경

1. 나는 나의 전세물건을 빨리 처리하고 싶어. 고객은 부동산 중개인이고 나의 전세물건을 잘 홍보해줄 내용의 홍보 메일을 자동으로 발급하는 시스템이 필요해.  

2. 고객(부동산 중개인)은 나의 전세물건 홍보메일을 보고 제공된 Google Forms 링크를 통해, 자신의 정보(연락처 및 필요정보 등)을 기입하고 전세물건을 등록하기 위한 정보(공급계약서, 주민번호 뒷번호를 삭제한 신분증 사본, 전세집 위치주소)를 자동으로 받을수 있는 링크를  자동으로 받을 수 있어.

3. 부동산 중개인을 크롤링 해서 활동량과 계약 건수가 높은 부동산 중개인 연락처(이름,이메일,중개소주소, 활동건수)를 제공된 Google Sheet에 저장하고 매주 나한테 메일주소로 정리해서 보내줘

4. 이 시스템은 홍보메일 발신 및 수신여부를 응답데이터를 Google Sheets에 저장 및 관리해주길 원하고  고객(부동산 중개인)이 홍보메일을 회신을 하면 그것을 날짜별로 기록하고 내메일로 자동정리 해주는  발송하는 기능을 갖추길 원해.

 4. 이메일 본문에 견적서를그냥텍스트형태로넣어주고. 견적서로직및양식은알아서코드상에넣어줘. 샘플형식으로.

 5. 구글 forms도 코드상으로자동으로생성해줘야해.

 6. 트리거도코드상으로자동으로설정되어야하고.



 ## 기초부터설명해주고, 해당코드를제공해줘.

 1. 어떤 파일을만들어야하는지

2. 각 파일에어떤코드를넣어야하는지 친절하게단계별로알려줘

흐흐 스터디장님이 만들어서 공유해주것에서 딱 2분 투자해서 바꾸어보았다. 내가 괴로우면 안되잖아.... 나소중한 머리털(?, 쿨럭 ㅠㅠ).

그래서 제미나이님께서 친절한 피드백을 주셨다!

Google 앱은 한국어를 형성합니다
Google Adwords Korea -Google Adwords Korea -Google Adwords
Google 애드워즈
/**
 * Google Forms를 자동으로 생성하고, Forms URL을 Google Sheet에 업데이트합니다.
 */
function createAndConfigureGoogleForm() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const configSheet = ss.getSheetByName('환경 설정');
  const formsUrlCell = configSheet.getRange('B4'); // Google Forms URL 셀

  // 이전에 생성된 폼이 있는지 확인 (선택 사항, 매번 새로 생성하거나 기존 폼 재활용)
  // 여기서는 매번 새로 생성하는 방식으로 구현합니다.
  const form = FormApp.create('전세 물건 등록 신청 (부동산 중개인용)');

  form.setTitle('전세 물건 등록 신청 (부동산 중개인용)')
      .setDescription('관심 있는 전세 물건에 대한 정보를 확인하고, 등록을 위한 서류를 제출해주세요.');

  // 1. 이름 (단답형 텍스트)
  form.addTextItem()
      .setTitle('부동산 중개인 성함')
      .setRequired(true);

  // 2. 연락처 (전화번호)
  form.addTextItem()
      .setTitle('연락처 (휴대폰 번호)')
      .setValidation(FormApp.createTextValidation()
          .requirePattern(FormApp.TextPattern.NUMBER, '숫자만 입력해주세요.')
          .build())
      .setRequired(true);

  // 3. 이메일 주소
  form.addTextItem()
      .setTitle('이메일 주소')
      .setValidation(FormApp.createTextValidation()
          .requirePattern(FormApp.TextPattern.EMAIL, '유효한 이메일 주소를 입력해주세요.')
          .build())
      .setRequired(true);

  // 4. 중개소명
  form.addTextItem()
      .setTitle('소속 중개소명')
      .setRequired(true);

  // 5. 전세 물건 등록 희망 (선택 질문)
  form.addMultipleChoiceItem()
      .setTitle('본 전세 물건 등록을 희망하십니까?')
      .setChoiceValues(['네', '아니오'])
      .setRequired(true);

  // 6. 첨부 파일 업로드 안내 (텍스트 설명)
  // 실제 파일 업로드는 Google Forms에서 직접적으로 지원되지 않으므로,
  // Google Drive 폴더 링크를 안내하고 그곳에 업로드하도록 유도합니다.
  // 또는, 이메일로 회신하도록 유도하는 방법도 있습니다.
  // 여기서는 파일 업로드 대신 관련 정보를 입력받는 방식으로 대체합니다.
  form.addSectionHeaderItem()
      .setTitle('필수 서류 제출 안내');
  form.addParagraphTextItem()
      .setTitle('전세 물건 등록을 위해 아래 서류를 준비해 주시고, 제출 방식은 개별적으로 안내해 드릴 예정입니다.')
      .setHelpText('제출 서류: 공급계약서 사본, 주민등록번호 뒷자리를 가린 신분증 사본, 전세집 위치 주소');
      
  // 중요: Google Forms의 파일 업로드 기능은 응답자가 Google Drive에 파일을 업로드하는 방식입니다.
  // 이 기능을 사용하려면 폼 생성 시 직접 설정해야 합니다.
  // Apps Script로는 파일 업로드 질문을 직접 추가하기 어렵습니다.
  // 따라서 위와 같이 안내 텍스트로 대신하거나, 응답 이메일로 별도 제출하도록 유도하는 것이 현실적입니다.
  // 만약 꼭 Forms 내에서 파일 업로드를 원한다면, 폼 생성 후 수동으로 해당 질문을 추가해야 합니다.


  const formUrl = form.getPublishedUrl();
  formsUrlCell.setValue(formUrl); // Google Sheet에 Forms URL 저장

  Logger.log('Google Form이 성공적으로 생성되었습니다: ' + formUrl);
}

// 이 함수를 한 번 실행하여 Google Forms를 생성하고 URL을 얻으세요.
// 스크립트 편집기에서 'createAndConfigureGoogleForm' 함수를 선택하고 실행 버튼을 누릅니다.

아 길다... 약간 후회가 되기 시작한다.... 오늘 사례글이 없다는 경고문구 카톡과 함께 후다닥 달려본다.

Google Docs Korean Google Docs Korean Google Docs Korean Google Docs Kor

/**
 * 전세 물건 홍보 메일을 부동산 중개인에게 발송합니다.
 */
function sendPromotionEmails() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const agentSheet = ss.getSheetByName('부동산 중개인');
  const logSheet = ss.getSheetByName('메일 발송 기록');
  const configSheet = ss.getSheetByName('환경 설정');

  const agents = agentSheet.getDataRange().getValues(); // 모든 중개인 데이터 가져오기
  const formsUrl = configSheet.getRange('B4').getValue(); // Google Forms URL 가져오기
  const subject = configSheet.getRange('B3').getValue(); // 홍보 메일 제목 가져오기

  // 첫 번째 행은 헤더이므로 건너뛰기
  for (let i = 1; i < agents.length; i++) {
    const agent = agents[i];
    const agentName = agent[0]; // 이름
    const agentEmail = agent[1]; // 이메일
    const officeAddress = agent[2]; // 중개소 주소 (현재 사용하지 않지만, 나중에 활용 가능)

    if (!agentEmail || !isValidEmail(agentEmail)) {
      Logger.log(`유효하지 않은 이메일 주소: ${agentEmail} (${agentName}). 이메일을 발송하지 않습니다.`);
      logMailStatus(agentEmail, false, '유효하지 않은 이메일 주소');
      continue;
    }

    const emailBody = createEmailBody(agentName, formsUrl);

    try {
      GmailApp.sendEmail(agentEmail, subject, '', {
        htmlBody: emailBody
      });
      Logger.log(`이메일 성공적으로 발송: ${agentEmail}`);
      logMailStatus(agentEmail, true, '');
    } catch (e) {
      Logger.log(`이메일 발송 실패 (${agentEmail}): ${e.message}`);
      logMailStatus(agentEmail, false, e.message);
    }
    // API 호출 제한을 피하기 위해 잠시 대기
    Utilities.sleep(1000); // 1초 대기
  }
}

/**
 * 이메일 본문을 생성합니다. (견적서 포함)
 * @param {string} agentName 부동산 중개인 이름
 * @param {string} formsUrl Google Forms URL
 * @returns {string} HTML 형식의 이메일 본문
 */
function createEmailBody(agentName, formsUrl) {
  const myEmail = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('환경 설정').getRange('B2').getValue();

  // 견적서 로직 및 양식 (샘플)
  const quotation = `
    <h3 style="color: #333;">[전세 물건] 역세권 신축 아파트 전세 견적서</h3>
    <table border="1" style="width:100%; border-collapse: collapse; margin-bottom: 20px;">
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">물건명</td>
        <td style="padding: 8px;">OOO 아파트 OOO동 OOO호</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">위치</td>
        <td style="padding: 8px;">서울특별시 강남구 테헤란로 123 (역삼동)</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">공급 면적</td>
        <td style="padding: 8px;">84.99 m² (구 25.7평)</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">전용 면적</td>
        <td style="padding: 8px;">59.99 m² (구 18.1평)</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">총 층수 / 해당 층</td>
        <td style="padding: 8px;">20층 / 15층</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">방 수 / 욕실 수</td>
        <td style="padding: 8px;">3개 / 2개</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">입주 가능일</td>
        <td style="padding: 8px;">즉시 입주 (협의 가능)</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">전세 보증금</td>
        <td style="padding: 8px; color: #e74c3c; font-weight: bold;">5억 5천만원</td>
      </tr>
      <tr>
        <td style="padding: 8px; background-color: #f2f2f2;">특징</td>
        <td style="padding: 8px;">역세권, 신축, 채광 좋음, 공원 인접, 주차 편리</td>
      </tr>
    </table>
  `;

  return `
    <p>안녕하세요, ${agentName} 공인중개사님.</p>
    <p>전세 물건 홍보를 위한 협력 제안 메일 드립니다.</p>
    <p>현재 저희가 보유하고 있는 매력적인 전세 물건이 있어, 중개사님의 귀한 고객분들께 소개해 드리고자 합니다.</p>
    
    ${quotation}
    
    <p>본 물건은 뛰어난 입지와 신축 건물로 높은 임차 수요가 예상됩니다.</p>
    <p>자세한 정보 확인 및 물건 등록을 원하시면 아래 Google Forms 링크를 통해 정보를 기입해주시면 감사하겠습니다.</p>
    <p><a href="${formsUrl}" style="display: inline-block; padding: 10px 20px; background-color: #28a745; color: white; text-decoration: none; border-radius: 5px;">전세 물건 등록 신청하기</a></p>
    <br>
    <p>궁금한 점이 있으시면 언제든지 회신 부탁드립니다.</p>
    <p>감사합니다.</p>
    <br>
    <p><strong>[본인 이름]</strong> 드림</p>
    <p>문의: ${myEmail}</p>
  `;
}

/**
 * 메일 발송 기록을 Google Sheet에 기록합니다.
 * @param {string} recipientEmail 수신자 이메일 주소
 * @param {boolean} success 발송 성공 여부
 * @param {string} errorMessage 오류 메시지 (실패 시)
 */
function logMailStatus(recipientEmail, success, errorMessage) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const logSheet = ss.getSheetByName('메일 발송 기록');
  const now = new Date();
  logSheet.appendRow([now, recipientEmail, success ? '성공' : '실패', '', '', errorMessage]);
}

/**
 * 유효한 이메일 형식인지 확인합니다.
 * @param {string} email 이메일 주소
 * @returns {boolean} 유효성 여부
 */
function isValidEmail(email) {
  const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return emailRegex.test(String(email).toLowerCase());
}
흰색 배경을 가진 한국 캐릭터의 사진

이건 멀까 ㅠㅠ

// 주요 함수들을 호출하는 메인 함수 (필요에 따라 이름을 변경하여 사용)
function main() {
  // 1. Google Forms 생성 및 URL 업데이트 (최초 1회 또는 폼 변경 시 실행)
  // createAndConfigureGoogleForm(); // 이 함수는 Forms가 준비되면 주석 처리하거나 수동 실행하세요.

  // 2. 부동산 중개인에게 홍보 메일 발송
  sendPromotionEmails();

  // 3. 부동산 중개인 활동량 크롤링 및 업데이트 (아래에서 구현 예정)
  // crawlAndSaveAgentData(); // 수동 실행 또는 별도 트리거 설정

  // 4. 회신 메일 처리 (아래에서 구현 예정)
  // processRepliedEmails(); // 트리거로 주기적으로 실행
}
한국어 텍스트가있는 페이지
/**
 * 보낸 메일에 대한 회신 메일을 확인하고 Google Sheet에 기록합니다.
 * 이 함수는 주기적으로 (예: 매일) 실행되도록 트리거를 설정해야 합니다.
 */
function processRepliedEmails() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const logSheet = ss.getSheetByName('메일 발송 기록');
  const myEmail = ss.getSheetByName('환경 설정').getRange('B2').getValue();

  const logData = logSheet.getDataRange().getValues();
  const header = logData[0];
  const emailColumnIndex = header.indexOf('수신자 이메일');
  const replyStatusColumnIndex = header.indexOf('회신 여부');
  const replyContentColumnIndex = header.indexOf('회신 내용');

  // 첫 번째 행 (헤더) 제외
  for (let i = 1; i < logData.length; i++) {
    const row = logData[i];
    const recipientEmail = row[emailColumnIndex];
    const currentReplyStatus = row[replyStatusColumnIndex];

    // 이미 회신이 확인되었거나, 유효하지 않은 이메일이면 건너뛰기
    if (currentReplyStatus === '회신 확인' || !recipientEmail) {
      continue;
    }

    // 해당 수신자로부터의 회신 메일 검색
    // 보낸 메일의 ThreadId를 알면 더 정확하지만, 여기서는 단순화하여 검색합니다.
    const threads = GmailApp.search(`from:${recipientEmail} to:${myEmail} in:inbox after:${Utilities.formatDate(new Date(row[0]), Session.getScriptTimeZone(), 'yyyy/MM/dd HH:mm:ss')}`);

    if (threads.length > 0) {
      // 가장 최근 회신 메일의 내용 가져오기 (원하는 방식에 따라 조절)
      const latestMessage = threads[0].getMessages().pop(); // 스레드의 마지막 메시지
      const messageBody = latestMessage.getPlainBody().substring(0, 500); // 500자까지 저장

      logSheet.getRange(i + 1, replyStatusColumnIndex + 1).setValue('회신 확인');
      logSheet.getRange(i + 1, replyContentColumnIndex + 1).setValue(messageBody);
      Logger.log(`회신 확인 및 기록 완료: ${recipientEmail}`);

      // 본인에게 회신 메일 도착 알림 (선택 사항)
      // MailApp.sendEmail(myEmail, `[자동 알림] ${recipientEmail}님으로부터 전세 물건 회신 메일 도착`,
      //   `전세 물건 홍보 메일에 대해 ${recipientEmail}님으로부터 회신이 도착했습니다.\n\n내용:\n${messageBody}`);

    }
  }
}
한국과 영어로 된 숫자 목록

미안해 제미나이야..... 간단하게 할껄 꺼이꺼이~ ㅜㅜ

한국어 텍스트가있는 페이지
흑백 글꼴의 흑백 이미지
/**
 * (주의: 매우 제한적이며, 실제 웹사이트에 따라 수정이 필수)
 * 특정 웹사이트에서 부동산 중개인 정보를 크롤링하여 Google Sheet에 저장합니다.
 * 이 함수는 웹사이트의 HTML 구조를 정확히 알아야 하며, 동적 콘텐츠는 가져오기 어렵습니다.
 */
function crawlAndSaveAgentData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const agentSheet = ss.getSheetByName('부동산 중개인');

  // 크롤링할 웹사이트 URL (예시: 실제 웹사이트 URL로 변경 필요)
  const targetUrl = 'http://www.example-realestate.com/agents';

  try {
    const response = UrlFetchApp.fetch(targetUrl);
    const htmlContent = response.getContentText();

    // HTML 파싱 (정규 표현식 예시 - 실제 웹사이트 구조에 맞게 수정 필요)
    // 이 예시는 매우 단순하며, 실제 복잡한 HTML에서는 사용하기 어렵습니다.
    const agentRegex = /<div class="agent-card">.*?<h3 class="agent-name">(.*?)<\/h3>.*?<p class="agent-email">(.*?)<\/p>.*?<p class="agent-address">(.*?)<\/p>.*?<span class="activity-count">활동: (.*?)<\/span>.*?<span class="contract-count">계약: (.*?)<\/span>.*?<\/div>/g;
    let match;
    const newAgents = [];

    while ((match = agentRegex.exec(htmlContent)) !== null) {
      const name = match[1].trim();
      const email = match[2].trim();
      const address = match[3].trim();
      const activityCount = parseInt(match[4], 10) || 0;
      const contractCount = parseInt(match[5], 10) || 0;

      newAgents.push([name, email, address, activityCount, contractCount]);
    }

    if (newAgents.length > 0) {
      // 기존 데이터 유지하고 새로운 데이터 추가 (중복 방지 로직 필요)
      // 여기서는 단순히 새로운 데이터를 추가합니다. 실제로는 중복 확인 후 업데이트해야 합니다.
      newAgents.forEach(agent => {
        agentSheet.appendRow(agent);
      });
      Logger.log(`${newAgents.length}명의 중개인 정보가 업데이트되었습니다.`);
    } else {
      Logger.log('새로운 중개인 정보를 찾을 수 없습니다.');
    }

  } catch (e) {
    Logger.log(`크롤링 실패: ${e.message}`);
  }
}

/**
 * 매주 활동량과 계약 건수를 기준으로 상위 중개인을 이메일로 발송합니다.
 */
function sendWeeklyAgentReport() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const agentSheet = ss.getSheetByName('부동산 중개인');
  const myEmail = ss.getSheetByName('환경 설정').getRange('B2').getValue();

  const agents = agentSheet.getDataRange().getValues();
  if (agents.length <= 1) {
    Logger.log('중개인 데이터가 없습니다.');
    return;
  }

  const header = agents[0];
  const data = agents.slice(1); // 헤더 제외

  // 활동 건수 및 계약 건수 기준으로 정렬 (내림차순)
  data.sort((a, b) => {
    const activityA = parseInt(a[3], 10) || 0;
    const activityB = parseInt(b[3], 10) || 0;
    const contractA = parseInt(a[4], 10) || 0;
    const contractB = parseInt(b[4], 10) || 0;

    // 계약 건수가 높은 순, 다음으로 활동 건수가 높은 순
    if (contractA !== contractB) {
      return contractB - contractA;
    }
    return activityB - activityA;
  });

  // 상위 10명 또는 모든 중개인 정보 포함
  const topAgents = data.slice(0, Math.min(data.length, 10)); // 상위 10명만

  let reportBody = '<h3>주간 부동산 중개인 활동 보고서</h3>';
  reportBody += '<p>활동량과 계약 건수가 높은 상위 중개인 목록입니다.</p>';
  reportBody += '<table border="1" style="width:100%; border-collapse: collapse;">';
  reportBody += '<tr>' +
                '<th style="padding: 8px; background-color: #f2f2f2;">이름</th>' +
                '<th style="padding: 8px; background-color: #f2f2f2;">이메일</th>' +
                '<th style="padding: 8px; background-color: #f2f2f2;">중개소 주소</th>' +
                '<th style="padding: 8px; background-color: #f2f2f2;">활동 건수</th>' +
                '<th style="padding: 8px; background-color: #f2f2f2;">계약 건수</th>' +
                '</tr>';

  topAgents.forEach(agent => {
    reportBody += '<tr>' +
                  `<td style="padding: 8px;">${agent[0]}</td>` +
                  `<td style="padding: 8px;">${agent[1]}</td>` +
                  `<td style="padding: 8px;">${agent[2]}</td>` +
                  `<td style="padding: 8px;">${agent[3]}</td>` +
                  `<td style="padding: 8px;">${agent[4]}</td>` +
                  '</tr>';
  });
  reportBody += '</table>';
  reportBody += `<p>보고서: ${ss.getUrl()}</p>`;

  MailApp.sendEmail(myEmail, '[자동 보고서] 주간 부동산 중개인 활동 요약', '', { htmlBody: reportBody });
  Logger.log('주간 중개인 활동 보고서가 성공적으로 발송되었습니다.');
}
흰색 배경에 일련의 숫자와 글자

이론 미친.... 나한테 복수하냐? 시간이 없다고 구현까지 해야 되는뎅 ㅠㅠ

한국어 단어가있는 페이지
한국어로 된 Google 양식 페이지의 스크린 샷

흠...

음...

엄...

흑...

악...

** 담주까지 구현까지 해보겠습니다. 꾸벅~



여기에서 시도를 해보다가 큰 문제점이 맞닿되게 되어서 급하게 수정합니다. 내용은 다음과 같습니다.

무서운 말이군요... 법적인 문제(?) 그래서 방향을 틉니다.

"부동산 중개인 활동량 및 계약 건수 크롤링" 대신, 공공데이터 API를 활용하여 전세 물건 주변의 실거래가 정보를 가져와 홍보 메일에 포함하는 기능으로 변경하고자 합니다.

숫자와 문자 형태의 한국 숫자

.... 업데이트 중

(25.6/3일자) 과연 업데이트가 되었을까? 당연히 아니다 안전히 잊어먹고 있었다. 업데이트 중... 거짓말도 참 잘한다. 이론이론~




결과와 배운 점

배운 점과 나만의 꿀팁을 알려주세요.

  1. 사례를 쓰고자 제미나이 괴롭히기 금지

  2. 새로운 아이디어를 도출하더라도 PRD 작성하는 단계는 거칠 것

  3. 오늘만 사는 직딩한테 AI는 저희 상사(?) 그래도 담주까지 구현하고 싶은 의지를 불태워줌.

    도움 받은 글 (옵션)

참고한 지피터스 글이나 외부 사례를 알려주세요.

스터디장님의 경고문구 이번주 스터디글 "0"개!!! 독려작렬.... 이따뵈요

스터디장님의 경고문구 이번주 스터디글 딱 "2"개!!! 독려문구 발사..... 커피마시다가 쿨럭

1

뉴스레터 무료 구독