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`.
|
(500k floor for project quotes), quote no `OD-YYYY-NNN`.
|
||||||
|
|
||||||
### Non-goals
|
### Non-goals
|
||||||
- No invented rates. `digital_ads` / `digital_branding` ship as **effort stubs** (clearly
|
- No invented rates. `digital_ads` / `digital_branding` are built from real docs (디하이브 ads
|
||||||
marked) until real quotes exist.
|
대행 계약, 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).
|
- 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
|
- Does not replace consuming skills' domain logic (e.g. SEO findings→scope mapping stays in
|
||||||
presales-seo).
|
presales-seo).
|
||||||
@@ -39,8 +39,8 @@ All methods share: cover-sheet output, 부가세 별도, 유효기간 14d, 현
|
|||||||
catalog/
|
catalog/
|
||||||
seo.yaml # method: effort — tiers smb/basic/treatment (real)
|
seo.yaml # method: effort — tiers smb/basic/treatment (real)
|
||||||
education.yaml # method: coaching — courses + per-subject levels (real)
|
education.yaml # method: coaching — courses + per-subject levels (real)
|
||||||
digital_ads.yaml # method: effort — STUB
|
digital_ads.yaml # method: effort — REAL (₩6.0M/월 retainer + per-deal media commission)
|
||||||
digital_branding.yaml # method: effort — STUB
|
digital_branding.yaml # method: effort — REAL (fixed 진단 stages → ₩9.0M)
|
||||||
scripts/
|
scripts/
|
||||||
estimate.py # CLI dispatcher
|
estimate.py # CLI dispatcher
|
||||||
methods/effort.py
|
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.
|
- presales-seo end-to-end: findings→scope→engine→견적 + deck reproduces SHR/L'Escape.
|
||||||
|
|
||||||
## 9. Future
|
## 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
|
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`)
|
## Costing methods (catalog entry declares its `method`)
|
||||||
- **effort** — `role_rate × 청구율(0.70) × 표준 업무시간`, by module; auto-tier (smb/basic/treatment)
|
- **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);
|
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.
|
- **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.
|
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.
|
- **procurement** — `unit_cost × qty × (1 + 0.15)` for non-labor items.
|
||||||
@@ -20,8 +22,10 @@ catalog and renders the 견적.
|
|||||||
## Files
|
## Files
|
||||||
- `references/rate_card.yaml` — universal: role rates, billing, basis, terms, tiering, scaling,
|
- `references/rate_card.yaml` — universal: role rates, billing, basis, terms, tiering, scaling,
|
||||||
procurement, coaching prices/levels/discounts, tools. **Private.**
|
procurement, coaching prices/levels/discounts, tools. **Private.**
|
||||||
- `catalog/<service>.yaml` — per-service: `service`, `method`, body. Real: `seo` (effort),
|
- `catalog/<service>.yaml` — per-service: `service`, `method`, body. All real:
|
||||||
`education` (coaching). Stubs: `digital_ads`, `digital_branding` (effort — `_stub: true`).
|
`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/estimate.py` — CLI dispatcher; `scripts/methods/{effort,coaching,procurement}.py`;
|
||||||
`scripts/render.py` (md/xlsx/json). `scope.schema.json` — input contract.
|
`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
|
## Adding / editing services
|
||||||
- New real service: add `catalog/<service>.yaml` with `method` + body from a **real quote**.
|
- 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
|
- Any new stub should carry `_stub: true` (the 견적 prints a ⚠STUB banner) until replaced with
|
||||||
with real quote data before client use (the 견적 prints a ⚠STUB banner).
|
real quote data. All current catalogs are real.
|
||||||
- Rates change in `rate_card.yaml` only (single source). Validate against a known real quote.
|
- Rates change in `rate_card.yaml` only (single source). Validate against a known real quote.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
# Digital Ads catalog — method: effort. STUB — placeholder hours, NOT real rates.
|
# Digital Ads / Performance Marketing catalog — method: effort (monthly retainer).
|
||||||
# TODO: populate tiers/modules/hours from a real OurDigital digital-ads quote (Drive),
|
# Source: real signed contract "D.intelligence_디지털 광고 및 퍼포먼스 마케팅 대행 계약서_(주)디하이브"
|
||||||
# the same way seo.yaml was built. Until then estimates here are illustrative only.
|
# (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
|
service: digital_ads
|
||||||
method: effort
|
method: effort
|
||||||
_stub: true
|
|
||||||
tiers:
|
tiers:
|
||||||
basic:
|
performance_retainer:
|
||||||
label: "Digital Ads Setup & Management (STUB)"
|
label: "디지털 광고·퍼포먼스 마케팅 대행 (월 정기)"
|
||||||
|
notes:
|
||||||
|
- "매체비(media spend)는 별도. 집행금액 대비 광고대행수수료(%)는 계약 시 약정 — 본 견적 미포함."
|
||||||
|
- "제작비·프로모션비는 외주 실비 + 대행수수료 별도. Ad-hoc 진단/분석 보고서는 별도 약정."
|
||||||
|
- "media 커미션이 필요하면 procurement 메서드로 별도 라인 산정(요율은 per-deal)."
|
||||||
modules:
|
modules:
|
||||||
- name: "Planning & Management"
|
- name: "퍼포먼스 마케팅 대행"
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "캠페인 전략·계정 구조 설계", desc: "STUB — replace with real SOW", role: senior_manager, hours: 16, scale: false}
|
- {task: "퍼포먼스 마케팅 대행", desc: "KPI 설정·측정계획·데이터/GTM 관리·정기 리포팅·SEO 모니터링 등 (계약 별첨 SOW)", fixed: 6000000, unit: monthly}
|
||||||
- name: "Ads Operation"
|
|
||||||
tasks:
|
|
||||||
- {task: "캠페인 셋업·태깅·전환 추적", desc: "STUB", role: manager, hours: 24, scale: false}
|
|
||||||
- {task: "운영·최적화·리포팅", desc: "STUB (월 운영)", role: manager, hours: 24, scale: true}
|
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
# Digital Branding catalog — method: effort. STUB — placeholder hours, NOT real rates.
|
# Digital Branding catalog — method: effort (fixed-amount stages).
|
||||||
# TODO: populate from a real OurDigital digital-branding quote (Drive). Illustrative only.
|
# 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
|
service: digital_branding
|
||||||
method: effort
|
method: effort
|
||||||
_stub: true
|
|
||||||
tiers:
|
tiers:
|
||||||
basic:
|
diagnostic:
|
||||||
label: "Digital Branding (STUB)"
|
label: "디지털 브랜딩 진단 컨설팅"
|
||||||
|
notes:
|
||||||
|
- "6~8주 일정 (착수 후 업무 수행계획서 제시)."
|
||||||
|
- "후속 실행(브랜드 아이덴티티·웹사이트·콘텐츠 마케팅)은 진단 결과에 따라 별도 견적."
|
||||||
modules:
|
modules:
|
||||||
- name: "Planning & Management"
|
- name: "브랜딩 진단 컨설팅"
|
||||||
tasks:
|
tasks:
|
||||||
- {task: "브랜드 전략·포지셔닝", desc: "STUB — replace with real SOW", role: director, hours: 16, scale: false}
|
- {task: "1단계 킥오프·과업 정의", desc: "인터뷰·자료 수집·목표 정의", fixed: 2000000, unit: one_time}
|
||||||
- name: "Branding Execution"
|
- {task: "2단계 디지털 자산 진단", desc: "채널/계정/콘텐츠 구조 분석", fixed: 2000000, unit: one_time}
|
||||||
tasks:
|
- {task: "3단계 브랜드 가시성 진단", desc: "검색 키워드·브랜드 명칭 중심", fixed: 2000000, unit: one_time}
|
||||||
- {task: "브랜드 아이덴티티·가이드", desc: "STUB", role: senior_manager, hours: 24, scale: false}
|
- {task: "4단계 콘텐츠/채널 가시성 조사", desc: "블로그/카페/소셜 노출 현황 정리", fixed: 1500000, unit: one_time}
|
||||||
- {task: "디지털 자산·채널 적용", desc: "STUB", role: manager, hours: 24, scale: true}
|
- {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)
|
mult, driver, dcount = _scope_mult(rate, signals)
|
||||||
roles = rate["role_rates"]
|
roles = rate["role_rates"]
|
||||||
|
|
||||||
modules, grand = [], 0.0
|
modules, grand, recurring = [], 0.0, False
|
||||||
for mod in t["modules"]:
|
for mod in t["modules"]:
|
||||||
tasks, sub = [], 0.0
|
tasks, sub = [], 0.0
|
||||||
for task in mod["tasks"]:
|
for task in mod["tasks"]:
|
||||||
applied = mult if (task.get("scale") and mult != 1.0) else 1.0
|
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)
|
hours = round(task["hours"] * applied, 1)
|
||||||
rr = roles[task["role"]]
|
rr = roles[task["role"]]
|
||||||
amt = rr * billing * hours
|
amt = rr * billing * hours
|
||||||
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": task["role"],
|
tasks.append({"task": task["task"], "desc": task.get("desc", ""), "role": task["role"],
|
||||||
"role_rate": rr, "hours": hours, "amount": amt, "scaled": applied != 1.0})
|
"role_rate": rr, "hours": hours, "amount": amt,
|
||||||
|
"scaled": applied != 1.0, "unit": unit})
|
||||||
sub += amt
|
sub += amt
|
||||||
modules.append({"name": mod["name"], "subtotal": sub, "tasks": tasks})
|
modules.append({"name": mod["name"], "subtotal": sub, "tasks": tasks})
|
||||||
grand += sub
|
grand += sub
|
||||||
@@ -75,7 +85,8 @@ def build(scope, rate, catalog):
|
|||||||
proposal = int(math.floor(grand / rounding) * rounding)
|
proposal = int(math.floor(grand / rounding) * rounding)
|
||||||
return {
|
return {
|
||||||
"kind": "effort", "service": catalog["service"], "label": t.get("label", catalog["service"]),
|
"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,
|
"scope": {"driver": driver, "driver_count": dcount,
|
||||||
"properties_total": signals.get("properties_total", 0),
|
"properties_total": signals.get("properties_total", 0),
|
||||||
"subbrands_total": signals.get("subbrands_total", 0), "hours_multiplier": mult},
|
"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"]):
|
for i, t in enumerate(m["tasks"]):
|
||||||
grp = m["name"] if i == 0 else ""
|
grp = m["name"] if i == 0 else ""
|
||||||
mark = " *" if t["scaled"] 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'])}** |")
|
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주",
|
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":
|
elif q["kind"] == "coaching":
|
||||||
L += ["## 견적 내역", "", "| 구분 | 세부 | 방식 | 시간 | 단가 | 합계 |", "|---|---|:--:|--:|--:|--:|"]
|
L += ["## 견적 내역", "", "| 구분 | 세부 | 방식 | 시간 | 단가 | 합계 |", "|---|---|:--:|--:|--:|--:|"]
|
||||||
for it in q["lessons"]:
|
for it in q["lessons"]:
|
||||||
@@ -62,6 +68,8 @@ def _md(q, path):
|
|||||||
L += ["", "---", f"> {q['disclaimer']}"]
|
L += ["", "---", f"> {q['disclaimer']}"]
|
||||||
if q["kind"] == "effort" and any(t["scaled"] for m in q["modules"] for t in m["tasks"]):
|
if q["kind"] == "effort" and any(t["scaled"] for m in q["modules"] for t in m["tasks"]):
|
||||||
L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.")
|
L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.")
|
||||||
|
for n in q.get("notes", []):
|
||||||
|
L.append(f"> {n}")
|
||||||
with open(path, "w", encoding="utf-8") as fh:
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
fh.write("\n".join(L) + "\n")
|
fh.write("\n".join(L) + "\n")
|
||||||
|
|
||||||
@@ -85,10 +93,12 @@ def _xlsx(q, path):
|
|||||||
hdr(["구분", "세부 업무", "담당", "시간(h)", "합계(원)"])
|
hdr(["구분", "세부 업무", "담당", "시간(h)", "합계(원)"])
|
||||||
for m in q["modules"]:
|
for m in q["modules"]:
|
||||||
for i, t in enumerate(m["tasks"]):
|
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(["", f"{m['name']} 소계", "", "", int(round(m["subtotal"]))])
|
||||||
ws.append([])
|
ws.append([])
|
||||||
ws.append(["", "제안가(절사 적용)", "", "", int(q["proposal"])])
|
ws.append(["", "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)", "", "", int(q["proposal"])])
|
||||||
widths = [22, 40, 8, 8, 16]
|
widths = [22, 40, 8, 8, 16]
|
||||||
elif q["kind"] == "coaching":
|
elif q["kind"] == "coaching":
|
||||||
hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"])
|
hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"])
|
||||||
|
|||||||
Reference in New Issue
Block a user