πŸ”— ꡬ글 μŠ€ν”„λ ˆλ“œ μ‹œνŠΈ-λ…Έμ…˜ WBS μžλ™ν™” 처리

차슈·2025λ…„ 10μ›” 1일
1
post-thumbnail

1. Notion μ€€λΉ„

1) Notion API 토큰

  • Notionμ—μ„œ Internal Integration을 생성해
    Secret Token = 프라이빗 API 톡합 μ‹œν¬λ¦Ώ λ°œκΈ‰ (μ›Œν¬μŠ€νŽ˜μ΄μŠ€ μƒμ„±μžλ§Œ κ°€λŠ₯)

  • 토큰은 ntn_ 으둜 μ‹œμž‘

  • 이 토큰은 톡합에 곡유된 νŽ˜μ΄μ§€/DB만 μ ‘κ·Ό κ°€λŠ₯
    토큰 생성 μ£Όμ†Œ

2) κΆŒν•œ / μ—­ν• 

  • 게슀트 계정이라도 κ°€λŠ₯ν•˜κΈ΄ ν•˜μ§€λ§Œ, ν•΄λ‹Ή DBλ₯Ό 톡합에 κ³΅μœ ν•΄μ•Όν•©λ‚˜λ””γ….
    • DB μ†Œμœ μž/κ΄€λ¦¬μžμ—κ²Œ μš”μ²­: DB 우츑 상단 곡유 -> μ—°κ²° -> λ…Έμ…˜ 톡합을 μ΄ˆλŒ€
    • κ³΅μœ κ°€ λ˜μ§€ μ•ŠλŠ”λ‹€λ©΄ 404 μ—λŸ¬ λ©”μ„Έμ§€κ°€ λœΉλ‹ˆλ‹€!

3) Database ID (인라인 DB 포함) 확인

  • DB νŽ˜μ΄μ§€λ₯Ό μ—° λ’€ νŽ˜μ΄μ§€ μ£Όμ†Œμ— λ³΄μ΄λŠ” 32자 IDλ₯Ό μ‚¬μš©
  • ?v= 뒀에 순수 IDλ₯Ό μ‚¬μš©
    • APIμ—μ„œλŠ” ν•˜μ΄ν”ˆ 포함 ID ν˜•νƒœ(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)κ°€ ν•„μš”.
      • EX) 24ed5864-227a-8032-b957-fdc14deca543

4) 속성 섀계

  • μ„€κ³„λŠ” μž‘μ„±μžμ˜ λ§ˆμŒλŒ€λ‘œ λ…Έμ…˜ -> λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό λ§Œλ“€μ–΄μ„œ μž‘μ„±ν•΄λ„ λ©λ‹ˆλ‹€.
  • μž‘μ—… λ‚ μ§œ λ―Έμž…λ ₯μ‹œμ— WBS에 μžλ™μœΌλ‘œ 연동이 λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€!
  • 업무 λ‚΄μš©: Title(νŽ˜μ΄μ§€ 제λͺ©)
  • μƒνƒœ: Status νƒ€μž…(사전 μ •μ˜λœ μ˜΅μ…˜ μ‚¬μš©)
  • 파트: Multi-select(기획/λ””μžμΈ/ν”„λ‘ νŠΈμ—”λ“œ/λ°±μ—”λ“œ λ“±)
  • λ‹΄λ‹Ήμž: People
  • μž‘μ—… λ‚ μ§œ: Date(μ‹œμž‘~μ’…λ£Œ λ²”μœ„ μ‚¬μš©)

2. Google Sheets μ€€λΉ„

1) 빈 μŠ€ν”„λ ˆλ“œμ‹œνŠΈλ₯Ό μ€€λΉ„

  • 빈 μŠ€ν”„λ ˆλ“œμ‹œνŠΈμ— ν‘œλΆ€ν„° λ‹€ λ§Œλ“€μ–΄μ£ΌκΈ° λ•Œλ¬Έμ— λ°˜λ“œμ‹œ λΉ„μ–΄μžˆλŠ” μŠ€ν”„λ ˆλ“œ μ‹œνŠΈμ—¬μ•Όν•©λ‹ˆλ‹€.

2) Apps Script

  • μŠ€ν”„λ ˆλ“œμ‹œνŠΈμ—μ„œ ν™•μž₯ ν”„λ‘œκ·Έλž¨ -> Apps Script 클릭
  • μŠ€ν¬λ¦½ν”„ νŒŒμΌμ— 둜직 μž‘μ„±

