From 08cb20fc6704c4b780300118b67b6eccdd894416 Mon Sep 17 00:00:00 2001 From: Andrew Yim Date: Thu, 28 May 2026 02:06:59 +0900 Subject: [PATCH] Add real digital_ads + digital_branding catalogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built from real D.intelligence docs (not the 견적 자료 folder, which has only GA4/GTM + education — confirmed): - digital_branding: TNS 유학원 디지털 브랜딩 진단 컨설팅 → ₩9,000,000 (5 fixed stages) - digital_ads: 디하이브 디지털 광고·퍼포먼스 마케팅 대행 계약 → ₩6,000,000/월 retainer (media-spend commission % is per-deal, kept as a parameter — not invented) effort method now supports fixed-amount tasks (Unit Cost) and monthly retainers (unit: monthly); render shows 고정/—//월 and retainer/commission notes. Validated: branding ₩9.0M, ads ₩6.0M/월; no regression (SEO 25.0M, coaching 1.57M). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../96-ourdigital-estimate-engine/DESIGN.md | 13 +++++----- .../96-ourdigital-estimate-engine/SKILL.md | 14 ++++++---- .../catalog/digital_ads.yaml | 25 +++++++++--------- .../catalog/digital_branding.yaml | 26 +++++++++++-------- .../scripts/methods/effort.py | 25 +++++++++++++----- .../scripts/render.py | 18 ++++++++++--- 6 files changed, 76 insertions(+), 45 deletions(-) diff --git a/custom-skills/96-ourdigital-estimate-engine/DESIGN.md b/custom-skills/96-ourdigital-estimate-engine/DESIGN.md index fca10ff..481e9cd 100644 --- a/custom-skills/96-ourdigital-estimate-engine/DESIGN.md +++ b/custom-skills/96-ourdigital-estimate-engine/DESIGN.md @@ -24,8 +24,8 @@ 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` ship as **effort stubs** (clearly - marked) until real quotes exist. +- 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). @@ -39,8 +39,8 @@ All methods share: cover-sheet output, 부가세 별도, 유효기간 14d, 현 catalog/ seo.yaml # method: effort — tiers smb/basic/treatment (real) education.yaml # method: coaching — courses + per-subject levels (real) - digital_ads.yaml # method: effort — STUB - digital_branding.yaml # method: effort — STUB + 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 @@ -115,6 +115,7 @@ Each file: `service`, `method`, and method-specific body. - presales-seo end-to-end: findings→scope→engine→견적 + deck reproduces SHR/L'Escape. ## 9. Future -Populate `digital_ads` / `digital_branding` from real quotes; add `content_marketing` as a +`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 -quotes; more education courses. +coaching quotes; more education courses. diff --git a/custom-skills/96-ourdigital-estimate-engine/SKILL.md b/custom-skills/96-ourdigital-estimate-engine/SKILL.md index a8d9852..99578da 100644 --- a/custom-skills/96-ourdigital-estimate-engine/SKILL.md +++ b/custom-skills/96-ourdigital-estimate-engine/SKILL.md @@ -12,7 +12,9 @@ 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); - 제안가 = 합계 절사. Reproduces real SEO quotes (basic ₩10.5M / treatment ₩25.0M). + 제안가 = 합계 절사. 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. @@ -20,8 +22,10 @@ catalog and renders the 견적. ## Files - `references/rate_card.yaml` — universal: role rates, billing, basis, terms, tiering, scaling, procurement, coaching prices/levels/discounts, tools. **Private.** -- `catalog/.yaml` — per-service: `service`, `method`, body. Real: `seo` (effort), - `education` (coaching). Stubs: `digital_ads`, `digital_branding` (effort — `_stub: true`). +- `catalog/.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. @@ -46,8 +50,8 @@ Example consumer: `ourdigital-presales-seo` (`findings_to_scope.py` → engine ## Adding / editing services - New real service: add `catalog/.yaml` with `method` + body from a **real quote**. -- Stubs (`digital_ads`, `digital_branding`) carry `_stub: true` and placeholder hours — replace - with real quote data before client use (the 견적 prints a ⚠STUB banner). +- 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 diff --git a/custom-skills/96-ourdigital-estimate-engine/catalog/digital_ads.yaml b/custom-skills/96-ourdigital-estimate-engine/catalog/digital_ads.yaml index adb5acf..2b768ba 100644 --- a/custom-skills/96-ourdigital-estimate-engine/catalog/digital_ads.yaml +++ b/custom-skills/96-ourdigital-estimate-engine/catalog/digital_ads.yaml @@ -1,17 +1,18 @@ -# Digital Ads catalog — method: effort. STUB — placeholder hours, NOT real rates. -# TODO: populate tiers/modules/hours from a real OurDigital digital-ads quote (Drive), -# the same way seo.yaml was built. Until then estimates here are illustrative only. +# 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 -_stub: true tiers: - basic: - label: "Digital Ads Setup & Management (STUB)" + performance_retainer: + label: "디지털 광고·퍼포먼스 마케팅 대행 (월 정기)" + notes: + - "매체비(media spend)는 별도. 집행금액 대비 광고대행수수료(%)는 계약 시 약정 — 본 견적 미포함." + - "제작비·프로모션비는 외주 실비 + 대행수수료 별도. Ad-hoc 진단/분석 보고서는 별도 약정." + - "media 커미션이 필요하면 procurement 메서드로 별도 라인 산정(요율은 per-deal)." modules: - - name: "Planning & Management" + - name: "퍼포먼스 마케팅 대행" tasks: - - {task: "캠페인 전략·계정 구조 설계", desc: "STUB — replace with real SOW", role: senior_manager, hours: 16, scale: false} - - name: "Ads Operation" - tasks: - - {task: "캠페인 셋업·태깅·전환 추적", desc: "STUB", role: manager, hours: 24, scale: false} - - {task: "운영·최적화·리포팅", desc: "STUB (월 운영)", role: manager, hours: 24, scale: true} + - {task: "퍼포먼스 마케팅 대행", desc: "KPI 설정·측정계획·데이터/GTM 관리·정기 리포팅·SEO 모니터링 등 (계약 별첨 SOW)", fixed: 6000000, unit: monthly} diff --git a/custom-skills/96-ourdigital-estimate-engine/catalog/digital_branding.yaml b/custom-skills/96-ourdigital-estimate-engine/catalog/digital_branding.yaml index be19d0b..7998ca5 100644 --- a/custom-skills/96-ourdigital-estimate-engine/catalog/digital_branding.yaml +++ b/custom-skills/96-ourdigital-estimate-engine/catalog/digital_branding.yaml @@ -1,16 +1,20 @@ -# Digital Branding catalog — method: effort. STUB — placeholder hours, NOT real rates. -# TODO: populate from a real OurDigital digital-branding quote (Drive). Illustrative only. +# 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 -_stub: true tiers: - basic: - label: "Digital Branding (STUB)" + diagnostic: + label: "디지털 브랜딩 진단 컨설팅" + notes: + - "6~8주 일정 (착수 후 업무 수행계획서 제시)." + - "후속 실행(브랜드 아이덴티티·웹사이트·콘텐츠 마케팅)은 진단 결과에 따라 별도 견적." modules: - - name: "Planning & Management" + - name: "브랜딩 진단 컨설팅" tasks: - - {task: "브랜드 전략·포지셔닝", desc: "STUB — replace with real SOW", role: director, hours: 16, scale: false} - - name: "Branding Execution" - tasks: - - {task: "브랜드 아이덴티티·가이드", desc: "STUB", role: senior_manager, hours: 24, scale: false} - - {task: "디지털 자산·채널 적용", desc: "STUB", role: manager, hours: 24, scale: true} + - {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} diff --git a/custom-skills/96-ourdigital-estimate-engine/scripts/methods/effort.py b/custom-skills/96-ourdigital-estimate-engine/scripts/methods/effort.py index b8c4809..1f67ac7 100644 --- a/custom-skills/96-ourdigital-estimate-engine/scripts/methods/effort.py +++ b/custom-skills/96-ourdigital-estimate-engine/scripts/methods/effort.py @@ -57,16 +57,26 @@ def build(scope, rate, catalog): mult, driver, dcount = _scope_mult(rate, signals) roles = rate["role_rates"] - modules, grand = [], 0.0 + 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 - 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 = 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 @@ -75,7 +85,8 @@ def build(scope, rate, catalog): proposal = int(math.floor(grand / rounding) * rounding) return { "kind": "effort", "service": catalog["service"], "label": t.get("label", catalog["service"]), - "tier": tier, "billing_rate": billing, + "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}, diff --git a/custom-skills/96-ourdigital-estimate-engine/scripts/render.py b/custom-skills/96-ourdigital-estimate-engine/scripts/render.py index f2b5655..a35cf9e 100644 --- a/custom-skills/96-ourdigital-estimate-engine/scripts/render.py +++ b/custom-skills/96-ourdigital-estimate-engine/scripts/render.py @@ -42,10 +42,16 @@ def _md(q, path): 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'])} |") + 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"- **제안가(절사 적용): {won(q['proposal'])}** ({q['terms']['vat']})"] + 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"]: @@ -62,6 +68,8 @@ def _md(q, path): 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") @@ -85,10 +93,12 @@ def _xlsx(q, path): hdr(["구분", "세부 업무", "담당", "시간(h)", "합계(원)"]) 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"]))]) + 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(["", "제안가(절사 적용)", "", "", int(q["proposal"])]) + ws.append(["", "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)", "", "", int(q["proposal"])]) widths = [22, 40, 8, 8, 16] elif q["kind"] == "coaching": hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"])