Add real digital_ads + digital_branding catalogs
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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/<service>.yaml` — per-service: `service`, `method`, body. Real: `seo` (effort),
|
||||
`education` (coaching). Stubs: `digital_ads`, `digital_branding` (effort — `_stub: true`).
|
||||
- `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.
|
||||
|
||||
@@ -46,8 +50,8 @@ Example consumer: `ourdigital-presales-seo` (`findings_to_scope.py` → engine
|
||||
|
||||
## Adding / editing services
|
||||
- New real service: add `catalog/<service>.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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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(["구분", "세부", "방식", "시간", "단가", "합계(원)"])
|
||||
|
||||
Reference in New Issue
Block a user