Compare commits
3 Commits
34c3a1df4f
...
6ac547e78f
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ac547e78f | |||
| 08cb20fc67 | |||
| c9bdbb57f7 |
@@ -204,7 +204,7 @@ directly loadable. Migrate incrementally, not in bulk.
|
|||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
name: skill-name-kebab-case # letters, numbers, hyphens only
|
name: skill-name-kebab-case # clean name: dir minus the NN- prefix
|
||||||
description: |
|
description: |
|
||||||
What it does + when to use. Triggers: keyword1, keyword2, 한국어 트리거.
|
What it does + when to use. Triggers: keyword1, keyword2, 한국어 트리거.
|
||||||
---
|
---
|
||||||
@@ -214,6 +214,9 @@ description: |
|
|||||||
Content starts here...
|
Content starts here...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Naming convention:** the directory keeps its `NN-` prefix for ordering
|
||||||
|
(`16-seo-schema-validator/`), but the skill `name:` is the **clean** form without it
|
||||||
|
(`name: seo-schema-validator`). Names must be kebab-case and globally unique.
|
||||||
Whole frontmatter ≤ 1024 chars. Full rules + migration recipe: `reference/SKILL-MIGRATION-GUIDE.md`.
|
Whole frontmatter ≤ 1024 chars. Full rules + migration recipe: `reference/SKILL-MIGRATION-GUIDE.md`.
|
||||||
|
|
||||||
## Directory Layout
|
## Directory Layout
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 01-ourdigital-brand-guide
|
name: ourdigital-brand-guide
|
||||||
description: |
|
description: |
|
||||||
OurDigital 브랜드 기준 및 스타일 가이드 참조 스킬.
|
OurDigital 브랜드 기준 및 스타일 가이드 참조 스킬.
|
||||||
Activated with "ourdigital" keyword for brand-related queries.
|
Activated with "ourdigital" keyword for brand-related queries.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 02-ourdigital-blog
|
name: ourdigital-blog
|
||||||
description: |
|
description: |
|
||||||
Korean blog draft creation for blog.ourdigital.org.
|
Korean blog draft creation for blog.ourdigital.org.
|
||||||
Activated with "ourdigital" keyword for blog writing tasks.
|
Activated with "ourdigital" keyword for blog writing tasks.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 03-ourdigital-journal
|
name: ourdigital-journal
|
||||||
description: |
|
description: |
|
||||||
English essay and article creation for journal.ourdigital.org.
|
English essay and article creation for journal.ourdigital.org.
|
||||||
Activated with "ourdigital" keyword for English writing tasks.
|
Activated with "ourdigital" keyword for English writing tasks.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 04-ourdigital-research
|
name: ourdigital-research
|
||||||
description: |
|
description: |
|
||||||
Deep research and structured prompt generation for OurDigital workflows.
|
Deep research and structured prompt generation for OurDigital workflows.
|
||||||
Activated with "ourdigital" keyword for research tasks.
|
Activated with "ourdigital" keyword for research tasks.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 05-ourdigital-document
|
name: ourdigital-document
|
||||||
description: |
|
description: |
|
||||||
Notion-to-presentation workflow for OurDigital.
|
Notion-to-presentation workflow for OurDigital.
|
||||||
Activated with "ourdigital" keyword for document creation.
|
Activated with "ourdigital" keyword for document creation.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 06-ourdigital-designer
|
name: ourdigital-designer
|
||||||
description: |
|
description: |
|
||||||
Visual storytelling and image prompt generation for OurDigital.
|
Visual storytelling and image prompt generation for OurDigital.
|
||||||
Activated with "ourdigital" keyword for design tasks.
|
Activated with "ourdigital" keyword for design tasks.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 07-ourdigital-ad-manager
|
name: ourdigital-ad-manager
|
||||||
description: |
|
description: |
|
||||||
Ad copywriting and keyword research for OurDigital marketing.
|
Ad copywriting and keyword research for OurDigital marketing.
|
||||||
Activated with "ourdigital" keyword for advertising tasks.
|
Activated with "ourdigital" keyword for advertising tasks.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 08-ourdigital-trainer
|
name: ourdigital-trainer
|
||||||
description: |
|
description: |
|
||||||
Training material creation and workshop planning for OurDigital.
|
Training material creation and workshop planning for OurDigital.
|
||||||
Activated with "ourdigital" keyword for education tasks.
|
Activated with "ourdigital" keyword for education tasks.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 09-ourdigital-backoffice
|
name: ourdigital-backoffice
|
||||||
description: |
|
description: |
|
||||||
Business document creation for OurDigital consulting services.
|
Business document creation for OurDigital consulting services.
|
||||||
Activated with "ourdigital" keyword for business documents.
|
Activated with "ourdigital" keyword for business documents.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 10-ourdigital-skill-creator
|
name: ourdigital-skill-creator
|
||||||
description: |
|
description: |
|
||||||
Meta skill for creating and managing OurDigital Claude Skills.
|
Meta skill for creating and managing OurDigital Claude Skills.
|
||||||
Activated when user includes "ourdigital" keyword with skill creation requests.
|
Activated when user includes "ourdigital" keyword with skill creation requests.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 11-seo-comprehensive-audit
|
name: seo-comprehensive-audit
|
||||||
description: |
|
description: |
|
||||||
Comprehensive SEO audit orchestrator. Runs 6-stage audit pipeline (Technical, On-Page, CWV, Schema, Local, GSC) and produces a unified report with weighted health score.
|
Comprehensive SEO audit orchestrator. Runs 6-stage audit pipeline (Technical, On-Page, CWV, Schema, Local, GSC) and produces a unified report with weighted health score.
|
||||||
Triggers: comprehensive SEO, full SEO audit, 종합 SEO 감사, site audit, SEO health check.
|
Triggers: comprehensive SEO, full SEO audit, 종합 SEO 감사, site audit, SEO health check.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 12-seo-technical-audit
|
name: seo-technical-audit
|
||||||
description: |
|
description: |
|
||||||
Technical SEO analyzer for robots.txt, sitemap, and crawlability fundamentals.
|
Technical SEO analyzer for robots.txt, sitemap, and crawlability fundamentals.
|
||||||
Triggers: technical SEO, robots.txt, sitemap validation, crawlability, URL accessibility.
|
Triggers: technical SEO, robots.txt, sitemap validation, crawlability, URL accessibility.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 13-seo-on-page-audit
|
name: seo-on-page-audit
|
||||||
description: |
|
description: |
|
||||||
On-page SEO analyzer for meta tags, headings, links, images, and Open Graph.
|
On-page SEO analyzer for meta tags, headings, links, images, and Open Graph.
|
||||||
Triggers: on-page SEO, meta tags, title tag, heading structure, alt text.
|
Triggers: on-page SEO, meta tags, title tag, heading structure, alt text.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 14-seo-core-web-vitals
|
name: seo-core-web-vitals
|
||||||
description: |
|
description: |
|
||||||
Core Web Vitals analyzer for LCP, FID, CLS, and INP optimization recommendations.
|
Core Web Vitals analyzer for LCP, FID, CLS, and INP optimization recommendations.
|
||||||
Triggers: Core Web Vitals, page speed, LCP optimization, CLS fix, INP analysis.
|
Triggers: Core Web Vitals, page speed, LCP optimization, CLS fix, INP analysis.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 15-seo-search-console
|
name: seo-search-console
|
||||||
description: |
|
description: |
|
||||||
Google Search Console data analyzer for performance, queries, and index coverage.
|
Google Search Console data analyzer for performance, queries, and index coverage.
|
||||||
Triggers: Search Console, GSC analysis, search performance, rankings, CTR optimization.
|
Triggers: Search Console, GSC analysis, search performance, rankings, CTR optimization.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 16-seo-schema-validator
|
name: seo-schema-validator
|
||||||
description: |
|
description: |
|
||||||
Validates an AUTHORED JSON-LD schema dataset (pre-deployment QA) and audits
|
Validates an AUTHORED JSON-LD schema dataset (pre-deployment QA) and audits
|
||||||
live structured data (post-deployment). Runs a 5-layer offline validation
|
live structured data (post-deployment). Runs a 5-layer offline validation
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 17-seo-schema-generator
|
name: seo-schema-generator
|
||||||
description: |
|
description: |
|
||||||
Generates validation-ready JSON-LD structured data for a site, covering BOTH
|
Generates validation-ready JSON-LD structured data for a site, covering BOTH
|
||||||
scenarios: (1) from an existing website — extract facts from live pages; and
|
scenarios: (1) from an existing website — extract facts from live pages; and
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 18-seo-local-audit
|
name: seo-local-audit
|
||||||
description: |
|
description: |
|
||||||
Local business SEO auditor for Korean-market businesses. Covers business identity extraction,
|
Local business SEO auditor for Korean-market businesses. Covers business identity extraction,
|
||||||
NAP consistency, Google Business Profile, Naver Smart Place, Kakao Map, local citations,
|
NAP consistency, Google Business Profile, Naver Smart Place, Kakao Map, local citations,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 19-seo-keyword-strategy
|
name: seo-keyword-strategy
|
||||||
description: |
|
description: |
|
||||||
Keyword strategy and research for SEO campaigns.
|
Keyword strategy and research for SEO campaigns.
|
||||||
Triggers: keyword research, keyword analysis, keyword gap, search volume,
|
Triggers: keyword research, keyword analysis, keyword gap, search volume,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 20-seo-serp-analysis
|
name: seo-serp-analysis
|
||||||
description: |
|
description: |
|
||||||
SERP analysis for Google and Naver search results.
|
SERP analysis for Google and Naver search results.
|
||||||
Triggers: SERP analysis, search results, featured snippet, SERP features, Naver SERP, 검색결과 분석, SERP 분석.
|
Triggers: SERP analysis, search results, featured snippet, SERP features, Naver SERP, 검색결과 분석, SERP 분석.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 21-seo-position-tracking
|
name: seo-position-tracking
|
||||||
description: |
|
description: |
|
||||||
Keyword position tracking for keyword ranking monitoring.
|
Keyword position tracking for keyword ranking monitoring.
|
||||||
Triggers: rank tracking, position monitoring, keyword rankings, visibility score, ranking report, 키워드 순위, 순위 추적.
|
Triggers: rank tracking, position monitoring, keyword rankings, visibility score, ranking report, 키워드 순위, 순위 추적.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 22-seo-link-building
|
name: seo-link-building
|
||||||
description: |
|
description: |
|
||||||
Link building diagnosis and backlink analysis tool.
|
Link building diagnosis and backlink analysis tool.
|
||||||
Triggers: backlink audit, link building, referring domains, toxic links, link gap, broken backlinks, 백링크 분석, 링크빌딩.
|
Triggers: backlink audit, link building, referring domains, toxic links, link gap, broken backlinks, 백링크 분석, 링크빌딩.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 23-seo-content-strategy
|
name: seo-content-strategy
|
||||||
description: |
|
description: |
|
||||||
Content strategy and planning for SEO. Triggers: content audit, content strategy, content gap, topic clusters, content brief, editorial calendar, content decay, 콘텐츠 전략, 콘텐츠 감사.
|
Content strategy and planning for SEO. Triggers: content audit, content strategy, content gap, topic clusters, content brief, editorial calendar, content decay, 콘텐츠 전략, 콘텐츠 감사.
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 24-seo-ecommerce
|
name: seo-ecommerce
|
||||||
description: |
|
description: |
|
||||||
E-commerce SEO audit and optimization for product pages, product schema, category taxonomy,
|
E-commerce SEO audit and optimization for product pages, product schema, category taxonomy,
|
||||||
and Korean marketplace presence.
|
and Korean marketplace presence.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 25-seo-kpi-framework
|
name: seo-kpi-framework
|
||||||
description: |
|
description: |
|
||||||
SEO KPI and performance framework for unified metrics, health scores, ROI, and period-over-period reporting.
|
SEO KPI and performance framework for unified metrics, health scores, ROI, and period-over-period reporting.
|
||||||
Triggers: SEO KPI, performance report, health score, SEO metrics, ROI,
|
Triggers: SEO KPI, performance report, health score, SEO metrics, ROI,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 26-seo-international
|
name: seo-international
|
||||||
description: |
|
description: |
|
||||||
International SEO audit and hreflang validation for multi-language and multi-region websites.
|
International SEO audit and hreflang validation for multi-language and multi-region websites.
|
||||||
Triggers: hreflang, international SEO, multi-language, multi-region, content parity, x-default, ccTLD, 다국어 SEO.
|
Triggers: hreflang, international SEO, multi-language, multi-region, content parity, x-default, ccTLD, 다국어 SEO.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 27-seo-ai-visibility
|
name: seo-ai-visibility
|
||||||
description: |
|
description: |
|
||||||
AI search visibility and brand radar monitoring. Tracks how a brand appears
|
AI search visibility and brand radar monitoring. Tracks how a brand appears
|
||||||
in AI-generated search answers using our-seo-agent CLI or pre-fetched data.
|
in AI-generated search answers using our-seo-agent CLI or pre-fetched data.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 28-seo-knowledge-graph
|
name: seo-knowledge-graph
|
||||||
description: |
|
description: |
|
||||||
Knowledge Graph and entity SEO analysis.
|
Knowledge Graph and entity SEO analysis.
|
||||||
Triggers: knowledge panel, entity SEO, knowledge graph, PAA, FAQ schema,
|
Triggers: knowledge panel, entity SEO, knowledge graph, PAA, FAQ schema,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 29-seo-gateway-architect
|
name: seo-gateway-architect
|
||||||
description: |
|
description: |
|
||||||
Gateway page strategy planner for keyword research, content architecture, and SEO KPIs.
|
Gateway page strategy planner for keyword research, content architecture, and SEO KPIs.
|
||||||
Triggers: SEO strategy, gateway pages, keyword research, content architecture.
|
Triggers: SEO strategy, gateway pages, keyword research, content architecture.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 30-seo-gateway-builder
|
name: seo-gateway-builder
|
||||||
description: |
|
description: |
|
||||||
Gateway page content builder with templates, schema markup, and local SEO optimization.
|
Gateway page content builder with templates, schema markup, and local SEO optimization.
|
||||||
Triggers: build gateway page, create landing page, local service page, location pages.
|
Triggers: build gateway page, create landing page, local service page, location pages.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 31-notion-organizer
|
name: notion-organizer
|
||||||
description: |
|
description: |
|
||||||
Notion workspace manager for database optimization, property cleanup, and bulk operations.
|
Notion workspace manager for database optimization, property cleanup, and bulk operations.
|
||||||
Triggers: organize Notion, workspace cleanup, database schema, property standardization.
|
Triggers: organize Notion, workspace cleanup, database schema, property standardization.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 31-seo-competitor-intel
|
name: seo-competitor-intel
|
||||||
description: |
|
description: |
|
||||||
Competitor intelligence and SEO benchmarking.
|
Competitor intelligence and SEO benchmarking.
|
||||||
Triggers: competitor analysis, competitive intelligence, competitor comparison,
|
Triggers: competitor analysis, competitive intelligence, competitor comparison,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 32-notion-writer
|
name: notion-writer
|
||||||
description: |
|
description: |
|
||||||
Markdown to Notion page writer with database row creation support.
|
Markdown to Notion page writer with database row creation support.
|
||||||
Triggers: write to Notion, export to Notion, push content, create Notion page.
|
Triggers: write to Notion, export to Notion, push content, create Notion page.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 32-seo-crawl-budget
|
name: seo-crawl-budget
|
||||||
description: |
|
description: |
|
||||||
Crawl budget optimization and server log analysis for search engine bots.
|
Crawl budget optimization and server log analysis for search engine bots.
|
||||||
Triggers: crawl budget, log analysis, bot crawling, Googlebot, crawl waste,
|
Triggers: crawl budget, log analysis, bot crawling, Googlebot, crawl waste,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 33-seo-migration-planner
|
name: seo-migration-planner
|
||||||
description: |
|
description: |
|
||||||
SEO site migration planning and monitoring. Triggers: site migration, domain move, redirect mapping, platform migration, URL restructuring, HTTPS migration, subdomain consolidation, 사이트 이전, 도메인 이전, 리디렉트 매핑.
|
SEO site migration planning and monitoring. Triggers: site migration, domain move, redirect mapping, platform migration, URL restructuring, HTTPS migration, subdomain consolidation, 사이트 이전, 도메인 이전, 리디렉트 매핑.
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 34-seo-reporting-dashboard
|
name: seo-reporting-dashboard
|
||||||
description: |
|
description: |
|
||||||
SEO reporting dashboard and executive report generation. Aggregates data from all SEO skills
|
SEO reporting dashboard and executive report generation. Aggregates data from all SEO skills
|
||||||
into stakeholder-ready reports and interactive HTML dashboards.
|
into stakeholder-ready reports and interactive HTML dashboards.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 40-jamie-brand-editor
|
name: jamie-brand-editor
|
||||||
description: |
|
description: |
|
||||||
Jamie Plastic Surgery branded content generator for blog posts and marketing.
|
Jamie Plastic Surgery branded content generator for blog posts and marketing.
|
||||||
Triggers: write Jamie blog, Jamie content, brand copywriting, 제이미 콘텐츠.
|
Triggers: write Jamie blog, Jamie content, brand copywriting, 제이미 콘텐츠.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 41-jamie-brand-audit
|
name: jamie-brand-audit
|
||||||
description: |
|
description: |
|
||||||
Jamie Plastic Surgery brand compliance reviewer and content evaluator.
|
Jamie Plastic Surgery brand compliance reviewer and content evaluator.
|
||||||
Triggers: review content, brand audit, 제이미 브랜드 검토, tone and manner check.
|
Triggers: review content, brand audit, 제이미 브랜드 검토, tone and manner check.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 42-jamie-faq-entry
|
name: jamie-faq-entry
|
||||||
description: "카카오톡 플러스 채널 Kanana 상담매니저 Q&A 답변 생성 및 검토 스킬. 제이미성형외과의 카카오톡 채널에 등록할 고객 문의 질문과 답변 엔트리를 생성, 검토, 수정합니다. 의료광고 심의 준수, 브랜드 보이스 일관성, 카카오 카나나 가이드 규격을 모두 반영합니다. Triggers: 카나나 답변, Kanana Q&A, 카카오톡 챗봇, 카카오 상담 답변, 챗봇 문답, 자동답변 등록, 카나나 엔트리, chatbot QA, KakaoTalk channel reply, 카카오 자동응답. jamie-marketing-editor 및 jamie-brand-guardian 스킬과 연계하여 사용합니다."
|
description: "카카오톡 플러스 채널 Kanana 상담매니저 Q&A 답변 생성 및 검토 스킬. 제이미성형외과의 카카오톡 채널에 등록할 고객 문의 질문과 답변 엔트리를 생성, 검토, 수정합니다. 의료광고 심의 준수, 브랜드 보이스 일관성, 카카오 카나나 가이드 규격을 모두 반영합니다. Triggers: 카나나 답변, Kanana Q&A, 카카오톡 챗봇, 카카오 상담 답변, 챗봇 문답, 자동답변 등록, 카나나 엔트리, chatbot QA, KakaoTalk channel reply, 카카오 자동응답. jamie-marketing-editor 및 jamie-brand-guardian 스킬과 연계하여 사용합니다."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 43-jamie-youtube-manager
|
name: jamie-youtube-manager
|
||||||
description: |
|
description: |
|
||||||
Jamie Clinic YouTube channel SEO auditor and content manager.
|
Jamie Clinic YouTube channel SEO auditor and content manager.
|
||||||
Triggers: YouTube SEO, video audit, 제이미 유튜브, channel optimization.
|
Triggers: YouTube SEO, video audit, 제이미 유튜브, channel optimization.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 44-jamie-youtube-subtitle-checker
|
name: jamie-youtube-subtitle-checker
|
||||||
description: |
|
description: |
|
||||||
SBV subtitle file typo corrector and YouTube metadata generator for Jamie Clinic.
|
SBV subtitle file typo corrector and YouTube metadata generator for Jamie Clinic.
|
||||||
Triggers: check subtitles, subtitle QA, SBV correction, 자막 교정.
|
Triggers: check subtitles, subtitle QA, SBV correction, 자막 교정.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 45-jamie-instagram-manager
|
name: jamie-instagram-manager
|
||||||
description: |
|
description: |
|
||||||
Jamie Clinic Instagram account manager for engagement, content planning, and boost strategy.
|
Jamie Clinic Instagram account manager for engagement, content planning, and boost strategy.
|
||||||
Triggers: Instagram management, 제이미 인스타그램, IG strategy, social media.
|
Triggers: Instagram management, 제이미 인스타그램, IG strategy, social media.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 46-jamie-journal-editor
|
name: jamie-journal-editor
|
||||||
description: |
|
description: |
|
||||||
Jamie Clinic journal/blog content editor for "정기호의 성형외과 진료실 이야기" (journal.jamie.clinic).
|
Jamie Clinic journal/blog content editor for "정기호의 성형외과 진료실 이야기" (journal.jamie.clinic).
|
||||||
Creates educational medical blog posts in Dr. Jung's authentic voice with Korean medical ad compliance.
|
Creates educational medical blog posts in Dr. Jung's authentic voice with Korean medical ad compliance.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 47-jamie-marketing-editor
|
name: jamie-marketing-editor
|
||||||
description: |
|
description: |
|
||||||
Jamie Clinic marketing content editor for digital channels, ads, communications, and internal docs.
|
Jamie Clinic marketing content editor for digital channels, ads, communications, and internal docs.
|
||||||
Creates compliant marketing copy for website, blog, SNS, ads, and patient communications.
|
Creates compliant marketing copy for website, blog, SNS, ads, and patient communications.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 50-notebooklm-agent
|
name: notebooklm-agent
|
||||||
description: |
|
description: |
|
||||||
Q&A agent for NotebookLM notebooks. Ask questions and get grounded, citation-backed answers from your sources.
|
Q&A agent for NotebookLM notebooks. Ask questions and get grounded, citation-backed answers from your sources.
|
||||||
Triggers: ask NotebookLM, query notebook, research question, 노트북 질문, NotebookLM 에이전트.
|
Triggers: ask NotebookLM, query notebook, research question, 노트북 질문, NotebookLM 에이전트.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 51-notebooklm-automation
|
name: notebooklm-automation
|
||||||
description: |
|
description: |
|
||||||
Complete NotebookLM automation for notebooks, sources, and artifacts management.
|
Complete NotebookLM automation for notebooks, sources, and artifacts management.
|
||||||
Triggers: manage NotebookLM, create notebook, add sources, 노트북 관리, NotebookLM 자동화.
|
Triggers: manage NotebookLM, create notebook, add sources, 노트북 관리, NotebookLM 자동화.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 52-notebooklm-studio
|
name: notebooklm-studio
|
||||||
description: |
|
description: |
|
||||||
Content generation for NotebookLM Studio artifacts - podcasts, videos, quizzes, flashcards, and more.
|
Content generation for NotebookLM Studio artifacts - podcasts, videos, quizzes, flashcards, and more.
|
||||||
Triggers: create podcast, generate video, make quiz, 팟캐스트 만들기, 퀴즈 생성, NotebookLM 스튜디오.
|
Triggers: create podcast, generate video, make quiz, 팟캐스트 만들기, 퀴즈 생성, NotebookLM 스튜디오.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 53-notebooklm-research
|
name: notebooklm-research
|
||||||
description: |
|
description: |
|
||||||
Research and source discovery for NotebookLM. Web/Drive research, auto-import, and source text extraction.
|
Research and source discovery for NotebookLM. Web/Drive research, auto-import, and source text extraction.
|
||||||
Triggers: research topic, find sources, web research, 리서치, 자료 조사, NotebookLM 연구.
|
Triggers: research topic, find sources, web research, 리서치, 자료 조사, NotebookLM 연구.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 60-gtm-audit
|
name: gtm-audit
|
||||||
description: |
|
description: |
|
||||||
GTM container audit using Chrome DevTools and DTM Agent for tag verification.
|
GTM container audit using Chrome DevTools and DTM Agent for tag verification.
|
||||||
Triggers: audit GTM, GTM analysis, tag debugging, dataLayer inspection.
|
Triggers: audit GTM, GTM analysis, tag debugging, dataLayer inspection.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 61-gtm-editor
|
name: gtm-editor
|
||||||
description: >
|
description: >
|
||||||
GTM implementation toolkit. Creates, updates, and modifies GTM tags, triggers,
|
GTM implementation toolkit. Creates, updates, and modifies GTM tags, triggers,
|
||||||
variables via API. Generates Custom HTML with ES5 compliance. Handles workspace
|
variables via API. Generates Custom HTML with ES5 compliance. Handles workspace
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 62-gtm-validator
|
name: gtm-validator
|
||||||
description: >
|
description: >
|
||||||
GTM QA and validation toolkit. Verifies tags fire correctly on live pages,
|
GTM QA and validation toolkit. Verifies tags fire correctly on live pages,
|
||||||
tests trigger conditions against actual DOM, validates dataLayer schemas,
|
tests trigger conditions against actual DOM, validates dataLayer schemas,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 70-dintel-brand-guardian
|
name: dintel-brand-guardian
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
canon_compliance: v1.3
|
canon_compliance: v1.3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 71-dintel-brand-editor
|
name: dintel-brand-editor
|
||||||
description: |
|
description: |
|
||||||
This skill should be used when the user asks to "write D.intelligence content",
|
This skill should be used when the user asks to "write D.intelligence content",
|
||||||
"create Magazine D. article", "evaluate brand compliance", "check brand tone",
|
"create Magazine D. article", "evaluate brand compliance", "check brand tone",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 72-dintel-doc-secretary
|
name: dintel-doc-secretary
|
||||||
description: |
|
description: |
|
||||||
D.intelligence Documentation Secretary. Format meeting notes, reports, proposals, and deliverables with brand-compliant templates. Triggers: "format meeting notes", "회의록 정리", "prepare deliverable", "산출물 준비", "format report", "리포트 포맷팅", "apply brand template", "브랜드 템플릿 적용", "convert to DOCX", "문서 변환", "quality check document", "문서 품질 검토", "meeting minutes", "의사록 작성", "document formatting", "문서 정리"
|
D.intelligence Documentation Secretary. Format meeting notes, reports, proposals, and deliverables with brand-compliant templates. Triggers: "format meeting notes", "회의록 정리", "prepare deliverable", "산출물 준비", "format report", "리포트 포맷팅", "apply brand template", "브랜드 템플릿 적용", "convert to DOCX", "문서 변환", "quality check document", "문서 품질 검토", "meeting minutes", "의사록 작성", "document formatting", "문서 정리"
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 73-dintel-quotation-mgr
|
name: dintel-quotation-mgr
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
canon_compliance: v1.3
|
canon_compliance: v1.3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 74-dintel-service-architect
|
name: dintel-service-architect
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
canon_compliance: v1.3
|
canon_compliance: v1.3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 75-dintel-marketing-mgr
|
name: dintel-marketing-mgr
|
||||||
description: |
|
description: |
|
||||||
This skill should be used when the user asks to "draft Magazine D. article",
|
This skill should be used when the user asks to "draft Magazine D. article",
|
||||||
"prepare newsletter", "draft LinkedIn post", "plan content calendar",
|
"prepare newsletter", "draft LinkedIn post", "plan content calendar",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 76-dintel-backoffice-mgr
|
name: dintel-backoffice-mgr
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
canon_compliance: v1.3
|
canon_compliance: v1.3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 77-dintel-account-mgr
|
name: dintel-account-mgr
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
last_updated: 2026-05-18
|
last_updated: 2026-05-18
|
||||||
canon_compliance: v1.3
|
canon_compliance: v1.3
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 79-dintel-skill-update
|
name: dintel-skill-update
|
||||||
description: |
|
description: |
|
||||||
Meta-agent for D.intelligence Agent Corps cross-skill consistency.
|
Meta-agent for D.intelligence Agent Corps cross-skill consistency.
|
||||||
Use when updating brand guide, pricing, service architecture, terms, or any shared reference
|
Use when updating brand guide, pricing, service architecture, terms, or any shared reference
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 80-claude-settings-optimizer
|
name: claude-settings-optimizer
|
||||||
description: |
|
description: |
|
||||||
Diagnose all Claude Desktop errors and optimize context usage.
|
Diagnose all Claude Desktop errors and optimize context usage.
|
||||||
Covers 8 error categories: context limits, output interruption,
|
Covers 8 error categories: context limits, output interruption,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 82-our-gdrive-organizer
|
name: our-gdrive-organizer
|
||||||
description: |
|
description: |
|
||||||
Organize a Google Drive folder under OurDigital conventions: refresh the
|
Organize a Google Drive folder under OurDigital conventions: refresh the
|
||||||
root README.md index, refresh per-subfolder README.md meta files, propose
|
root README.md index, refresh per-subfolder README.md meta files, propose
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 01-reference-discovery
|
name: reference-discovery
|
||||||
description: |
|
description: |
|
||||||
Search and discover authoritative reference sources with credibility validation.
|
Search and discover authoritative reference sources with credibility validation.
|
||||||
Triggers: find sources, search documentation, discover references, source validation.
|
Triggers: find sources, search documentation, discover references, source validation.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 02-web-crawler-orchestrator
|
name: web-crawler-orchestrator
|
||||||
description: |
|
description: |
|
||||||
Multi-backend web crawler with Firecrawl MCP, rate limiting, and format handling.
|
Multi-backend web crawler with Firecrawl MCP, rate limiting, and format handling.
|
||||||
Triggers: crawl URLs, fetch pages, scrape content, web crawler.
|
Triggers: crawl URLs, fetch pages, scrape content, web crawler.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 03-content-repository
|
name: content-repository
|
||||||
description: |
|
description: |
|
||||||
MySQL storage manager for reference library with versioning and deduplication.
|
MySQL storage manager for reference library with versioning and deduplication.
|
||||||
Triggers: store content, manage repository, document database, content storage.
|
Triggers: store content, manage repository, document database, content storage.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 04-content-distiller
|
name: content-distiller
|
||||||
description: |
|
description: |
|
||||||
Raw content summarizer extracting key concepts, code snippets, and structured output.
|
Raw content summarizer extracting key concepts, code snippets, and structured output.
|
||||||
Triggers: distill content, summarize document, extract key concepts, compress content.
|
Triggers: distill content, summarize document, extract key concepts, compress content.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 05-quality-reviewer
|
name: quality-reviewer
|
||||||
description: |
|
description: |
|
||||||
Content quality evaluator with multi-criteria scoring and decision routing.
|
Content quality evaluator with multi-criteria scoring and decision routing.
|
||||||
Triggers: review quality, score content, QA review, approve refactor reject.
|
Triggers: review quality, score content, QA review, approve refactor reject.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 06-markdown-exporter
|
name: markdown-exporter
|
||||||
description: |
|
description: |
|
||||||
Export approved content to markdown files or JSONL for fine-tuning.
|
Export approved content to markdown files or JSONL for fine-tuning.
|
||||||
Triggers: export markdown, generate files, create JSONL, export content.
|
Triggers: export markdown, generate files, create JSONL, export content.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 07-pipeline-orchestrator
|
name: pipeline-orchestrator
|
||||||
description: |
|
description: |
|
||||||
Full reference curation pipeline coordinator with QA loop and state management.
|
Full reference curation pipeline coordinator with QA loop and state management.
|
||||||
Triggers: run pipeline, orchestrate workflow, full curation, pipeline start.
|
Triggers: run pipeline, orchestrate workflow, full curation, pipeline start.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 90-reference-curator
|
name: reference-curator
|
||||||
description: |
|
description: |
|
||||||
Full reference-documentation curation pipeline: discover authoritative sources →
|
Full reference-documentation curation pipeline: discover authoritative sources →
|
||||||
crawl → store → distill → quality-review (QA loop) → export to markdown / project files
|
crawl → store → distill → quality-review (QA loop) → export to markdown / project files
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: 92-tui-design-template
|
name: tui-design-template
|
||||||
description: Build Norton Commander / Gopher style TUI wizard interfaces for CLI tools using Python Rich. Covers architecture, components, keyboard input, bilingual i18n, and battle-tested gotchas.
|
description: Build Norton Commander / Gopher style TUI wizard interfaces for CLI tools using Python Rich. Covers architecture, components, keyboard input, bilingual i18n, and battle-tested gotchas.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
triggers:
|
triggers:
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ headless Chrome, python-pptx. Create `data/` subfolder. Initialize `findings.jso
|
|||||||
→ write `03_presales-opportunity-brief.md`.
|
→ write `03_presales-opportunity-brief.md`.
|
||||||
|
|
||||||
## Stage 5 — Estimate (견적) — REVIEW GATE
|
## Stage 5 — Estimate (견적) — REVIEW GATE
|
||||||
- `python scripts/estimate.py --findings <out>/data/findings.json --rate-card references/rate_card.yaml --sow references/sow_templates.yaml --out-dir <out> --seq <N> [--baseline basic|treatment] [--billing 0.70]`
|
Pricing is delegated to the **`ourdigital-estimate-engine`** skill (`../96-ourdigital-estimate-engine`); this skill only maps findings → scope, then calls the engine:
|
||||||
- **Effort-based** (OurDigital real model): cost = role_rate × 청구율 × 표준 업무시간, by module; 제안가 = 합계 절사. findings auto-select a tier (**smb / basic / treatment**) and scale **On-page** hours sub-linearly by `subbrands_total` (cap ×2.0); Technical is fixed. `smb` tier bills at 0.55, others 0.70. Override with `--baseline` / `--billing`. Premium verticals (luxury/5성/특1급…) are auto-floored to `basic` via `rate_card.tiering` (so a luxury single property won't drop to `smb`).
|
- `python scripts/findings_to_scope.py --findings <out>/data/findings.json --out <out>/data/scope.json --seq <N> [--tier auto|smb|basic|treatment] [--billing 0.70]`
|
||||||
- Produces `05_estimate_ko.md`, `05_estimate.xlsx`, `data/estimate.json`. Present the 견적; get sign-off.
|
- `ENG=../96-ourdigital-estimate-engine; python $ENG/scripts/estimate.py --rate-card $ENG/references/rate_card.yaml --catalog-dir $ENG/catalog --scope <out>/data/scope.json --out-dir <out>`
|
||||||
- Logic in `references/findings_to_service.md`; rates/hours in `rate_card.yaml` + `sow_templates.yaml` (edit together). Reproduces real Basic ₩10.5M / Treatment ₩25.0M; SMB entry ~₩3M; chains stay SMB-acceptable (e.g. 25-property → ~₩29.5M).
|
- Engine (effort method, `seo` catalog): auto-tier (smb/basic/treatment) by portfolio + premium-vertical floor; On-page hours scale by `subbrands_total` (cap ×2.0); 제안가 = 합계 절사. Reproduces Basic ₩10.5M / Treatment ₩25.0M; SMB ~₩3M; 25-property chain ~₩29.5M.
|
||||||
|
- Produces `05_estimate_ko.md`, `05_estimate.xlsx`, `data/estimate.json` (effort shape → consumed by `build_deck.py`). Present the 견적; get sign-off.
|
||||||
|
- SEO findings→scope/tier mapping lives here (`findings_to_service.md`); **rates/hours/tiers live in the engine** (`rate_card.yaml` + `catalog/seo.yaml`) — edit pricing there, not here.
|
||||||
|
|
||||||
## Stage 6 — Deliverables — REVIEW GATE before send
|
## Stage 6 — Deliverables — REVIEW GATE before send
|
||||||
- **Client PDF**: author the short brief HTML from `templates/client_brief.html` (fill the content; keep the CSS),
|
- **Client PDF**: author the short brief HTML from `templates/client_brief.html` (fill the content; keep the CSS),
|
||||||
|
|||||||
@@ -44,5 +44,8 @@ markup if billed through us. VAT 별도 · 유효기간 14d · 현금 · 절사
|
|||||||
- `smb` single property → ~**3,000,000** (lean × 55% billing)
|
- `smb` single property → ~**3,000,000** (lean × 55% billing)
|
||||||
- chain example (SHR, 5 sub-brands ×1.6, treatment) → ~**29,500,000** (vs naive 71.5M)
|
- chain example (SHR, 5 sub-brands ×1.6, treatment) → ~**29,500,000** (vs naive 71.5M)
|
||||||
|
|
||||||
Edit `rate_card.yaml` + `sow_templates.yaml` together when rates, hours, tiers, or
|
**Pricing now lives in the `ourdigital-estimate-engine` skill** (`../96-ourdigital-estimate-engine`):
|
||||||
scaling change.
|
role rates/billing/tiering/scaling in `references/rate_card.yaml`, SEO tiers/hours in
|
||||||
|
`catalog/seo.yaml`. This skill maps findings → `scope.json` (`scripts/findings_to_scope.py`)
|
||||||
|
and calls the engine. Edit rates/hours/tiers **in the engine**, not here. This file documents
|
||||||
|
only the SEO findings→tier/scope mapping.
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# OurDigital / D.intelligence — rate card (effort-based)
|
|
||||||
# Source of truth: 06_Working Template 견적서 (Google Drive), 2026-05.
|
|
||||||
# Model: 비용 = 시간단가(role) × 청구율(billing_rate) × 표준 업무시간(hours).
|
|
||||||
# Estimates are assembled bottom-up from sow_templates.yaml. Keep PRIVATE.
|
|
||||||
|
|
||||||
company:
|
|
||||||
legal_name: "(주)디인텔리전스 (D.intelligence Lab)"
|
|
||||||
brand: "OurDigital"
|
|
||||||
ceo: "임명재"
|
|
||||||
contact: "info@ourdigital.org"
|
|
||||||
address: "경기도 성남시 수정구 창업로 43 판교글로벌비즈센터 업무동 1층 128호"
|
|
||||||
|
|
||||||
quote_prefix: OD # OD-YYYY-NNN
|
|
||||||
currency: KRW
|
|
||||||
|
|
||||||
billing_rate: 0.70 # 청구율 표준 70%
|
|
||||||
billing_rate_floor: 0.60 # 협상 시 -10%p 까지(>10%p 조정은 CEO/CFO 승인)
|
|
||||||
basis:
|
|
||||||
hours_per_day: 8 # 일 8시간
|
|
||||||
weeks_per_month: 4 # 월 4주
|
|
||||||
procurement_markup: 0.15 # 조달-관리 수수료 15% (외부 조달 물품/서비스)
|
|
||||||
rounding_unit: 500000 # 제안가 = 합계 절사. 50만원 단위가 실제 견적(25,340,000→25,000,000;
|
|
||||||
# 10,612,000→10,500,000)을 재현. 필요시 100000/1000000 으로 조정.
|
|
||||||
|
|
||||||
terms:
|
|
||||||
vat: "부가세 별도"
|
|
||||||
validity_days: 14 # 견적 유효기간(약 2주)
|
|
||||||
payment: "현금"
|
|
||||||
|
|
||||||
# 자동 티어링 보정. 프리미엄/럭셔리 prospect 는 규모가 작아도 entry(smb) 아래로
|
|
||||||
# 자동 선택되지 않도록 최소 티어를 보장. prospect.vertical 문자열에 아래 용어가
|
|
||||||
# 포함되면 프리미엄으로 간주(부분 일치, 대소문자 무시).
|
|
||||||
tiering:
|
|
||||||
premium_verticals: ["luxury", "premium", "deluxe", "5_star", "5성", "특1급", "boutique_luxury"]
|
|
||||||
premium_min_tier: basic # 프리미엄 단일 프로퍼티 최소 티어 (smb < basic < treatment)
|
|
||||||
|
|
||||||
# 시간 단가 (KRW/hr) — 직급 기준. sow_templates.yaml의 task.role 이 이 키를 참조.
|
|
||||||
role_rates:
|
|
||||||
ceo: 180000 # 대표
|
|
||||||
evp: 150000 # 전무
|
|
||||||
svp: 120000 # 상무
|
|
||||||
technical_advisor: 120000 # 기술고문
|
|
||||||
director: 100000 # 이사 / 본부장
|
|
||||||
senior_manager: 90000 # 부장 (Senior SEO·PM·Senior Analyst·Account Director(국장))
|
|
||||||
deputy_manager: 80000 # 차장
|
|
||||||
manager: 70000 # 과장 (SEO·Data Analyst·Data Engineer·Engineer|Developer)
|
|
||||||
assistant_manager: 60000 # 대리
|
|
||||||
junior: 50000 # 주임
|
|
||||||
associate: 30000 # 사원
|
|
||||||
intern: 12000 # 인턴
|
|
||||||
|
|
||||||
# 포트폴리오 규모에 따른 '시간' 스케일(서브선형). On-page(scale:true) task 에만 적용.
|
|
||||||
# driver = findings.entity 의 카운트. 체인은 페이지 템플릿을 공유하므로 '프로퍼티 수'가 아닌
|
|
||||||
# '브랜드/템플릿 수(subbrands_total)'를 기준으로 하고, 캡을 ×2.0 로 낮게 둔다.
|
|
||||||
# (Technical SEO 는 사이트 단위 고정 작업이므로 스케일하지 않음 — sow_templates 의 scale:false)
|
|
||||||
scaling:
|
|
||||||
driver: subbrands_total
|
|
||||||
bands: [[1, 1.0], [3, 1.3], [6, 1.6], [999999, 2.0]]
|
|
||||||
|
|
||||||
# 별도 조달 항목(인력비와 분리). 청구 시 procurement_markup 적용 가능.
|
|
||||||
tools:
|
|
||||||
semrush_guru:
|
|
||||||
label: "Advanced SEO Tools — SEMrush Guru"
|
|
||||||
unit: "월간 구독"
|
|
||||||
price_usd: 249.95
|
|
||||||
note: "고객사 별도 구독(TBD), 사용자별 과금"
|
|
||||||
@@ -259,7 +259,7 @@ def main():
|
|||||||
tbl = s.shapes.add_table(rows, 2, Inches(0.85), Inches(2.0), Inches(11.6), Inches(0.5 * rows)).table
|
tbl = s.shapes.add_table(rows, 2, Inches(0.85), Inches(2.0), Inches(11.6), Inches(0.5 * rows)).table
|
||||||
tbl.columns[0].width = Inches(8.4)
|
tbl.columns[0].width = Inches(8.4)
|
||||||
tbl.columns[1].width = Inches(3.2)
|
tbl.columns[1].width = Inches(3.2)
|
||||||
for j, htxt in enumerate([f"{EST.get('service', 'SEO')} — 구분", "소계"]):
|
for j, htxt in enumerate([f"{EST.get('label') or EST.get('service', 'SEO')} — 구분", "소계"]):
|
||||||
c = tbl.cell(0, j)
|
c = tbl.cell(0, j)
|
||||||
c.text = htxt
|
c.text = htxt
|
||||||
c.fill.solid()
|
c.fill.solid()
|
||||||
|
|||||||
@@ -1,247 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Effort-based 견적 generator for the ourdigital-presales-seo skill (Stage 5).
|
|
||||||
|
|
||||||
Reproduces OurDigital/D.intelligence's real quoting model:
|
|
||||||
cost(task) = role_rate × billing_rate × standard_hours
|
|
||||||
assembled from sow_templates.yaml, grouped into modules, summed, then
|
|
||||||
제안가 = 합계 floored to rounding_unit (십만단위 절사).
|
|
||||||
|
|
||||||
findings.json selects the baseline (basic vs treatment) and scales scoped
|
|
||||||
task hours sub-linearly by portfolio size. Outputs:
|
|
||||||
- 05_estimate_ko.md (cover-sheet 견적)
|
|
||||||
- data/estimate.json (consumed by build_deck.py)
|
|
||||||
- 05_estimate.xlsx
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python estimate.py --findings data/findings.json \
|
|
||||||
--rate-card ../references/rate_card.yaml --sow ../references/sow_templates.yaml \
|
|
||||||
--out-dir <engagement> --seq 1 [--baseline basic|treatment] [--billing 0.70]
|
|
||||||
"""
|
|
||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from openpyxl import Workbook
|
|
||||||
from openpyxl.styles import Alignment, Font, PatternFill
|
|
||||||
|
|
||||||
|
|
||||||
def won(n):
|
|
||||||
return f"{int(round(n)):,}원"
|
|
||||||
|
|
||||||
|
|
||||||
def scope_multiplier(rate, f):
|
|
||||||
"""Sub-linear hours multiplier from the configured driver (default subbrands_total)."""
|
|
||||||
sc = rate.get("scaling", {})
|
|
||||||
driver = sc.get("driver", "subbrands_total")
|
|
||||||
bands = sc.get("bands", [[1, 1.0]])
|
|
||||||
count = max(int(f.get("entity", {}).get(driver, 0) or 0), 1)
|
|
||||||
for mx, m in bands:
|
|
||||||
if count <= mx:
|
|
||||||
return float(m), driver, count
|
|
||||||
return float(bands[-1][1]), driver, count
|
|
||||||
|
|
||||||
|
|
||||||
TIER_ORDER = {"smb": 0, "basic": 1, "treatment": 2}
|
|
||||||
|
|
||||||
|
|
||||||
def _higher(a, b):
|
|
||||||
return a if TIER_ORDER.get(a, 0) >= TIER_ORDER.get(b, 0) else b
|
|
||||||
|
|
||||||
|
|
||||||
def is_premium(f, rate):
|
|
||||||
vertical = (f.get("prospect", {}).get("vertical") or "").lower()
|
|
||||||
terms = [t.lower() for t in rate.get("tiering", {}).get("premium_verticals", [])]
|
|
||||||
return any(t in vertical for t in terms)
|
|
||||||
|
|
||||||
|
|
||||||
def pick_baseline(f, override, rate):
|
|
||||||
if override:
|
|
||||||
return override
|
|
||||||
e = f.get("entity", {})
|
|
||||||
props = e.get("properties_total", 0) or 0
|
|
||||||
subs = e.get("subbrands_total", 0) or 0
|
|
||||||
if props > 5 or subs > 3: # multi-brand / chain
|
|
||||||
tier = "treatment"
|
|
||||||
elif props <= 1 and subs == 0: # single property
|
|
||||||
tier = "smb"
|
|
||||||
else: # small multi-property / mid
|
|
||||||
tier = "basic"
|
|
||||||
# premium/luxury floor: don't auto-drop a premium prospect to the entry tier
|
|
||||||
if is_premium(f, rate):
|
|
||||||
tier = _higher(tier, rate.get("tiering", {}).get("premium_min_tier", "basic"))
|
|
||||||
return tier
|
|
||||||
|
|
||||||
|
|
||||||
def assemble(f, rate, sow, baseline, billing):
|
|
||||||
roles = rate["role_rates"]
|
|
||||||
mult, driver, dcount = scope_multiplier(rate, f)
|
|
||||||
tpl = sow["baselines"][baseline]
|
|
||||||
modules = []
|
|
||||||
grand = 0.0
|
|
||||||
for mod in tpl["modules"]:
|
|
||||||
tasks, sub = [], 0.0
|
|
||||||
for t in mod["tasks"]:
|
|
||||||
applied = mult if (t.get("scale") and mult != 1.0) else 1.0
|
|
||||||
hours = round(t["hours"] * applied, 1)
|
|
||||||
rate_hr = roles[t["role"]]
|
|
||||||
amount = rate_hr * billing * hours
|
|
||||||
tasks.append({
|
|
||||||
"task": t["task"], "desc": t.get("desc", ""), "role": t["role"],
|
|
||||||
"role_rate": rate_hr, "hours": hours, "amount": amount,
|
|
||||||
"scaled": applied != 1.0,
|
|
||||||
})
|
|
||||||
sub += amount
|
|
||||||
modules.append({"name": mod["name"], "subtotal": sub, "tasks": tasks})
|
|
||||||
grand += sub
|
|
||||||
return modules, grand, mult, driver, dcount, tpl["service"]
|
|
||||||
|
|
||||||
|
|
||||||
ROLE_KO = {
|
|
||||||
"ceo": "대표", "evp": "전무", "svp": "상무", "technical_advisor": "기술고문",
|
|
||||||
"director": "이사", "senior_manager": "부장", "deputy_manager": "차장",
|
|
||||||
"manager": "과장", "assistant_manager": "대리", "junior": "주임",
|
|
||||||
"associate": "사원", "intern": "인턴",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def write_md(path, q):
|
|
||||||
L = [f"# 견적서 — {q['prospect']}",
|
|
||||||
"", f"- **제공 서비스**: {q['service']}",
|
|
||||||
f"- **견적번호**: {q['quote_no']} · **작성일**: {q['date']} · **유효기간**: ~{q['valid_until']}",
|
|
||||||
f"- **공급자**: {q['company']['legal_name']} (대표 {q['company']['ceo']}, {q['company']['contact']})",
|
|
||||||
f"- **산정 기준**: SOW 기반 · 청구율 {int(q['billing_rate']*100)}% · 일 8시간/월 4주 · {q['terms']['vat']} · 지급 {q['terms']['payment']}",
|
|
||||||
""]
|
|
||||||
if q["scope"]["hours_multiplier"] != 1.0:
|
|
||||||
dl = "브랜드/템플릿" if q["scope"]["driver"] == "subbrands_total" else "프로퍼티"
|
|
||||||
L.append(f"> 규모 반영: {dl} {q['scope']['driver_count']}개 기준 On-page 업무시간 ×{q['scope']['hours_multiplier']:g} (서브선형)")
|
|
||||||
L.append("")
|
|
||||||
L += ["## 견적 내역", "",
|
|
||||||
"| 구분 | 세부 업무 | 담당 | 시간(h) | 합계 |",
|
|
||||||
"|---|---|:--:|--:|--:|"]
|
|
||||||
for m in q["modules"]:
|
|
||||||
for i, t in enumerate(m["tasks"]):
|
|
||||||
grp = m["name"] if i == 0 else ""
|
|
||||||
mark = " *" if t["scaled"] else ""
|
|
||||||
L.append(f"| {grp} | {t['task']}{mark} | {ROLE_KO.get(t['role'], t['role'])} | {t['hours']:g} | {won(t['amount'])} |")
|
|
||||||
L.append(f"| | **{m['name']} 소계** | | | **{won(m['subtotal'])}** |")
|
|
||||||
L += ["", "## 합계", "",
|
|
||||||
f"- 합계: **{won(q['subtotal_sum'])}**",
|
|
||||||
f"- **제안가(절사 적용): {won(q['proposal'])}** ({q['terms']['vat']})", ""]
|
|
||||||
if q.get("tools"):
|
|
||||||
L += ["## 별도 비용 (조달 — 인력비와 별도)", ""]
|
|
||||||
for t in q["tools"]:
|
|
||||||
L.append(f"- {t['label']}: {t['unit']} ${t['price_usd']} — {t['note']}")
|
|
||||||
L.append("")
|
|
||||||
L += ["---", f"> {q['disclaimer']}"]
|
|
||||||
if any(t["scaled"] for m in q["modules"] for t in m["tasks"]):
|
|
||||||
L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.")
|
|
||||||
with open(path, "w", encoding="utf-8") as fh:
|
|
||||||
fh.write("\n".join(L) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def write_xlsx(path, q):
|
|
||||||
wb = Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.title = "견적"
|
|
||||||
ws.append([f"견적서 — {q['prospect']} ({q['service']})"])
|
|
||||||
ws.append([f"견적번호 {q['quote_no']}", f"작성일 {q['date']}", f"유효 ~{q['valid_until']}",
|
|
||||||
f"청구율 {int(q['billing_rate']*100)}%", q["terms"]["vat"]])
|
|
||||||
ws.append([])
|
|
||||||
cols = ["구분", "세부 업무", "담당", "시간(h)", "합계(원)"]
|
|
||||||
ws.append(cols)
|
|
||||||
hdr_row = ws.max_row
|
|
||||||
for c in range(1, len(cols) + 1):
|
|
||||||
cell = ws.cell(row=hdr_row, column=c)
|
|
||||||
cell.fill = PatternFill("solid", fgColor="11243D")
|
|
||||||
cell.font = Font(color="FFFFFF", bold=True)
|
|
||||||
for m in q["modules"]:
|
|
||||||
for i, t in enumerate(m["tasks"]):
|
|
||||||
ws.append([m["name"] if i == 0 else "", t["task"], ROLE_KO.get(t["role"], t["role"]),
|
|
||||||
t["hours"], int(round(t["amount"]))])
|
|
||||||
ws.append(["", f"{m['name']} 소계", "", "", int(round(m["subtotal"]))])
|
|
||||||
ws.cell(row=ws.max_row, column=2).font = Font(bold=True)
|
|
||||||
ws.cell(row=ws.max_row, column=5).font = Font(bold=True)
|
|
||||||
ws.append([])
|
|
||||||
ws.append(["", "합계", "", "", int(round(q["subtotal_sum"]))])
|
|
||||||
ws.append(["", "제안가(절사 적용)", "", "", int(q["proposal"])])
|
|
||||||
ws.cell(row=ws.max_row, column=2).font = Font(bold=True)
|
|
||||||
ws.cell(row=ws.max_row, column=5).font = Font(bold=True, color="C0392B")
|
|
||||||
ws.append([])
|
|
||||||
ws.append([q["disclaimer"]])
|
|
||||||
for idx, w in enumerate([22, 40, 8, 8, 16], 1):
|
|
||||||
ws.column_dimensions[chr(64 + idx)].width = w
|
|
||||||
wb.save(path)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser(description="Effort-based 견적 from findings.json")
|
|
||||||
ap.add_argument("--findings", required=True)
|
|
||||||
ap.add_argument("--rate-card", required=True)
|
|
||||||
ap.add_argument("--sow", required=True)
|
|
||||||
ap.add_argument("--out-dir", default=".")
|
|
||||||
ap.add_argument("--seq", type=int, default=1)
|
|
||||||
ap.add_argument("--baseline", choices=["smb", "basic", "treatment"], default=None)
|
|
||||||
ap.add_argument("--billing", type=float, default=None)
|
|
||||||
args = ap.parse_args()
|
|
||||||
|
|
||||||
with open(args.findings, encoding="utf-8") as fh:
|
|
||||||
f = json.load(fh)
|
|
||||||
with open(args.rate_card, encoding="utf-8") as fh:
|
|
||||||
rate = yaml.safe_load(fh)
|
|
||||||
with open(args.sow, encoding="utf-8") as fh:
|
|
||||||
sow = yaml.safe_load(fh)
|
|
||||||
|
|
||||||
baseline = pick_baseline(f, args.baseline, rate)
|
|
||||||
tpl_billing = sow["baselines"][baseline].get("billing_rate")
|
|
||||||
billing = (args.billing if args.billing is not None
|
|
||||||
else tpl_billing if tpl_billing is not None else rate["billing_rate"])
|
|
||||||
modules, grand, mult, driver, dcount, service = assemble(f, rate, sow, baseline, billing)
|
|
||||||
props = f.get("entity", {}).get("properties_total", 0)
|
|
||||||
subs = f.get("entity", {}).get("subbrands_total", 0)
|
|
||||||
|
|
||||||
rounding = rate["rounding_unit"]
|
|
||||||
proposal = int(math.floor(grand / rounding) * rounding)
|
|
||||||
|
|
||||||
date = f.get("prospect", {}).get("audit_date") or datetime.date.today().isoformat()
|
|
||||||
d0 = datetime.date.fromisoformat(date)
|
|
||||||
valid_until = (d0 + datetime.timedelta(days=rate["terms"]["validity_days"])).isoformat()
|
|
||||||
quote_no = f"{rate.get('quote_prefix', 'OD')}-{date[:4]}-{args.seq:03d}"
|
|
||||||
|
|
||||||
disclaimer = ("본 견적은 공개 데이터 기반 사전 추정이며 표준 업무시간(SOW)·청구율 "
|
|
||||||
f"{int(billing*100)}% 기준입니다. Search Console/Analytics 권한 확보 후 정밀 "
|
|
||||||
"진단을 통해 과업 시간과 범위를 확정합니다. 외부 조달 항목은 인력비와 별도이며 조달 수수료 15%가 적용될 수 있습니다.")
|
|
||||||
|
|
||||||
q = {
|
|
||||||
"quote_no": quote_no, "date": date, "valid_until": valid_until,
|
|
||||||
"prospect": f.get("prospect", {}).get("name", "(prospect)"),
|
|
||||||
"service": service, "baseline": baseline, "billing_rate": billing,
|
|
||||||
"company": rate["company"], "terms": rate["terms"],
|
|
||||||
"scope": {"driver": driver, "driver_count": dcount,
|
|
||||||
"properties_total": props, "subbrands_total": subs,
|
|
||||||
"hours_multiplier": mult},
|
|
||||||
"modules": modules, "subtotal_sum": grand, "proposal": proposal,
|
|
||||||
"rounding_unit": rounding,
|
|
||||||
"tools": [dict(label=v["label"], unit=v["unit"], price_usd=v["price_usd"], note=v["note"])
|
|
||||||
for v in rate.get("tools", {}).values()],
|
|
||||||
"disclaimer": disclaimer,
|
|
||||||
}
|
|
||||||
|
|
||||||
os.makedirs(args.out_dir, exist_ok=True)
|
|
||||||
data_dir = os.path.join(args.out_dir, "data")
|
|
||||||
os.makedirs(data_dir, exist_ok=True)
|
|
||||||
write_md(os.path.join(args.out_dir, "05_estimate_ko.md"), q)
|
|
||||||
write_xlsx(os.path.join(args.out_dir, "05_estimate.xlsx"), q)
|
|
||||||
with open(os.path.join(data_dir, "estimate.json"), "w", encoding="utf-8") as fh:
|
|
||||||
json.dump(q, fh, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
print(f"견적 {quote_no} [{baseline}] 제안가 {won(proposal)} (합계 {won(grand)}) "
|
|
||||||
f"| {driver}={dcount} ×{mult:g} | 청구율 {int(billing*100)}%")
|
|
||||||
for m in modules:
|
|
||||||
print(f" {m['name']:24} {won(m['subtotal'])}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Map an SEO findings.json → estimate-engine scope.json (service=seo, effort).
|
||||||
|
|
||||||
|
Stage 5 of ourdigital-presales-seo: keeps the SEO-specific mapping here; the engine
|
||||||
|
(96-ourdigital-estimate-engine) owns the costing. Then call:
|
||||||
|
python <engine>/scripts/estimate.py --rate-card <engine>/references/rate_card.yaml \
|
||||||
|
--catalog-dir <engine>/catalog --scope scope.json --out-dir <engagement>
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python findings_to_scope.py --findings data/findings.json --out data/scope.json [--tier auto] [--seq N]
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="SEO findings.json -> engine scope.json")
|
||||||
|
ap.add_argument("--findings", required=True)
|
||||||
|
ap.add_argument("--out", required=True)
|
||||||
|
ap.add_argument("--tier", default="auto", help="auto|smb|basic|treatment")
|
||||||
|
ap.add_argument("--billing", type=float, default=None)
|
||||||
|
ap.add_argument("--seq", type=int, default=1)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
with open(args.findings, encoding="utf-8") as fh:
|
||||||
|
f = json.load(fh)
|
||||||
|
e = f.get("entity", {})
|
||||||
|
p = f.get("prospect", {})
|
||||||
|
severity = sorted({x.get("severity") for x in f.get("findings", []) if x.get("severity")})
|
||||||
|
|
||||||
|
scope = {
|
||||||
|
"service": "seo",
|
||||||
|
"method": "effort",
|
||||||
|
"tier": args.tier,
|
||||||
|
"billing_rate": args.billing,
|
||||||
|
"signals": {
|
||||||
|
"properties_total": e.get("properties_total", 0),
|
||||||
|
"subbrands_total": e.get("subbrands_total", 0),
|
||||||
|
"vertical": p.get("vertical", ""),
|
||||||
|
"severity": severity,
|
||||||
|
},
|
||||||
|
"prospect": {
|
||||||
|
"name": p.get("name", ""), "domain": p.get("domain", ""),
|
||||||
|
"audit_date": p.get("audit_date", ""), "account_code": p.get("account_code", ""),
|
||||||
|
},
|
||||||
|
"seq": args.seq,
|
||||||
|
}
|
||||||
|
with open(args.out, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(scope, fh, ensure_ascii=False, indent=2)
|
||||||
|
print(f"scope -> {args.out} | service=seo tier={args.tier} "
|
||||||
|
f"properties={scope['signals']['properties_total']} subbrands={scope['signals']['subbrands_total']} "
|
||||||
|
f"vertical={scope['signals']['vertical']!r}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
121
custom-skills/96-ourdigital-estimate-engine/DESIGN.md
Normal file
121
custom-skills/96-ourdigital-estimate-engine/DESIGN.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Design — `ourdigital-estimate-engine` skill
|
||||||
|
|
||||||
|
- **Status**: Approved design (2026-05-28). Implementing.
|
||||||
|
- **Origin**: Extracted from `ourdigital-presales-seo`'s estimate engine and generalized to
|
||||||
|
cover all OurDigital / D.intelligence professional services via multiple costing methods.
|
||||||
|
- **Source data**: real quotes in Google Drive `06_Working Template` (SEO) and `견적 자료`
|
||||||
|
(GA4/GTM, education). Company: (주)디인텔리전스 / D.intelligence Lab, info@ourdigital.org.
|
||||||
|
|
||||||
|
## 1. Purpose & scope
|
||||||
|
A reusable, **method-aware** estimate engine that produces OurDigital 견적 for any service
|
||||||
|
line. Consuming skills (e.g. `ourdigital-presales-seo`) map their context → a generic
|
||||||
|
`scope.json` and call the engine CLI. The engine owns the rate card + service catalog
|
||||||
|
(single source of truth).
|
||||||
|
|
||||||
|
### Costing methods (from real data)
|
||||||
|
1. **effort** — `role_rate × billing_rate(0.70) × hours`, grouped into modules; tiering,
|
||||||
|
sub-brand scaling, premium-vertical floor, 절사. (SEO Audit & Treatment, etc.)
|
||||||
|
2. **coaching** — `Σ(lesson_type_price × hours)` over a lesson plan, with student-count
|
||||||
|
discount. Base type prices reproduce real quotes; optional subject×level list-price
|
||||||
|
matrix (`base × level_multiple`). (GA4/GTM courses, workshops, education.)
|
||||||
|
3. **procurement** — `unit_cost × qty × (1 + 0.15)` for non-labor items (tools, 3rd-party).
|
||||||
|
|
||||||
|
All methods share: cover-sheet output, 부가세 별도, 유효기간 14d, 현금, 십만 단위 미만 절사
|
||||||
|
(500k floor for project quotes), quote no `OD-YYYY-NNN`.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
- No invented rates. `digital_ads` / `digital_branding` are built from real docs (디하이브 ads
|
||||||
|
대행 계약, TNS 브랜딩 진단 견적); any future service without real data ships as a flagged stub.
|
||||||
|
- No GA4/GTM *effort-implementation* catalog (delivered as coaching → a course in education).
|
||||||
|
- Does not replace consuming skills' domain logic (e.g. SEO findings→scope mapping stays in
|
||||||
|
presales-seo).
|
||||||
|
|
||||||
|
## 2. Structure
|
||||||
|
```
|
||||||
|
96-ourdigital-estimate-engine/
|
||||||
|
SKILL.md
|
||||||
|
references/
|
||||||
|
rate_card.yaml # UNIVERSAL pricing config (see §3)
|
||||||
|
catalog/
|
||||||
|
seo.yaml # method: effort — tiers smb/basic/treatment (real)
|
||||||
|
education.yaml # method: coaching — courses + per-subject levels (real)
|
||||||
|
digital_ads.yaml # method: effort — REAL (₩6.0M/월 retainer + per-deal media commission)
|
||||||
|
digital_branding.yaml # method: effort — REAL (fixed 진단 stages → ₩9.0M)
|
||||||
|
scripts/
|
||||||
|
estimate.py # CLI dispatcher
|
||||||
|
methods/effort.py
|
||||||
|
methods/coaching.py
|
||||||
|
methods/procurement.py
|
||||||
|
render.py # md / xlsx / json output (cover-sheet)
|
||||||
|
scope.schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. `rate_card.yaml` (universal)
|
||||||
|
- `company`, `quote_prefix: OD`, `currency: KRW`, `rounding_unit: 500000`
|
||||||
|
- `terms`: vat 부가세 별도, validity_days 14, payment 현금
|
||||||
|
- **effort**: `billing_rate: 0.70` (floor 0.60), `basis {8h/day, 4wk/mo}`,
|
||||||
|
`procurement_markup: 0.15`, `role_rates {대표 180k … 과장 70k … 인턴 12k}`,
|
||||||
|
`tiering {premium_verticals, premium_min_tier}`, `scaling {driver: subbrands_total,
|
||||||
|
bands cap ×2.0}`, `tools`.
|
||||||
|
- **coaching**:
|
||||||
|
- `lesson_type_prices`: 화상 80,000 · 대면 100,000 · 실습 150,000 · 워크숍 300,000 · 트리트먼트 500,000
|
||||||
|
- `level_multiple`: Beginners 1.0 · Intermediate 1.5 · Advanced 2.0 · Expert 2.5 · Trainers 3.0
|
||||||
|
- `subject_levels`: {Content Marketing: Beginners, SEO: Beginners, Google Analytics: Intermediate,
|
||||||
|
Google Tag Manager: Advanced, Digital Marketing Strategy: Advanced, Digital Communication:
|
||||||
|
Intermediate, KPI Setup & Measurement Plan: Expert, Business Model Canvas: Expert,
|
||||||
|
e-Commerce Audit: Advanced}
|
||||||
|
- `pricing_mode: base` (default; reproduces real quotes) | `matrix` (list price = base×level_multiple)
|
||||||
|
- `student_discount_bands`: [[5,0.20],[10,0.0],[20,0.20],[30,0.30]] (30+ = 별도 협의)
|
||||||
|
|
||||||
|
## 4. Catalog entries
|
||||||
|
Each file: `service`, `method`, and method-specific body.
|
||||||
|
- **seo.yaml** (effort): the current `sow_templates.yaml` verbatim (tiers smb/basic/treatment,
|
||||||
|
modules→tasks with role/hours/scale). Validated to reproduce 10.5M/25.0M.
|
||||||
|
- **education.yaml** (coaching): `courses:` — named lesson plans, each `lessons: [{subject,
|
||||||
|
type, hours}]`. Seed with `ga4_gtm_intermediate` (the real 17-lesson, ₩1,570,000 plan) and
|
||||||
|
`ga4_gtm_marketing_analytics`. Subjects resolve levels from rate_card.subject_levels.
|
||||||
|
- **digital_ads.yaml / digital_branding.yaml** (effort): STUB — one placeholder tier +
|
||||||
|
`_stub: true`, with a header comment to populate from a real quote.
|
||||||
|
|
||||||
|
## 5. `scope.json` (generic input — `scope.schema.json`)
|
||||||
|
```jsonc
|
||||||
|
// effort
|
||||||
|
{"service":"seo","method":"effort","tier":"auto", // or smb|basic|treatment
|
||||||
|
"signals":{"properties_total":25,"subbrands_total":5,"vertical":"hotel_resort"},
|
||||||
|
"billing_rate":null, "seq":1, "prospect":{"name":"","audit_date":""}}
|
||||||
|
// coaching
|
||||||
|
{"service":"education","method":"coaching","course":"ga4_gtm_intermediate",
|
||||||
|
"students":1, "pricing_mode":"base", "prospect":{...}} // or "lessons":[{subject,type,hours}]
|
||||||
|
```
|
||||||
|
`tier:"auto"` → engine runs tiering (size + premium floor) from `signals`.
|
||||||
|
|
||||||
|
## 6. CLI
|
||||||
|
`python scripts/estimate.py --catalog-dir catalog --rate-card references/rate_card.yaml
|
||||||
|
--scope scope.json --out-dir <dir> [--seq N]`
|
||||||
|
- Dispatcher loads scope → catalog entry → method module → `render.py`.
|
||||||
|
- Outputs `05_estimate_ko.md`, `05_estimate.xlsx`, `data/estimate.json`.
|
||||||
|
- **effort `estimate.json` keeps the current shape** (modules + 제안가 + scope + terms) so
|
||||||
|
`ourdigital-presales-seo/build_deck.py` keeps working unchanged.
|
||||||
|
|
||||||
|
## 7. `ourdigital-presales-seo` refactor (consumption = CLI)
|
||||||
|
- **Move** `references/rate_card.yaml` + `references/sow_templates.yaml` → engine
|
||||||
|
(`catalog/seo.yaml`). Delete `scripts/estimate.py` from presales-seo.
|
||||||
|
- **Add** `scripts/findings_to_scope.py` (thin): findings.json → scope.json (tier signals,
|
||||||
|
vertical, prospect). Keeps SEO-specific mapping out of the engine.
|
||||||
|
- **Stage 5** in SKILL.md: `findings_to_scope.py` → call engine
|
||||||
|
`estimate.py --service seo --scope scope.json`.
|
||||||
|
- `build_deck.py`, `kg_query.py`, `render_pdf.sh`, `findings_to_service.md`,
|
||||||
|
`findings.schema.json`, templates → unchanged.
|
||||||
|
|
||||||
|
## 8. Validation (before commit)
|
||||||
|
- effort: SEO basic ₩10.5M, treatment ₩25.0M (1 property); SHR ₩29.5M; L'Escape `basic` ₩10.5M
|
||||||
|
— identical to pre-refactor.
|
||||||
|
- coaching: `ga4_gtm_intermediate` (17 lessons, 1 student) → **₩1,570,000** exactly.
|
||||||
|
- procurement: unit_cost × qty × 1.15 sanity check.
|
||||||
|
- presales-seo end-to-end: findings→scope→engine→견적 + deck reproduces SHR/L'Escape.
|
||||||
|
|
||||||
|
## 9. Future
|
||||||
|
`digital_ads` / `digital_branding` now real (effort: monthly retainer / fixed stages). Next:
|
||||||
|
optional **commission** line for ad media spend (per-deal %); add `content_marketing` as a
|
||||||
|
project-service if effort quotes emerge (currently a coaching subject); optional matrix-mode
|
||||||
|
coaching quotes; more education courses.
|
||||||
59
custom-skills/96-ourdigital-estimate-engine/SKILL.md
Normal file
59
custom-skills/96-ourdigital-estimate-engine/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: ourdigital-estimate-engine
|
||||||
|
description: Method-aware estimate/견적 engine for OurDigital / D.intelligence professional services (SEO, GA4/GTM, education/coaching, digital ads, branding, …). Generates a Korean 견적 (md/xlsx/json) from a generic scope.json using the real company rate card. Use for "OurDigital 견적", "estimate", "quote", "proposal pricing", "cost estimate", "견적서 생성", or when another skill needs to price a service. Costing methods: effort (role×billing×hours), coaching (lesson×hours), procurement (+15%).
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Estimate Engine
|
||||||
|
|
||||||
|
Single source of truth for OurDigital/D.intelligence pricing. Consuming skills map their
|
||||||
|
context into a `scope.json` and call the engine CLI; the engine owns the rate card + service
|
||||||
|
catalog and renders the 견적.
|
||||||
|
|
||||||
|
## Costing methods (catalog entry declares its `method`)
|
||||||
|
- **effort** — `role_rate × 청구율(0.70) × 표준 업무시간`, by module; auto-tier (smb/basic/treatment)
|
||||||
|
by portfolio size + premium-vertical floor; On-page hours scale by sub-brands (cap ×2.0);
|
||||||
|
제안가 = 합계 절사. Also supports **fixed-amount tasks** (`fixed:` Unit Cost — e.g. branding
|
||||||
|
진단 stages) and **monthly retainers** (`unit: monthly` — e.g. ads 대행). Reproduces real
|
||||||
|
SEO (10.5M/25.0M), branding (₩9.0M), ads (₩6.0M/월).
|
||||||
|
- **coaching** — `Σ(lesson_type_price × hours)` over a lesson plan; student discount opt-in.
|
||||||
|
Reproduces real GA4/GTM 코칭 (₩1,570,000). Base prices default; subject×level matrix optional.
|
||||||
|
- **procurement** — `unit_cost × qty × (1 + 0.15)` for non-labor items.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `references/rate_card.yaml` — universal: role rates, billing, basis, terms, tiering, scaling,
|
||||||
|
procurement, coaching prices/levels/discounts, tools. **Private.**
|
||||||
|
- `catalog/<service>.yaml` — per-service: `service`, `method`, body. All real:
|
||||||
|
`seo` (effort, role-hours tiers), `education` (coaching, incl. GA4/GTM course),
|
||||||
|
`digital_branding` (effort, fixed 진단 stages → ₩9.0M), `digital_ads` (effort, ₩6.0M/월
|
||||||
|
retainer; media commission % is per-deal, not in catalog).
|
||||||
|
- `scripts/estimate.py` — CLI dispatcher; `scripts/methods/{effort,coaching,procurement}.py`;
|
||||||
|
`scripts/render.py` (md/xlsx/json). `scope.schema.json` — input contract.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```
|
||||||
|
python scripts/estimate.py \
|
||||||
|
--rate-card references/rate_card.yaml --catalog-dir catalog \
|
||||||
|
--scope scope.json --out-dir <engagement> [--seq N]
|
||||||
|
```
|
||||||
|
`scope.json` examples (see `scope.schema.json`):
|
||||||
|
- effort: `{"service":"seo","tier":"auto","signals":{"properties_total":25,"subbrands_total":5,"vertical":"hotel_resort"},"prospect":{"name":"…","audit_date":"YYYY-MM-DD"}}`
|
||||||
|
- coaching: `{"service":"education","course":"ga4_gtm_intermediate","students":1,"prospect":{…}}`
|
||||||
|
- procurement: `{"service":"seo","method":"procurement","items":[{"label":"SEMrush Guru","unit_cost":330000,"qty":6}]}`
|
||||||
|
|
||||||
|
Outputs `05_estimate_ko.md`, `05_estimate.xlsx`, `data/estimate.json`. The effort `estimate.json`
|
||||||
|
shape is consumed by `ourdigital-presales-seo/build_deck.py`.
|
||||||
|
|
||||||
|
## Consuming from another skill (CLI)
|
||||||
|
1. Map your context → `scope.json` (service, tier signals or lessons, prospect).
|
||||||
|
2. Call `estimate.py`. 3. Use the rendered 견적; for SEO, feed `data/estimate.json` to the deck.
|
||||||
|
Example consumer: `ourdigital-presales-seo` (`findings_to_scope.py` → engine → 견적 + deck).
|
||||||
|
|
||||||
|
## Adding / editing services
|
||||||
|
- New real service: add `catalog/<service>.yaml` with `method` + body from a **real quote**.
|
||||||
|
- Any new stub should carry `_stub: true` (the 견적 prints a ⚠STUB banner) until replaced with
|
||||||
|
real quote data. All current catalogs are real.
|
||||||
|
- Rates change in `rate_card.yaml` only (single source). Validate against a known real quote.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
Korean-first output · 부가세 별도 · 유효기간 14d · 현금 · `OD-YYYY-NNN`. Don't invent rates —
|
||||||
|
stub and flag instead. Legal entity (주)디인텔리전스 / info@ourdigital.org.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Digital Ads / Performance Marketing catalog — method: effort (monthly retainer).
|
||||||
|
# Source: real signed contract "D.intelligence_디지털 광고 및 퍼포먼스 마케팅 대행 계약서_(주)디하이브"
|
||||||
|
# (2023-04-03) → 퍼포먼스 마케팅 대행 = 월 ₩6,000,000 (부가세 별도), 월말 세금계산서.
|
||||||
|
# Media spend (매체비) is separate; 광고대행수수료(%) on media spend is agreed per contract
|
||||||
|
# (NOT a fixed rate — kept as a per-engagement parameter, not invented here).
|
||||||
|
service: digital_ads
|
||||||
|
method: effort
|
||||||
|
tiers:
|
||||||
|
performance_retainer:
|
||||||
|
label: "디지털 광고·퍼포먼스 마케팅 대행 (월 정기)"
|
||||||
|
notes:
|
||||||
|
- "매체비(media spend)는 별도. 집행금액 대비 광고대행수수료(%)는 계약 시 약정 — 본 견적 미포함."
|
||||||
|
- "제작비·프로모션비는 외주 실비 + 대행수수료 별도. Ad-hoc 진단/분석 보고서는 별도 약정."
|
||||||
|
- "media 커미션이 필요하면 procurement 메서드로 별도 라인 산정(요율은 per-deal)."
|
||||||
|
modules:
|
||||||
|
- name: "퍼포먼스 마케팅 대행"
|
||||||
|
tasks:
|
||||||
|
- {task: "퍼포먼스 마케팅 대행", desc: "KPI 설정·측정계획·데이터/GTM 관리·정기 리포팅·SEO 모니터링 등 (계약 별첨 SOW)", fixed: 6000000, unit: monthly}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Digital Branding catalog — method: effort (fixed-amount stages).
|
||||||
|
# Source: real quote "D.intelligence_TNS 유학원_디지털 브랜딩 진단 컨설팅_20250625" → ₩9,000,000
|
||||||
|
# (5 fixed stages, 6~8주 진단 컨설팅). Stages are lump-sum Unit Cost (no labor-hour basis),
|
||||||
|
# per the SOW rule for non-hour-measurable tasks. Follow-on execution = separate quote.
|
||||||
|
service: digital_branding
|
||||||
|
method: effort
|
||||||
|
tiers:
|
||||||
|
diagnostic:
|
||||||
|
label: "디지털 브랜딩 진단 컨설팅"
|
||||||
|
notes:
|
||||||
|
- "6~8주 일정 (착수 후 업무 수행계획서 제시)."
|
||||||
|
- "후속 실행(브랜드 아이덴티티·웹사이트·콘텐츠 마케팅)은 진단 결과에 따라 별도 견적."
|
||||||
|
modules:
|
||||||
|
- name: "브랜딩 진단 컨설팅"
|
||||||
|
tasks:
|
||||||
|
- {task: "1단계 킥오프·과업 정의", desc: "인터뷰·자료 수집·목표 정의", fixed: 2000000, unit: one_time}
|
||||||
|
- {task: "2단계 디지털 자산 진단", desc: "채널/계정/콘텐츠 구조 분석", fixed: 2000000, unit: one_time}
|
||||||
|
- {task: "3단계 브랜드 가시성 진단", desc: "검색 키워드·브랜드 명칭 중심", fixed: 2000000, unit: one_time}
|
||||||
|
- {task: "4단계 콘텐츠/채널 가시성 조사", desc: "블로그/카페/소셜 노출 현황 정리", fixed: 1500000, unit: one_time}
|
||||||
|
- {task: "5단계 KPI 구조·전략 로드맵", desc: "성과 프레임 기초 설계·로드맵 보고서", fixed: 1500000, unit: one_time}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Education / coaching catalog — method: coaching.
|
||||||
|
# Cost = Σ(lesson_type_prices[type] × hours) from rate_card.coaching (pricing_mode: base).
|
||||||
|
# Subjects resolve levels from rate_card.coaching.subject_levels (used only in matrix mode).
|
||||||
|
service: education
|
||||||
|
method: coaching
|
||||||
|
courses:
|
||||||
|
|
||||||
|
# Real quote: GA4/GTM 중급 과정 1:1 코칭 → ₩1,570,000 (7 대면×100k + 9 화상×80k + 1 실습×150k).
|
||||||
|
ga4_gtm_intermediate:
|
||||||
|
title: "GA4/GTM 중급 과정 1:1 코칭"
|
||||||
|
lessons:
|
||||||
|
- {subject: "Google Analytics", title: "설치-설정 진단", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "측정 계획 수립", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "기본 리포트 설정", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "맞춤 리포트 구성", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "획득 보고서의 이해", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "참여도 보고서 해석", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "수익창출 보고서 관리", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "주요 이벤트와 전환", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "탐색 분석 활용", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Tag Manager", title: "이벤트 태깅 관리 준비", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Tag Manager", title: "GTM 설정 분석과 검수", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Tag Manager", title: "이벤트 태깅 실습", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Tag Manager", title: "마케팅 태그 설정", type: 대면, hours: 1}
|
||||||
|
- {subject: "Google Tag Manager", title: "e-Commerce 태그 설정", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "잠재 고객 설정과 활용", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "캠페인 성과 분석", type: 화상, hours: 1}
|
||||||
|
- {subject: "Google Analytics", title: "Looker Studio 대시보드", type: 실습, hours: 1}
|
||||||
|
|
||||||
|
# Add more courses (workshops, SEO/Content Marketing coaching, etc.) as real lesson plans arrive.
|
||||||
|
# For an ad-hoc plan, pass scope.lessons directly instead of a named course.
|
||||||
@@ -1,90 +1,73 @@
|
|||||||
# SOW task templates — standard 업무 시간(hours) by module.
|
# SEO service catalog — method: effort. Tiers priced via rate_card (role×billing×hours).
|
||||||
# Priced via rate_card.yaml: cost = role_rate × billing_rate × hours.
|
# Reproduces real quotes: basic ₩10.5M, treatment ₩25.0M (1 property, billing 0.70).
|
||||||
# Tiers (제안가 at default billing): smb < basic < treatment.
|
# Only On-page tasks (scale:true) scale by sub-brands; Technical/P&M/Growth fixed.
|
||||||
# basic -> ₩10,500,000 (real SEO Basic & Coaching quote, billing 0.70)
|
service: seo
|
||||||
# treatment -> ₩25,000,000 (real SEO Audit & Treatment quote, billing 0.70)
|
method: effort
|
||||||
# smb -> lean entry tier for single-property SMBs (billing 0.55)
|
tiers:
|
||||||
# Scaling: only On-page tasks (scale:true) scale by sub-brands/templates
|
|
||||||
# (rate_card.scaling); Technical SEO is fixed site-wide work. P&M & Growth fixed.
|
|
||||||
# Each baseline may override billing_rate; else rate_card.billing_rate (0.70).
|
|
||||||
|
|
||||||
baselines:
|
|
||||||
|
|
||||||
smb:
|
smb:
|
||||||
service: "SEO Quick Audit (SMB)"
|
label: "SEO Quick Audit (SMB)"
|
||||||
billing_rate: 0.55 # SMB 진입 티어 — 낮은 청구율
|
billing_rate: 0.55
|
||||||
modules:
|
modules:
|
||||||
- name: "Planning & Management"
|
- name: "Planning & Management"
|
||||||
trigger: [always]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "업무 관리·착수", desc: "과업 정의·일정·리포팅", role: senior_manager, hours: 8, scale: false}
|
- {task: "업무 관리·착수", desc: "과업 정의·일정·리포팅", role: senior_manager, hours: 8, scale: false}
|
||||||
- {task: "웹 사이트 분석", desc: "유입·행동·전환 flow 요약 분석", role: senior_manager, hours: 8, scale: false}
|
- {task: "웹 사이트 분석", desc: "유입·행동·전환 flow 요약 분석", role: senior_manager, hours: 8, scale: false}
|
||||||
- name: "Technical SEO"
|
- name: "Technical SEO"
|
||||||
trigger: [crawlability, cwv, schema_entity]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "Crawling·Indexing 점검", desc: "검색사이트 등록·수집·Site Health 점검", role: manager, hours: 8, scale: false}
|
- {task: "Crawling·Indexing 점검", desc: "검색사이트 등록·수집·Site Health 점검", role: manager, hours: 8, scale: false}
|
||||||
- {task: "속도·CWV 점검", desc: "로딩속도·Core Web Vitals·모바일 점검", role: manager, hours: 8, scale: false}
|
- {task: "속도·CWV 점검", desc: "로딩속도·Core Web Vitals·모바일 점검", role: manager, hours: 8, scale: false}
|
||||||
- {task: "구조·메타·사이트맵 점검", desc: "사이트/URL 구조·메타·사이트맵·색인", role: manager, hours: 8, scale: false}
|
- {task: "구조·메타·사이트맵 점검", desc: "사이트/URL 구조·메타·사이트맵·색인", role: manager, hours: 8, scale: false}
|
||||||
- name: "On-page SEO"
|
- name: "On-page SEO"
|
||||||
trigger: [onpage, schema_entity]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "키워드·메타 진단", desc: "중점 키워드·페이지 메타·템플릿 진단", role: manager, hours: 12, scale: true}
|
- {task: "키워드·메타 진단", desc: "중점 키워드·페이지 메타·템플릿 진단", role: manager, hours: 12, scale: true}
|
||||||
- {task: "on-page 퀵윈 가이드", desc: "타이틀·메타·헤더·링크·이미지 핵심 개선 가이드", role: manager, hours: 16, scale: true}
|
- {task: "on-page 퀵윈 가이드", desc: "타이틀·메타·헤더·링크·이미지 핵심 개선 가이드", role: manager, hours: 16, scale: true}
|
||||||
- name: "SEO Growth"
|
- name: "SEO Growth"
|
||||||
trigger: [measurement, always]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "기본 성과 지표 설정", desc: "핵심 SEO 지표·중점 키워드 트래킹 설정", role: manager, hours: 8, scale: false}
|
- {task: "기본 성과 지표 설정", desc: "핵심 SEO 지표·중점 키워드 트래킹 설정", role: manager, hours: 8, scale: false}
|
||||||
|
|
||||||
basic:
|
basic:
|
||||||
service: "SEO Audit & Basic Treatment"
|
label: "SEO Audit & Basic Treatment"
|
||||||
modules:
|
modules:
|
||||||
- name: "Planning & Management"
|
- name: "Planning & Management"
|
||||||
trigger: [always]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "업무 관리", desc: "프로젝트 설계·과업 정의·일정/산출물 관리·리포팅", role: senior_manager, hours: 20, scale: false}
|
- {task: "업무 관리", desc: "프로젝트 설계·과업 정의·일정/산출물 관리·리포팅", role: senior_manager, hours: 20, scale: false}
|
||||||
- {task: "웹 사이트 분석", desc: "사용자 유입·채널 내 행동·전환 flow 분석", role: senior_manager, hours: 12, scale: false}
|
- {task: "웹 사이트 분석", desc: "사용자 유입·채널 내 행동·전환 flow 분석", role: senior_manager, hours: 12, scale: false}
|
||||||
- name: "Technical SEO"
|
- name: "Technical SEO"
|
||||||
trigger: [crawlability, cwv, schema_entity]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "Crawling & Indexing 설정", desc: "검색사이트 등록/수집 관리, Site Health Check 도구 설정", role: technical_advisor, hours: 16, scale: false}
|
- {task: "Crawling & Indexing 설정", desc: "검색사이트 등록/수집 관리·Site Health Check 도구 설정", role: technical_advisor, hours: 16, scale: false}
|
||||||
- {task: "속도·UX·수집 설정", desc: "로딩속도·페이지 UX·링크·수집 제외 설정", role: manager, hours: 16, scale: false}
|
- {task: "속도·UX·수집 설정", desc: "로딩속도·페이지 UX·링크·수집 제외 설정", role: manager, hours: 16, scale: false}
|
||||||
- {task: "사이트/URL 구조·메타", desc: "구조·URL·메타데이터·사이트맵·리다이렉션", role: senior_manager, hours: 12, scale: false}
|
- {task: "사이트/URL 구조·메타", desc: "구조·URL·메타데이터·사이트맵·리다이렉션", role: senior_manager, hours: 12, scale: false}
|
||||||
- {task: "색인·CWV 진단", desc: "GSC·SEO Tools 활용 색인/크롤오류/Core Web Vitals 진단", role: senior_manager, hours: 16, scale: false}
|
- {task: "색인·CWV 진단", desc: "GSC·SEO Tools 활용 색인/크롤오류/Core Web Vitals 진단", role: senior_manager, hours: 16, scale: false}
|
||||||
- name: "On-page SEO"
|
- name: "On-page SEO"
|
||||||
trigger: [onpage, schema_entity]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "키워드·템플릿·메타 진단", desc: "중점 키워드·페이지 템플릿·메타·개체 활용 진단", role: manager, hours: 24, scale: true}
|
- {task: "키워드·템플릿·메타 진단", desc: "중점 키워드·페이지 템플릿·메타·개체 활용 진단", role: manager, hours: 24, scale: true}
|
||||||
- {task: "링크·콘텐츠 도구 진단", desc: "내외부 링크·공유 메타·콘텐츠 생성-관리 도구", role: manager, hours: 12, scale: true}
|
- {task: "링크·콘텐츠 도구 진단", desc: "내외부 링크·공유 메타·콘텐츠 생성-관리 도구", role: manager, hours: 12, scale: true}
|
||||||
- {task: "on-page 관리 가이드", desc: "타이틀·메타·헤더태그·링크·이미지 메타 가이드", role: manager, hours: 36, scale: true}
|
- {task: "on-page 관리 가이드", desc: "타이틀·메타·헤더태그·링크·이미지 메타 가이드", role: manager, hours: 36, scale: true}
|
||||||
- name: "SEO Growth"
|
- name: "SEO Growth"
|
||||||
trigger: [measurement, always]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "성과 지표 관리", desc: "SEO 관리지표·중점 키워드·포지셔닝 트래킹·3rd party 데이터 설정", role: manager, hours: 24, scale: false}
|
- {task: "성과 지표 관리", desc: "SEO 관리지표·중점 키워드·포지셔닝 트래킹·3rd party 데이터 설정", role: manager, hours: 24, scale: false}
|
||||||
|
|
||||||
treatment:
|
treatment:
|
||||||
service: "SEO Audit & Treatment"
|
label: "SEO Audit & Treatment"
|
||||||
modules:
|
modules:
|
||||||
- name: "Planning & Management"
|
- name: "Planning & Management"
|
||||||
trigger: [always]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "업무 관리", desc: "프로젝트 설계·과업 정의·일정/산출물 관리·리포팅", role: senior_manager, hours: 36, scale: false}
|
- {task: "업무 관리", desc: "프로젝트 설계·과업 정의·일정/산출물 관리·리포팅", role: senior_manager, hours: 36, scale: false}
|
||||||
- {task: "웹 사이트 분석", desc: "사용자 유입·채널 내 행동·전환 flow 분석", role: senior_manager, hours: 60, scale: false}
|
- {task: "웹 사이트 분석", desc: "사용자 유입·채널 내 행동·전환 flow 분석", role: senior_manager, hours: 60, scale: false}
|
||||||
- {task: "측정 계획 수립", desc: "채널 운영 목적·사용자 세그먼트·기대행동 가설 도출", role: senior_manager, hours: 20, scale: false}
|
- {task: "측정 계획 수립", desc: "채널 운영 목적·사용자 세그먼트·기대행동 가설 도출", role: senior_manager, hours: 20, scale: false}
|
||||||
- name: "Technical SEO"
|
- name: "Technical SEO"
|
||||||
trigger: [crawlability, cwv, schema_entity]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "Crawling & Indexing 설정", desc: "검색사이트 등록/수집 관리, Site Health Check 도구 설정", role: manager, hours: 24, scale: false}
|
- {task: "Crawling & Indexing 설정", desc: "검색사이트 등록/수집 관리·Site Health Check 도구 설정", role: manager, hours: 24, scale: false}
|
||||||
- {task: "속도·UX·리다이렉트", desc: "로딩속도·페이지 UX·링크·리다이렉트·수집 제외 설정", role: technical_advisor, hours: 30, scale: false}
|
- {task: "속도·UX·리다이렉트", desc: "로딩속도·페이지 UX·링크·리다이렉트·수집 제외 설정", role: technical_advisor, hours: 30, scale: false}
|
||||||
- {task: "사이트/URL 구조·보안", desc: "구조·URL·메타데이터·사이트맵·보안 관리 진단", role: senior_manager, hours: 24, scale: false}
|
- {task: "사이트/URL 구조·보안", desc: "구조·URL·메타데이터·사이트맵·보안 관리 진단", role: senior_manager, hours: 24, scale: false}
|
||||||
- {task: "모바일·CWV·개선과제", desc: "모바일 최적화·Core Web Vitals 진단·개선 과제 도출", role: manager, hours: 20, scale: false}
|
- {task: "모바일·CWV·개선과제", desc: "모바일 최적화·Core Web Vitals 진단·개선 과제 도출", role: manager, hours: 20, scale: false}
|
||||||
- name: "On-page SEO"
|
- name: "On-page SEO"
|
||||||
trigger: [onpage, schema_entity]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "키워드·템플릿·메타 진단", desc: "중점 키워드·페이지 템플릿·메타·개체 활용 진단", role: manager, hours: 48, scale: true}
|
- {task: "키워드·템플릿·메타 진단", desc: "중점 키워드·페이지 템플릿·메타·개체 활용 진단", role: manager, hours: 48, scale: true}
|
||||||
- {task: "링크·콘텐츠 도구 진단", desc: "내외부 링크·공유 메타·콘텐츠 생성-관리 도구", role: manager, hours: 36, scale: true}
|
- {task: "링크·콘텐츠 도구 진단", desc: "내외부 링크·공유 메타·콘텐츠 생성-관리 도구", role: manager, hours: 36, scale: true}
|
||||||
- {task: "on-page 관리 가이드", desc: "타이틀·메타·헤더태그·링크·이미지 메타 가이드", role: manager, hours: 60, scale: true}
|
- {task: "on-page 관리 가이드", desc: "타이틀·메타·헤더태그·링크·이미지 메타 가이드", role: manager, hours: 60, scale: true}
|
||||||
- name: "SEO Growth"
|
- name: "SEO Growth"
|
||||||
trigger: [measurement, always]
|
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "핵심 성과 지표 설정", desc: "중점과제·목표·SEO 핵심성과지표·모니터링 지표 설정", role: senior_manager, hours: 20, scale: false}
|
- {task: "핵심 성과 지표 설정", desc: "중점과제·목표·SEO 핵심성과지표·모니터링 지표 설정", role: senior_manager, hours: 20, scale: false}
|
||||||
- {task: "지표 정의·리포팅 설계", desc: "데이터 소스·지표 산식·리포팅/진단 주기 설정", role: manager, hours: 16, scale: false}
|
- {task: "지표 정의·리포팅 설계", desc: "데이터 소스·지표 산식·리포팅/진단 주기 설정", role: manager, hours: 16, scale: false}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# OurDigital / D.intelligence — UNIVERSAL rate card (shared across all service lines).
|
||||||
|
# Methods: effort (role×billing×hours), coaching (lesson_type×hours), procurement (+15%).
|
||||||
|
# Source: real quotes (06_Working Template = SEO; 견적 자료 = GA4/GTM, education). Keep PRIVATE.
|
||||||
|
|
||||||
|
company:
|
||||||
|
legal_name: "(주)디인텔리전스 (D.intelligence Lab)"
|
||||||
|
brand: "OurDigital"
|
||||||
|
ceo: "임명재"
|
||||||
|
contact: "info@ourdigital.org"
|
||||||
|
address: "경기도 성남시 수정구 창업로 43 판교글로벌비즈센터 업무동 1층 128호"
|
||||||
|
|
||||||
|
quote_prefix: OD
|
||||||
|
currency: KRW
|
||||||
|
rounding_unit: 500000 # 제안가 절사 단위(효과 기반 프로젝트 견적). 실제 견적 25,340,000→25,000,000 재현.
|
||||||
|
|
||||||
|
terms:
|
||||||
|
vat: "부가세 별도"
|
||||||
|
validity_days: 14
|
||||||
|
payment: "현금"
|
||||||
|
|
||||||
|
# ── effort method (project services: SEO Audit & Treatment 등) ──────────────
|
||||||
|
billing_rate: 0.70 # 청구율 표준 70%
|
||||||
|
billing_rate_floor: 0.60 # 협상 시 -10%p 까지(>10%p 는 CEO/CFO 승인)
|
||||||
|
basis: {hours_per_day: 8, weeks_per_month: 4}
|
||||||
|
procurement_markup: 0.15 # 외부 조달 물품/서비스 수수료
|
||||||
|
role_rates: # 시간 단가 KRW/hr (직급 기준)
|
||||||
|
ceo: 180000
|
||||||
|
evp: 150000
|
||||||
|
svp: 120000
|
||||||
|
technical_advisor: 120000
|
||||||
|
director: 100000
|
||||||
|
senior_manager: 90000 # 부장 (Senior SEO·PM·Senior Analyst·Account Director)
|
||||||
|
deputy_manager: 80000 # 차장
|
||||||
|
manager: 70000 # 과장 (SEO·Data Analyst·Engineer|Developer)
|
||||||
|
assistant_manager: 60000 # 대리
|
||||||
|
junior: 50000 # 주임
|
||||||
|
associate: 30000 # 사원
|
||||||
|
intern: 12000
|
||||||
|
tiering: # 자동 티어 선택 보정(effort 카탈로그가 tier 를 가질 때)
|
||||||
|
premium_verticals: ["luxury", "premium", "deluxe", "5_star", "5성", "특1급", "boutique_luxury"]
|
||||||
|
premium_min_tier: basic # 프리미엄 단일 프로퍼티 최소 티어
|
||||||
|
scaling: # On-page(scale:true) 시간 스케일 — 브랜드/템플릿 수 기준, 캡 ×2.0
|
||||||
|
driver: subbrands_total
|
||||||
|
bands: [[1, 1.0], [3, 1.3], [6, 1.6], [999999, 2.0]]
|
||||||
|
tools:
|
||||||
|
semrush_guru: {label: "Advanced SEO Tools — SEMrush Guru", unit: "월간 구독", price_usd: 249.95, note: "고객사 별도 구독(TBD)"}
|
||||||
|
|
||||||
|
# ── coaching method (education: 1:1 코칭, 워크숍) ────────────────────────────
|
||||||
|
# 실제 견적 재현: 비용 = Σ(lesson_type_prices[type] × hours). subject 행렬은 base×level_multiple
|
||||||
|
# (list price) 이나 실제 견적은 base 가격으로 청구되므로 pricing_mode 기본값 base.
|
||||||
|
coaching:
|
||||||
|
pricing_mode: base # base | matrix
|
||||||
|
lesson_type_prices: {화상: 80000, 대면: 100000, 실습: 150000, 워크숍: 300000, 트리트먼트: 500000}
|
||||||
|
level_multiple: {Beginners: 1.0, Intermediate: 1.5, Advanced: 2.0, Expert: 2.5, Trainers: 3.0}
|
||||||
|
subject_levels: # matrix 모드에서 subject×type 단가 = base × level_multiple[level]
|
||||||
|
"Content Marketing": Beginners
|
||||||
|
"SEO": Beginners
|
||||||
|
"Google Analytics": Intermediate
|
||||||
|
"Google Tag Manager": Advanced
|
||||||
|
"Digital Marketing Strategy": Advanced
|
||||||
|
"Digital Communication": Intermediate
|
||||||
|
"KPI Setup & Measurement Plan": Expert
|
||||||
|
"Business Model Canvas": Expert
|
||||||
|
"e-Commerce Audit": Advanced
|
||||||
|
student_discount:
|
||||||
|
apply_default: false # 실제 1:1 견적은 할인 미적용 → 기본 off (opt-in)
|
||||||
|
bands: [[5, 0.20], [10, 0.0], [20, 0.20], [30, 0.30]] # 30+ = 별도 협의
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "ourdigital-estimate-engine scope input",
|
||||||
|
"description": "Generic estimate request. Consuming skills map their context into this.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["service"],
|
||||||
|
"properties": {
|
||||||
|
"service": {"type": "string", "description": "catalog entry name, e.g. seo | education | digital_ads"},
|
||||||
|
"method": {"type": "string", "enum": ["effort", "coaching", "procurement"], "description": "optional; defaults to the catalog entry's method"},
|
||||||
|
"prospect": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"}, "domain": {"type": "string"},
|
||||||
|
"audit_date": {"type": "string"}, "account_code": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seq": {"type": "integer", "default": 1},
|
||||||
|
|
||||||
|
"tier": {"type": "string", "description": "effort: smb|basic|treatment|auto (default auto)"},
|
||||||
|
"billing_rate": {"type": ["number", "null"], "description": "effort: override billing rate"},
|
||||||
|
"signals": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "effort auto-tiering inputs",
|
||||||
|
"properties": {
|
||||||
|
"properties_total": {"type": "integer"},
|
||||||
|
"subbrands_total": {"type": "integer"},
|
||||||
|
"vertical": {"type": "string"},
|
||||||
|
"severity": {"type": "array", "items": {"type": "string"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"course": {"type": "string", "description": "coaching: named course in catalog (else use lessons)"},
|
||||||
|
"lessons": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "coaching: explicit lesson plan",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "hours"],
|
||||||
|
"properties": {
|
||||||
|
"subject": {"type": "string"}, "title": {"type": "string"},
|
||||||
|
"type": {"type": "string", "enum": ["화상", "대면", "실습", "워크숍", "트리트먼트"]},
|
||||||
|
"hours": {"type": "number"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"students": {"type": "integer", "default": 1},
|
||||||
|
"pricing_mode": {"type": "string", "enum": ["base", "matrix"]},
|
||||||
|
"apply_student_discount": {"type": "boolean"},
|
||||||
|
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "procurement: non-labor line items",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["label", "unit_cost"],
|
||||||
|
"properties": {
|
||||||
|
"label": {"type": "string"}, "unit_cost": {"type": "number"},
|
||||||
|
"qty": {"type": "number", "default": 1}, "currency": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OurDigital estimate engine — method-aware 견적 generator (CLI dispatcher).
|
||||||
|
|
||||||
|
Loads a generic scope.json + the universal rate_card + the service catalog entry,
|
||||||
|
routes to the costing method (effort | coaching | procurement), enriches with
|
||||||
|
quote metadata/terms, and renders 05_estimate_ko.md / .xlsx / data/estimate.json.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python estimate.py --rate-card references/rate_card.yaml --catalog-dir catalog \
|
||||||
|
--scope scope.json --out-dir <dir> [--seq N]
|
||||||
|
|
||||||
|
scope.json: see scope.schema.json. Consuming skills (e.g. ourdigital-presales-seo)
|
||||||
|
map their context into scope.json and call this CLI.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from methods import coaching, effort, procurement # noqa: E402
|
||||||
|
import render # noqa: E402
|
||||||
|
|
||||||
|
METHODS = {"effort": effort, "coaching": coaching, "procurement": procurement}
|
||||||
|
|
||||||
|
DISCLAIMER = {
|
||||||
|
"effort": ("본 견적은 공개/사전 정보 기반 추정이며 표준 업무시간(SOW)·청구율 {billing}% 기준입니다. "
|
||||||
|
"권한 확보 후 정밀 진단을 통해 과업 시간과 범위를 확정합니다. 외부 조달 항목은 인력비와 별도, 조달 수수료 15%가 적용될 수 있습니다."),
|
||||||
|
"coaching": ("본 견적은 레슨 시간 기준으로 작성되었으며(대면 100,000원/h, 화상 80,000원/h 등), 1:1 레슨 전제입니다. "
|
||||||
|
"그룹 레슨·단체 워크숍/실습은 별도 협의가 필요합니다. 수업 구성은 사전상담으로 맞춤 조율됩니다."),
|
||||||
|
"procurement": "조달 물품/서비스 견적이며 조달-관리 수수료 15%를 포함합니다. 실제 공급가는 계약 시점에 확정됩니다.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="OurDigital estimate engine")
|
||||||
|
ap.add_argument("--rate-card", required=True)
|
||||||
|
ap.add_argument("--catalog-dir", required=True)
|
||||||
|
ap.add_argument("--scope", required=True)
|
||||||
|
ap.add_argument("--out-dir", default=".")
|
||||||
|
ap.add_argument("--seq", type=int, default=None)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
with open(args.rate_card, encoding="utf-8") as fh:
|
||||||
|
rate = yaml.safe_load(fh)
|
||||||
|
with open(args.scope, encoding="utf-8") as fh:
|
||||||
|
scope = json.load(fh)
|
||||||
|
|
||||||
|
service = scope["service"]
|
||||||
|
cat_path = os.path.join(args.catalog_dir, f"{service}.yaml")
|
||||||
|
if not os.path.exists(cat_path):
|
||||||
|
sys.exit(f"ERROR: no catalog for service '{service}' at {cat_path}")
|
||||||
|
with open(cat_path, encoding="utf-8") as fh:
|
||||||
|
catalog = yaml.safe_load(fh)
|
||||||
|
|
||||||
|
method = scope.get("method") or catalog.get("method")
|
||||||
|
if method not in METHODS:
|
||||||
|
sys.exit(f"ERROR: unknown method '{method}' (have {list(METHODS)})")
|
||||||
|
|
||||||
|
q = METHODS[method].build(scope, rate, catalog)
|
||||||
|
|
||||||
|
# enrich with universal metadata
|
||||||
|
prospect = scope.get("prospect", {})
|
||||||
|
date = prospect.get("audit_date") or datetime.date.today().isoformat()
|
||||||
|
d0 = datetime.date.fromisoformat(date)
|
||||||
|
seq = args.seq if args.seq is not None else scope.get("seq", 1)
|
||||||
|
q.update({
|
||||||
|
"quote_no": f"{rate.get('quote_prefix', 'OD')}-{date[:4]}-{seq:03d}",
|
||||||
|
"date": date,
|
||||||
|
"valid_until": (d0 + datetime.timedelta(days=rate["terms"]["validity_days"])).isoformat(),
|
||||||
|
"prospect": prospect, "company": rate["company"], "terms": rate["terms"],
|
||||||
|
"disclaimer": DISCLAIMER[method].format(billing=int(q.get("billing_rate", rate["billing_rate"]) * 100)),
|
||||||
|
})
|
||||||
|
|
||||||
|
render.write_all(q, args.out_dir)
|
||||||
|
print(f"견적 {q['quote_no']} [{service}/{method}"
|
||||||
|
+ (f"/{q['tier']}" if q['kind'] == 'effort' else "")
|
||||||
|
+ f"] 제안가 {q['proposal']:,}원 (합계 {int(round(q['subtotal_sum'])):,}원)"
|
||||||
|
+ (" ⚠STUB" if q.get("stub") else ""))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# estimate-engine costing methods: effort, coaching, procurement.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Coaching method: cost = Σ(lesson_type_price × hours) over a lesson plan.
|
||||||
|
|
||||||
|
Base lesson-type prices reproduce real quotes (GA4/GTM 중급 → ₩1,570,000).
|
||||||
|
Optional matrix mode prices by subject level (base × level_multiple).
|
||||||
|
Student-count discount is opt-in (real 1:1 quotes apply none).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _discount(rate, students, apply):
|
||||||
|
if not apply:
|
||||||
|
return 0.0
|
||||||
|
for mx, r in rate["coaching"]["student_discount"]["bands"]:
|
||||||
|
if students <= mx:
|
||||||
|
return float(r)
|
||||||
|
return 0.0 # 30+ → 별도 협의
|
||||||
|
|
||||||
|
|
||||||
|
def _unit_price(rate, lesson, mode):
|
||||||
|
c = rate["coaching"]
|
||||||
|
base = c["lesson_type_prices"][lesson["type"]]
|
||||||
|
if mode == "matrix":
|
||||||
|
lvl = c.get("subject_levels", {}).get(lesson.get("subject"))
|
||||||
|
if lvl:
|
||||||
|
base = base * c["level_multiple"].get(lvl, 1.0)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def build(scope, rate, catalog):
|
||||||
|
c = rate["coaching"]
|
||||||
|
mode = scope.get("pricing_mode") or c.get("pricing_mode", "base")
|
||||||
|
if scope.get("lessons"):
|
||||||
|
lessons = scope["lessons"]
|
||||||
|
title = scope.get("course") or "맞춤 코칭"
|
||||||
|
else:
|
||||||
|
cname = scope.get("course")
|
||||||
|
courses = catalog.get("courses", {})
|
||||||
|
if cname not in courses:
|
||||||
|
raise SystemExit(f"course '{cname}' not in catalog {list(courses)}")
|
||||||
|
lessons = courses[cname]["lessons"]
|
||||||
|
title = courses[cname].get("title", cname)
|
||||||
|
|
||||||
|
items, subtotal = [], 0.0
|
||||||
|
for L in lessons:
|
||||||
|
up = _unit_price(rate, L, mode)
|
||||||
|
amt = up * L["hours"]
|
||||||
|
items.append({"subject": L.get("subject", ""), "title": L.get("title", ""),
|
||||||
|
"type": L["type"], "hours": L["hours"], "unit_price": up, "amount": amt})
|
||||||
|
subtotal += amt
|
||||||
|
|
||||||
|
students = scope.get("students", 1)
|
||||||
|
apply = scope.get("apply_student_discount", c["student_discount"]["apply_default"])
|
||||||
|
disc = _discount(rate, students, apply)
|
||||||
|
disc_amt = subtotal * disc
|
||||||
|
total = subtotal - disc_amt
|
||||||
|
return {
|
||||||
|
"kind": "coaching", "service": catalog["service"], "label": title,
|
||||||
|
"pricing_mode": mode, "students": students,
|
||||||
|
"discount_rate": disc, "discount_amount": disc_amt,
|
||||||
|
"lessons": items, "subtotal_sum": subtotal, "proposal": int(round(total)),
|
||||||
|
"stub": bool(catalog.get("_stub", False)),
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""Effort method: cost = role_rate × billing_rate × hours, grouped by module.
|
||||||
|
|
||||||
|
Tier auto-selection (size + premium-vertical floor) and sub-brand hours scaling
|
||||||
|
are driven by rate_card config. Reproduces real SEO quotes (10.5M/25.0M).
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
|
||||||
|
TIER_ORDER = {"smb": 0, "basic": 1, "treatment": 2}
|
||||||
|
|
||||||
|
|
||||||
|
def _higher(a, b):
|
||||||
|
return a if TIER_ORDER.get(a, 0) >= TIER_ORDER.get(b, 0) else b
|
||||||
|
|
||||||
|
|
||||||
|
def _is_premium(signals, rate):
|
||||||
|
v = (signals.get("vertical") or "").lower()
|
||||||
|
return any(t.lower() in v for t in rate.get("tiering", {}).get("premium_verticals", []))
|
||||||
|
|
||||||
|
|
||||||
|
def _scope_mult(rate, signals):
|
||||||
|
sc = rate.get("scaling", {})
|
||||||
|
driver = sc.get("driver", "subbrands_total")
|
||||||
|
bands = sc.get("bands", [[1, 1.0]])
|
||||||
|
count = max(int(signals.get(driver, 0) or 0), 1)
|
||||||
|
for mx, m in bands:
|
||||||
|
if count <= mx:
|
||||||
|
return float(m), driver, count
|
||||||
|
return float(bands[-1][1]), driver, count
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_tier(signals, rate, available):
|
||||||
|
props = signals.get("properties_total", 0) or 0
|
||||||
|
subs = signals.get("subbrands_total", 0) or 0
|
||||||
|
if props > 5 or subs > 3:
|
||||||
|
tier = "treatment"
|
||||||
|
elif props <= 1 and subs == 0:
|
||||||
|
tier = "smb"
|
||||||
|
else:
|
||||||
|
tier = "basic"
|
||||||
|
if _is_premium(signals, rate):
|
||||||
|
tier = _higher(tier, rate.get("tiering", {}).get("premium_min_tier", "basic"))
|
||||||
|
if tier not in available:
|
||||||
|
tier = "basic" if "basic" in available else sorted(available, key=lambda t: TIER_ORDER.get(t, 9))[0]
|
||||||
|
return tier
|
||||||
|
|
||||||
|
|
||||||
|
def build(scope, rate, catalog):
|
||||||
|
tiers = catalog["tiers"]
|
||||||
|
signals = scope.get("signals", {})
|
||||||
|
tier = scope.get("tier") or "auto"
|
||||||
|
if tier == "auto":
|
||||||
|
tier = _pick_tier(signals, rate, set(tiers))
|
||||||
|
if tier not in tiers:
|
||||||
|
raise SystemExit(f"tier '{tier}' not in catalog tiers {list(tiers)}")
|
||||||
|
t = tiers[tier]
|
||||||
|
billing = scope.get("billing_rate") or t.get("billing_rate") or rate["billing_rate"]
|
||||||
|
mult, driver, dcount = _scope_mult(rate, signals)
|
||||||
|
roles = rate["role_rates"]
|
||||||
|
|
||||||
|
modules, grand, recurring = [], 0.0, False
|
||||||
|
for mod in t["modules"]:
|
||||||
|
tasks, sub = [], 0.0
|
||||||
|
for task in mod["tasks"]:
|
||||||
|
applied = mult if (task.get("scale") and mult != 1.0) else 1.0
|
||||||
|
unit = task.get("unit", "one_time")
|
||||||
|
if unit == "monthly":
|
||||||
|
recurring = True
|
||||||
|
if "fixed" in task: # quoted lump sum / Unit Cost (no labor-hour basis)
|
||||||
|
amt = task["fixed"] * applied
|
||||||
|
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": "fixed",
|
||||||
|
"role_rate": None, "hours": None, "amount": amt,
|
||||||
|
"scaled": applied != 1.0, "unit": unit})
|
||||||
|
else:
|
||||||
|
hours = round(task["hours"] * applied, 1)
|
||||||
|
rr = roles[task["role"]]
|
||||||
|
amt = rr * billing * hours
|
||||||
|
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": task["role"],
|
||||||
|
"role_rate": rr, "hours": hours, "amount": amt,
|
||||||
|
"scaled": applied != 1.0, "unit": unit})
|
||||||
|
sub += amt
|
||||||
|
modules.append({"name": mod["name"], "subtotal": sub, "tasks": tasks})
|
||||||
|
grand += sub
|
||||||
|
|
||||||
|
rounding = rate["rounding_unit"]
|
||||||
|
proposal = int(math.floor(grand / rounding) * rounding)
|
||||||
|
return {
|
||||||
|
"kind": "effort", "service": catalog["service"], "label": t.get("label", catalog["service"]),
|
||||||
|
"tier": tier, "billing_rate": billing, "recurring": recurring,
|
||||||
|
"notes": t.get("notes", []),
|
||||||
|
"scope": {"driver": driver, "driver_count": dcount,
|
||||||
|
"properties_total": signals.get("properties_total", 0),
|
||||||
|
"subbrands_total": signals.get("subbrands_total", 0), "hours_multiplier": mult},
|
||||||
|
"modules": modules, "subtotal_sum": grand, "proposal": proposal,
|
||||||
|
"rounding_unit": rounding, "stub": bool(catalog.get("_stub", False)),
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"""Procurement method: cost = Σ(unit_cost × qty × (1 + markup)) for non-labor items."""
|
||||||
|
|
||||||
|
|
||||||
|
def build(scope, rate, catalog):
|
||||||
|
markup = rate.get("procurement_markup", 0.15)
|
||||||
|
items, total = [], 0.0
|
||||||
|
for it in scope.get("items", []):
|
||||||
|
qty = it.get("qty", 1)
|
||||||
|
amt = it["unit_cost"] * qty * (1 + markup)
|
||||||
|
items.append({"label": it["label"], "unit_cost": it["unit_cost"], "qty": qty,
|
||||||
|
"markup": markup, "amount": amt, "currency": it.get("currency", "KRW")})
|
||||||
|
total += amt
|
||||||
|
return {
|
||||||
|
"kind": "procurement", "service": catalog.get("service", "procurement"),
|
||||||
|
"label": "조달 항목 (Buying & Supplying)", "markup": markup,
|
||||||
|
"items": items, "subtotal_sum": total, "proposal": int(round(total)),
|
||||||
|
"stub": bool(catalog.get("_stub", False)),
|
||||||
|
}
|
||||||
133
custom-skills/96-ourdigital-estimate-engine/scripts/render.py
Normal file
133
custom-skills/96-ourdigital-estimate-engine/scripts/render.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""Render an enriched quote dict to 05_estimate_ko.md, 05_estimate.xlsx, data/estimate.json.
|
||||||
|
Handles all kinds: effort (modules), coaching (lessons), procurement (items)."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill
|
||||||
|
|
||||||
|
ROLE_KO = {"ceo": "대표", "evp": "전무", "svp": "상무", "technical_advisor": "기술고문",
|
||||||
|
"director": "이사", "senior_manager": "부장", "deputy_manager": "차장",
|
||||||
|
"manager": "과장", "assistant_manager": "대리", "junior": "주임",
|
||||||
|
"associate": "사원", "intern": "인턴"}
|
||||||
|
NAVY = "11243D"
|
||||||
|
|
||||||
|
|
||||||
|
def won(n):
|
||||||
|
return f"{int(round(n)):,}원"
|
||||||
|
|
||||||
|
|
||||||
|
def _header_md(q):
|
||||||
|
svc = q.get("label", q["service"])
|
||||||
|
if q["kind"] == "effort":
|
||||||
|
svc += f" ({q['tier']})"
|
||||||
|
L = [f"# 견적서 — {q['prospect'].get('name', '(prospect)')}", "",
|
||||||
|
f"- **제공 서비스**: {svc}",
|
||||||
|
f"- **견적번호**: {q['quote_no']} · **작성일**: {q['date']} · **유효기간**: ~{q['valid_until']}",
|
||||||
|
f"- **공급자**: {q['company']['legal_name']} (대표 {q['company']['ceo']}, {q['company']['contact']})"]
|
||||||
|
if q.get("stub"):
|
||||||
|
L.append("- ⚠️ **STUB 카탈로그** — 실제 단가 미반영(placeholder). 실제 견적 자료로 교체 필요.")
|
||||||
|
L.append("")
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
def _md(q, path):
|
||||||
|
L = _header_md(q)
|
||||||
|
if q["kind"] == "effort":
|
||||||
|
if q["scope"]["hours_multiplier"] != 1.0:
|
||||||
|
dl = "브랜드/템플릿" if q["scope"]["driver"] == "subbrands_total" else "프로퍼티"
|
||||||
|
L += [f"> 규모 반영: {dl} {q['scope']['driver_count']}개 기준 On-page 업무시간 ×{q['scope']['hours_multiplier']:g}", ""]
|
||||||
|
L += ["## 견적 내역", "", "| 구분 | 세부 업무 | 담당 | 시간(h) | 합계 |", "|---|---|:--:|--:|--:|"]
|
||||||
|
for m in q["modules"]:
|
||||||
|
for i, t in enumerate(m["tasks"]):
|
||||||
|
grp = m["name"] if i == 0 else ""
|
||||||
|
mark = " *" if t["scaled"] else ""
|
||||||
|
role_lbl = "고정" if t["role"] == "fixed" else ROLE_KO.get(t["role"], t["role"])
|
||||||
|
hrs = f"{t['hours']:g}" if t.get("hours") is not None else "—"
|
||||||
|
umark = " /월" if t.get("unit") == "monthly" else ""
|
||||||
|
L.append(f"| {grp} | {t['task']}{mark} | {role_lbl} | {hrs} | {won(t['amount'])}{umark} |")
|
||||||
|
L.append(f"| | **{m['name']} 소계** | | | **{won(m['subtotal'])}** |")
|
||||||
|
plabel = "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)"
|
||||||
|
L += ["", f"- 합계: **{won(q['subtotal_sum'])}** · 청구율 {int(q['billing_rate']*100)}% · 일8h/월4주",
|
||||||
|
f"- **{plabel}: {won(q['proposal'])}** ({q['terms']['vat']})"]
|
||||||
|
if q.get("recurring"):
|
||||||
|
L.append("- ※ '/월' 항목은 월 정기(retainer) 비용 — 계약 기간에 따라 합산.")
|
||||||
|
elif q["kind"] == "coaching":
|
||||||
|
L += ["## 견적 내역", "", "| 구분 | 세부 | 방식 | 시간 | 단가 | 합계 |", "|---|---|:--:|--:|--:|--:|"]
|
||||||
|
for it in q["lessons"]:
|
||||||
|
L.append(f"| {it['subject']} | {it['title']} | {it['type']} | {it['hours']:g} | {won(it['unit_price'])} | {won(it['amount'])} |")
|
||||||
|
L += ["", f"- 합계: **{won(q['subtotal_sum'])}** ({q['pricing_mode']} 단가, 수강생 {q['students']}명)"]
|
||||||
|
if q["discount_rate"]:
|
||||||
|
L.append(f"- 할인({int(q['discount_rate']*100)}%): -{won(q['discount_amount'])}")
|
||||||
|
L.append(f"- **제안가: {won(q['proposal'])}** ({q['terms']['vat']})")
|
||||||
|
elif q["kind"] == "procurement":
|
||||||
|
L += ["## 조달 내역", "", "| 항목 | 단가 | 수량 | 수수료 | 합계 |", "|---|--:|--:|:--:|--:|"]
|
||||||
|
for it in q["items"]:
|
||||||
|
L.append(f"| {it['label']} | {it['unit_cost']:,}{it['currency']} | {it['qty']:g} | {int(it['markup']*100)}% | {won(it['amount'])} |")
|
||||||
|
L += ["", f"- **합계: {won(q['proposal'])}** ({q['terms']['vat']})"]
|
||||||
|
L += ["", "---", f"> {q['disclaimer']}"]
|
||||||
|
if q["kind"] == "effort" and any(t["scaled"] for m in q["modules"] for t in m["tasks"]):
|
||||||
|
L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.")
|
||||||
|
for n in q.get("notes", []):
|
||||||
|
L.append(f"> {n}")
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write("\n".join(L) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _xlsx(q, path):
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "견적"
|
||||||
|
ws.append([f"견적서 — {q['prospect'].get('name', '(prospect)')} ({q.get('label', q['service'])})"])
|
||||||
|
ws.append([f"견적번호 {q['quote_no']}", f"작성일 {q['date']}", f"유효 ~{q['valid_until']}", q["terms"]["vat"]])
|
||||||
|
ws.append([])
|
||||||
|
|
||||||
|
def hdr(cols):
|
||||||
|
ws.append(cols)
|
||||||
|
for c in range(1, len(cols) + 1):
|
||||||
|
cell = ws.cell(row=ws.max_row, column=c)
|
||||||
|
cell.fill = PatternFill("solid", fgColor=NAVY)
|
||||||
|
cell.font = Font(color="FFFFFF", bold=True)
|
||||||
|
|
||||||
|
if q["kind"] == "effort":
|
||||||
|
hdr(["구분", "세부 업무", "담당", "시간(h)", "합계(원)"])
|
||||||
|
for m in q["modules"]:
|
||||||
|
for i, t in enumerate(m["tasks"]):
|
||||||
|
role_lbl = "고정" if t["role"] == "fixed" else ROLE_KO.get(t["role"], t["role"])
|
||||||
|
hrs = t["hours"] if t.get("hours") is not None else "—"
|
||||||
|
ws.append([m["name"] if i == 0 else "", t["task"], role_lbl, hrs, int(round(t["amount"]))])
|
||||||
|
ws.append(["", f"{m['name']} 소계", "", "", int(round(m["subtotal"]))])
|
||||||
|
ws.append([])
|
||||||
|
ws.append(["", "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)", "", "", int(q["proposal"])])
|
||||||
|
widths = [22, 40, 8, 8, 16]
|
||||||
|
elif q["kind"] == "coaching":
|
||||||
|
hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"])
|
||||||
|
for it in q["lessons"]:
|
||||||
|
ws.append([it["subject"], it["title"], it["type"], it["hours"], int(it["unit_price"]), int(round(it["amount"]))])
|
||||||
|
ws.append([])
|
||||||
|
ws.append(["", "제안가", "", "", "", int(q["proposal"])])
|
||||||
|
widths = [18, 34, 8, 6, 12, 14]
|
||||||
|
else: # procurement
|
||||||
|
hdr(["항목", "단가", "수량", "수수료", "합계(원)"])
|
||||||
|
for it in q["items"]:
|
||||||
|
ws.append([it["label"], it["unit_cost"], it["qty"], it["markup"], int(round(it["amount"]))])
|
||||||
|
ws.append([])
|
||||||
|
ws.append(["", "합계", "", "", int(q["proposal"])])
|
||||||
|
widths = [34, 14, 8, 10, 16]
|
||||||
|
ws.cell(row=ws.max_row, column=2).font = Font(bold=True)
|
||||||
|
ws.cell(row=ws.max_row, column=len(widths)).font = Font(bold=True, color="C0392B")
|
||||||
|
ws.append([])
|
||||||
|
ws.append([q["disclaimer"]])
|
||||||
|
for idx, w in enumerate(widths, 1):
|
||||||
|
ws.column_dimensions[chr(64 + idx)].width = w
|
||||||
|
wb.save(path)
|
||||||
|
|
||||||
|
|
||||||
|
def write_all(q, out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
ddir = os.path.join(out_dir, "data")
|
||||||
|
os.makedirs(ddir, exist_ok=True)
|
||||||
|
with open(os.path.join(ddir, "estimate.json"), "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(q, fh, ensure_ascii=False, indent=2)
|
||||||
|
_md(q, os.path.join(out_dir, "05_estimate_ko.md"))
|
||||||
|
_xlsx(q, os.path.join(out_dir, "05_estimate.xlsx"))
|
||||||
@@ -51,7 +51,7 @@ description: | # third person; START with "Use when…" / wh
|
|||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] `name`: kebab-case, no spaces/parens/special chars. (May keep the `NN-` prefix.)
|
- [ ] `name`: the **clean** skill name = directory name **minus its `NN-` ordering prefix** (dir `16-seo-schema-validator` → `name: seo-schema-validator`). kebab-case, no spaces/parens/special chars. Dirs keep the number prefix for ordering; the skill `name` never carries it.
|
||||||
- [ ] `description`: third-person; says **when to use** + trigger keywords (EN + KO).
|
- [ ] `description`: third-person; says **when to use** + trigger keywords (EN + KO).
|
||||||
- [ ] **Whole frontmatter ≤ 1024 characters.**
|
- [ ] **Whole frontmatter ≤ 1024 characters.**
|
||||||
- [ ] Body: concise markdown (aim < 500 words of always-on content); push detail into
|
- [ ] Body: concise markdown (aim < 500 words of always-on content); push detail into
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ migrate_skill_root.py — additive bulk migration: give each skill a root SKILL.
|
|||||||
|
|
||||||
Non-destructive. For every skill directory under custom-skills/ that lacks a root
|
Non-destructive. For every skill directory under custom-skills/ that lacks a root
|
||||||
SKILL.md, it generates one by copying the skill's existing directive and:
|
SKILL.md, it generates one by copying the skill's existing directive and:
|
||||||
- setting `name:` to the directory name (guaranteed kebab-case + unique),
|
- setting `name:` to the directory name MINUS its NN- ordering prefix
|
||||||
|
(dir `16-seo-schema-validator` -> name `seo-schema-validator`; clean + unique),
|
||||||
- preserving `description` and the markdown body verbatim,
|
- preserving `description` and the markdown body verbatim,
|
||||||
- validating the result (frontmatter <= 1024 chars, kebab name, description present).
|
- validating the result (frontmatter <= 1024 chars, kebab name, description present).
|
||||||
|
|
||||||
@@ -29,6 +30,11 @@ FM_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.S)
|
|||||||
MAX_FM = 1024
|
MAX_FM = 1024
|
||||||
|
|
||||||
|
|
||||||
|
def skill_name(dirname):
|
||||||
|
"""Skill `name:` = directory name without its NN- ordering prefix."""
|
||||||
|
return re.sub(r"^\d+-", "", dirname)
|
||||||
|
|
||||||
|
|
||||||
def is_skill_dir(d):
|
def is_skill_dir(d):
|
||||||
"""A skill dir is a real skill, not shared infra."""
|
"""A skill dir is a real skill, not shared infra."""
|
||||||
if not d.is_dir():
|
if not d.is_dir():
|
||||||
@@ -106,7 +112,7 @@ def main(argv=None):
|
|||||||
rows.append((name, "MANUAL", "no desktop/ or code/ SKILL.md source (commands/README only)"))
|
rows.append((name, "MANUAL", "no desktop/ or code/ SKILL.md source (commands/README only)"))
|
||||||
manual += 1
|
manual += 1
|
||||||
continue
|
continue
|
||||||
text, issues = build_root_skill(src.read_text(encoding="utf-8"), d.name)
|
text, issues = build_root_skill(src.read_text(encoding="utf-8"), skill_name(d.name))
|
||||||
rel = src.relative_to(d)
|
rel = src.relative_to(d)
|
||||||
if text is None:
|
if text is None:
|
||||||
rows.append((name, "MANUAL", f"{rel}: {issues[0]}"))
|
rows.append((name, "MANUAL", f"{rel}: {issues[0]}"))
|
||||||
|
|||||||
Reference in New Issue
Block a user