Renumber 12 existing skills to new ranges: - SEO: 11→13, 12→18, 13→16, 14→17, 15→14, 16→15, 17→29, 18→30, 19→12 - GTM: 20→60, 21→61, 22→62 Update cross-references in gateway architect/builder skills, GTM guardian README, CLAUDE.md (skill tables + directory layout), and AGENTS.md (domain routing ranges). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.4 KiB
7.4 KiB
Phase 7: Event Lookup & Parameter Library Web App
Google Apps Script를 활용한 Event Taxonomy Sheet 기반 업무 지원용 앱 배포.
Objectives
- Event Taxonomy 데이터 조회 앱
- 실시간 파라미터 참조 도구
- 비개발자용 Self-service 도구
- 팀 간 일관된 이벤트 정보 공유
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
- Google Sheets 원본 데이터 수정
- Web App 자동 반영 (실시간)
Code Update
- Apps Script 수정
- Deploy > Manage deployments
- 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으로 직접 삽입.