'NOTION_TOKEN': notion scret token => ntn으둜 μ‹œμž‘ν•˜λŠ” 것
'NOTION_DATABASE_TOKEN' : ν•˜μ΄ν”ˆμ΄ ν¬ν•¨λœ μ£Όμ†Œ

function getNotionDataWBSWithDates() {
  const token = 'NOTION_TOKEN'; 
  const databaseId = 'NOTION_DATABASE_TOKEN';
  
  const url = `https://api.notion.com/v1/databases/${databaseId}/query`;
  const options = {
    method: 'post',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Notion-Version': '2022-06-28',
      'Content-Type': 'application/json'
    }
  };

  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('μ‹œνŠΈ1');

  // ==== μ‹œνŠΈ μ΄ˆκΈ°ν™” ====
  sheet.getRange(1,1,sheet.getMaxRows(), sheet.getMaxColumns()).breakApart();
  sheet.clearContents();
  sheet.clearFormats();

  // ==== κΈ°λ³Έ WBS μ—΄ 생성 ====
  const headers = ['ꡬ뢄', '업무 λ‚΄μš©', 'μƒνƒœ', '파트', 'λ‹΄λ‹Ήμž'];
  // 이건 λ…Έμ…˜μ—μ„œ μƒμ„±ν•œ db의 이름과 λ˜‘κ°™μ•„μ•Όν•©λ‹ˆλ‹€! 
  const startDate = new Date("2025-09-29");
  const endDate = new Date("2025-11-29");

  // λ‚ μ§œ 헀더 생성
  const dateHeaders = [];
  let currentDate = new Date(startDate);
  while(currentDate <= endDate){
    dateHeaders.push(`${currentDate.getMonth()+1}μ›” ${currentDate.getDate()}일`);
    currentDate.setDate(currentDate.getDate() + 1);
  }

  // 헀더 κ°’ μ„€μ •
  sheet.getRange(1, 1, 1, headers.length + dateHeaders.length).setValues([headers.concat(dateHeaders)]);
  sheet.getRange(1,1,1,headers.length + dateHeaders.length).setBackground('#D9D9D9');
  sheet.getRange(1,1,1,headers.length + dateHeaders.length).setFontWeight('bold');
  sheet.getRange(1,1,1,headers.length + dateHeaders.length).setHorizontalAlignment('center');

  // λ‚ μ§œ 헀더 폰트 크기 쀄이기
  sheet.getRange(1, headers.length + 1, 1, dateHeaders.length).setFontSize(8);

  // μ—΄ λ„ˆλΉ„ μ§€μ •
  sheet.setColumnWidth(1, 100); // ꡬ뢄
  sheet.setColumnWidth(2, 300); // 업무 λ‚΄μš©
  sheet.setColumnWidth(3, 100); // μƒνƒœ
  sheet.setColumnWidth(4, 120); // 파트
  sheet.setColumnWidth(5, 150); // λ‹΄λ‹Ήμž
  for(let i=0;i<dateHeaders.length;i++){
    sheet.setColumnWidth(headers.length + i + 1, 50); // λ‚ μ§œ μ—΄ λ„ˆλΉ„ κ³ μ •
  }

  // ==== λ‹΄λ‹Ήμž 이름 λ§€ν•‘ ====
  const assigneeMap = {
    'λ‹΄λ‹Ήμž λ…Έμ…˜μ΄λ¦„': 'WBS에 μž…λ ₯될 이름',
 
  };

  // ==== ꡬ뢄 ν‚€μ›Œλ“œ ====
  const sections = ['기획 및 섀계', 'λ””μžμΈ', '개발'];
  let row = 2;

  sections.forEach(section => {
    const sectionStartRow = row;

    const filtered = data.results.filter(page => {
      const partArray = page.properties['파트']?.multi_select?.map(p => p.name) || [];
      const start = page.properties['μž‘μ—… λ‚ μ§œ']?.date?.start;
      if (!start) return false; // λ‚ μ§œ μ—†λŠ” ν•­λͺ© μ œμ™Έ
      if(section === '기획 및 섀계') return partArray.includes('기획');
      if(section === 'λ””μžμΈ') return partArray.includes('λ””μžμΈ');
      if(section === '개발') return partArray.some(p => ['ν”„λ‘ νŠΈμ—”λ“œ','λ°±μ—”λ“œ'].includes(p));
      return false;
    });

    filtered.forEach(page => {
      const properties = page.properties;
      const task = properties['업무 λ‚΄μš©']?.title[0]?.plain_text || '';
      const status = properties['μƒνƒœ']?.status?.name || '';
      let part = properties['파트']?.multi_select?.map(p => p.name).join(', ') || '';

      // λ‹΄λ‹Ήμž 이름 λ³€ν™˜
      let assignees = '';
      if(properties['λ‹΄λ‹Ήμž']?.people){
        assignees = properties['λ‹΄λ‹Ήμž'].people
          .map(p => assigneeMap[p.name] || p.name)
          .join(', ');
      }

      // μž‘μ—… λ‚ μ§œ λ²”μœ„
      const start = properties['μž‘μ—… λ‚ μ§œ']?.date?.start;
      const end = properties['μž‘μ—… λ‚ μ§œ']?.date?.end;
      let workDates = [];
      if(start){
        let s = new Date(start);
        let e = end ? new Date(end) : new Date(start);
        if(e < s) e = new Date(start); // start > end λ°©μ§€
        while(s <= e){
          workDates.push(`${s.getMonth()+1}μ›” ${s.getDate()}일`);
          s.setDate(s.getDate() + 1);
        }
      }

      // ν–‰ 생성
      sheet.getRange(row,1,1,headers.length).setValues([[section, task, status, part, assignees]]);
      sheet.getRange(row,1,1,headers.length).setBackground('#FFFFFF'); // κΈ°λ³Έ 흰색

      // μž‘μ—… λ‚ μ§œ 색칠
      const sectionColor = section === '기획 및 섀계' ? '#FFC7C7' :
                           section === 'λ””μžμΈ' ? '#C7D9FF' :
                           '#FFFAC7'; // 개발
      workDates.forEach(d => {
        const dateCol = headers.length + dateHeaders.indexOf(d) + 1;
        if(dateCol > headers.length){
          sheet.getRange(row,dateCol).setBackground(sectionColor);
        }
      });

      row++; // ν•œ ν–‰ 증가
    });

    const sectionEndRow = row - 1;
    if(sectionEndRow >= sectionStartRow){
      sheet.getRange(sectionStartRow,1,sectionEndRow-sectionStartRow+1,1).merge();
      sheet.getRange(sectionStartRow,1).setValue(section);
      sheet.getRange(sectionStartRow,1,sectionEndRow-sectionStartRow+1,1).setBackground('#D9D9D9');
      sheet.getRange(sectionStartRow,1,sectionEndRow-sectionStartRow+1,1).setFontWeight('bold');
      sheet.getRange(sectionStartRow,1,sectionEndRow-sectionStartRow+1,1).setHorizontalAlignment('center');
    }
  });
}
  • 색상 변경도 κ°€λŠ₯ν•˜κ³  μ»€μŠ€ν…€λ„ κ°€λŠ₯ν•©λ‹ˆλ‹€!

