Files
our-claude-skills/custom-skills/22-gtm-guardian/code/references/phase7-lookup-app.md
Andrew Yim b859d0a266 feat(gtm-guardian): Reorganize skill with dual-platform structure
- Add desktop/ directory for Claude Desktop (Phase 1-5: analysis, design, docs)
- Add code/ directory for Claude Code (Phase 6-7: automation, audit)
- Create SKILL.md with YAML frontmatter for Desktop compatibility
- Create CLAUDE.md for Code automation workflows
- Organize references by platform scope with shared files duplicated
- Add templates for tagging plan and event taxonomy
- Include README.md with overview and usage guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 19:59:22 +09:00

7.4 KiB

Phase 7: Event Lookup & Parameter Library Web App

Google Apps Script를 활용한 Event Taxonomy Sheet 기반 업무 지원용 앱 배포.

Objectives

  1. Event Taxonomy 데이터 조회 앱
  2. 실시간 파라미터 참조 도구
  3. 비개발자용 Self-service 도구
  4. 팀 간 일관된 이벤트 정보 공유

App Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Google Sheets  │────▶│  Apps Script    │────▶│   Web App       │
│  (Taxonomy DB)  │     │  (Backend)      │     │  (Frontend)     │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Google Sheets Data Source

Required Sheets

  • Events Master
  • Parameters Reference
  • Custom Definitions
  • Platform Mapping

Column Requirements

→ Phase 4 참조: phase4-taxonomy.md

Apps Script Setup

1. Project Creation

1. Google Sheets 열기
2. Extensions > Apps Script
3. 프로젝트 이름 설정

2. Core Functions

Data Retrieval

function getEventsData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Events Master');
  const data = sheet.getDataRange().getValues();
  const headers = data[0];
  
  return data.slice(1).map(row => {
    const obj = {};
    headers.forEach((header, i) => {
      obj[header] = row[i];
    });
    return obj;
  });
}

function getParametersData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName('Parameters Reference');
  const data = sheet.getDataRange().getValues();
  const headers = data[0];
  
  return data.slice(1).map(row => {
    const obj = {};
    headers.forEach((header, i) => {
      obj[header] = row[i];
    });
    return obj;
  });
}

Search Function

function searchEvents(query) {
  const events = getEventsData();
  const searchTerm = query.toLowerCase();
  
  return events.filter(event => 
    event.event_name.toLowerCase().includes(searchTerm) ||
    event.event_category.toLowerCase().includes(searchTerm) ||
    event.notes.toLowerCase().includes(searchTerm)
  );
}

function getEventDetails(eventName) {
  const events = getEventsData();
  const params = getParametersData();
  
  const event = events.find(e => e.event_name === eventName);
  if (!event) return null;
  
  const eventParams = event.parameters.split(',').map(p => p.trim());
  const paramDetails = params.filter(p => 
    eventParams.includes(p.parameter_name)
  );
  
  return {
    event: event,
    parameters: paramDetails
  };
}

3. Web App Handler

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('GTM Event Lookup')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function doPost(e) {
  const action = e.parameter.action;
  
  switch(action) {
    case 'search':
      return ContentService.createTextOutput(
        JSON.stringify(searchEvents(e.parameter.query))
      ).setMimeType(ContentService.MimeType.JSON);
      
    case 'details':
      return ContentService.createTextOutput(
        JSON.stringify(getEventDetails(e.parameter.eventName))
      ).setMimeType(ContentService.MimeType.JSON);
      
    default:
      return ContentService.createTextOutput(
        JSON.stringify({error: 'Invalid action'})
      ).setMimeType(ContentService.MimeType.JSON);
  }
}

4. HTML Frontend (index.html)

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: 'Roboto', sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
    .search-box { width: 100%; padding: 12px; font-size: 16px; margin-bottom: 20px; }
    .event-card { border: 1px solid #ddd; padding: 16px; margin: 10px 0; border-radius: 8px; }
    .event-name { font-size: 18px; font-weight: bold; color: #1a73e8; }
    .param-badge { background: #e8f0fe; color: #1967d2; padding: 4px 8px; border-radius: 4px; margin: 2px; display: inline-block; }
    .category { color: #5f6368; font-size: 14px; }
    .priority-p1 { border-left: 4px solid #ea4335; }
    .priority-p2 { border-left: 4px solid #fbbc04; }
    .priority-p3 { border-left: 4px solid #34a853; }
  </style>
</head>
<body>
  <h1>🏷️ GTM Event Lookup</h1>
  <input type="text" class="search-box" id="searchInput" placeholder="이벤트명 또는 카테고리 검색...">
  <div id="results"></div>
  
  <script>
    const searchInput = document.getElementById('searchInput');
    const results = document.getElementById('results');
    
    searchInput.addEventListener('input', debounce(function() {
      const query = this.value;
      if (query.length < 2) {
        results.innerHTML = '';
        return;
      }
      
      google.script.run
        .withSuccessHandler(displayResults)
        .searchEvents(query);
    }, 300));
    
    function displayResults(events) {
      results.innerHTML = events.map(event => `
        <div class="event-card priority-${event.priority.toLowerCase()}">
          <div class="event-name">${event.event_name}</div>
          <div class="category">${event.event_category} | ${event.priority}</div>
          <div style="margin-top: 10px;">
            ${event.parameters.split(',').map(p => 
              `<span class="param-badge">${p.trim()}</span>`
            ).join('')}
          </div>
          <div style="margin-top: 10px; color: #5f6368;">${event.notes}</div>
        </div>
      `).join('');
    }
    
    function debounce(func, wait) {
      let timeout;
      return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
      };
    }
  </script>
</body>
</html>

Deployment

1. Deploy as Web App

1. Deploy > New deployment
2. Type: Web app
3. Execute as: Me
4. Access: Anyone within organization (또는 Anyone)
5. Deploy

2. Get Web App URL

배포 후 제공되는 URL 복사
https://script.google.com/macros/s/[ID]/exec

3. Share with Team

  • URL을 Notion/Slack에 공유
  • 북마크 권장

Features Roadmap

Phase 1 (MVP)

  • 이벤트 검색
  • 파라미터 조회
  • 우선순위 필터

Phase 2

  • DataLayer 코드 스니펫 복사
  • 플랫폼별 코드 예시
  • 최근 조회 히스토리

Phase 3

  • 이벤트 추가/수정 폼
  • 변경 이력 추적
  • Slack 알림 연동

Maintenance

Data Update

  1. Google Sheets 원본 데이터 수정
  2. Web App 자동 반영 (실시간)

Code Update

  1. Apps Script 수정
  2. Deploy > Manage deployments
  3. New version 배포

Backup

  • Google Sheets 자동 버전 히스토리
  • 주기적 수동 백업 권장

Access Control

Role Access Level
Admin 시트 편집 + 스크립트 수정
Editor 시트 편집
Viewer Web App 조회만

Troubleshooting

Issue Solution
앱 로딩 느림 데이터 캐싱 구현
검색 결과 없음 검색어 확인, 데이터 존재 확인
권한 오류 배포 설정에서 접근 권한 확인
데이터 미반영 시트 새로고침, 캐시 클리어

Integration with Notion

Web App URL을 Notion에 embed:

/embed [Web App URL]

또는 iframe으로 직접 삽입.