3) μžλ™ 트리거

  • μ‚¬μ΄λ“œλ°”μ—μ„œ 트리거 클릭

  • μ‹€ν–‰ν•¨μˆ˜ : 슀크립트 ν•¨μˆ˜ 이름
  • 이벀트 μ†ŒμŠ€μ„ νƒ : μ‹œκ°„κΈ°λ°˜
  • μ‹œκ°„ μœ ν˜• 선택: νŒ€ 상황에 맞게 선택
  • λΆ„ 간격 선택: νŒ€ 상황에 맞게 선택

4) 초기 μ‹€ν–‰μ‹œ κΆŒν•œ / κ²½κ³ 

  • 트리거 μ„€μ •ν•˜κ³  μ €μž₯을 λˆ„λ₯΄λ©΄ κΆŒν•œμœΌλ‘œ 인해 μ‹€νŒ¨κ°€ 뜰 수 있음
  • Googleμ—μ„œ ν™•μΈν•˜μ§€ μ•ŠλŠ” μ•± κ²½κ³ !
  • κ³ κΈ‰ (advance) -> 이동 (unsafe) -> ν—ˆμš©
    이 μˆœμ„œλŒ€λ‘œ ν•˜λ©΄ 무리 없이 μ§„ν–‰ κ°€λŠ₯ν•©λ‹ˆλ‹€.

5) κ²°κ³Ό

  • λ…Έμ…˜ λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ μž…λ ₯ν•˜λ©΄ μžλ™μœΌλ‘œ μŠ€ν”„λ ˆλ“œμ‹œνŠΈμ— λ°˜μ˜λ©λ‹ˆλ‹€!
  • κΈ°/λ””/개발 λͺ¨λ‘κ°€ νŽΈν•˜κ²Œ WBS μžλ™μœΌλ‘œ ν”„λ‘œμ νŠΈ ν•˜μ„Έμš”! μ•„μ£Ό νŽΈν•©λ‹ˆλ‹€:)

0개의 λŒ“κΈ€