Compare commits
11 Commits
c11917b551
...
8b98fdd2fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b98fdd2fc | |||
| c73d284d44 | |||
| fd7b7c7fbb | |||
| c60e2abd34 | |||
| ccdd2c44ef | |||
| 311adaf06c | |||
| 1d236192b0 | |||
| 18bf611663 | |||
| 44731eb2f8 | |||
| d6fd629943 | |||
| 3b42b3d5a9 |
84
.claude/commands/jamie-journal-editor.md
Normal file
84
.claude/commands/jamie-journal-editor.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: "Jamie Clinic journal/blog content editor for journal.jamie.clinic. Creates educational medical blog posts in Dr. Jung's voice with Korean medical ad compliance."
|
||||
---
|
||||
|
||||
# Jamie Journal Editor
|
||||
|
||||
Journal/blog content editor for "정기호의 성형외과 진료실 이야기" (journal.jamie.clinic).
|
||||
|
||||
## Triggers
|
||||
- "Jamie journal", "제이미 저널", "진료실 이야기"
|
||||
- "journal blog", "Jamie blog post", "블로그 콘텐츠"
|
||||
|
||||
## Scripts
|
||||
|
||||
```bash
|
||||
# Full validation
|
||||
python /Users/ourdigital/Project/our-claude-skills/custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input draft.md
|
||||
|
||||
# Specific checks (structure, compliance, spelling, links)
|
||||
python /Users/ourdigital/Project/our-claude-skills/custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input draft.md --check structure,compliance
|
||||
|
||||
# Verbose JSON report
|
||||
python /Users/ourdigital/Project/our-claude-skills/custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input draft.md --verbose --output report.json
|
||||
```
|
||||
|
||||
## Brand Voice (Dr. Jung)
|
||||
|
||||
| Trait | Expression |
|
||||
|-------|-----------|
|
||||
| Trustworthy Expert | "2008년부터 눈 성형을 전문적으로 시행하고 있고" |
|
||||
| Warm Explainer | "나무 옮겨 심는 거랑 똑같다고 하거든요" |
|
||||
| Honest Advisor | "100% 성공률을 가진 의사는 없어요" |
|
||||
| Patient-Centered | "환자분들이 말씀하시는 졸린 눈은..." |
|
||||
| Humble Confidence | "저희들이 시행하고 있습니다" |
|
||||
|
||||
- 90% 격식체 (~습니다/~입니다), 6% 서비스형 (~드립니다), 4% 부드러운 어미 (~거든요)
|
||||
- Opening: "안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
- Honorifics: 환자분 (61%), 고객님 (22%), 여러분 (17%)
|
||||
|
||||
## Analogy Dictionary
|
||||
|
||||
| Topic | Metaphor |
|
||||
|-------|----------|
|
||||
| Fat graft survival | "나무 옮겨 심는 거랑 똑같다고 하거든요" |
|
||||
| 3-point fixation | "인형극 실 비유 — 실이 두 줄인 거랑 세 줄 네 줄인 거랑은 움직임의 자연스러움이 차이" |
|
||||
| Revision surgery | "깨끗한 도화지에 그림을 그리면 화가의 실력이 100% 발휘가 될 텐데, 재수술은 낙서가 있는 도화지에 덧칠" |
|
||||
| Endotine | "똑딱이 단추와 같은 나사" |
|
||||
|
||||
## Content Structure (5-Step Body)
|
||||
|
||||
1. **Problem Statement** (Empathy) — "~로 고민하시는 분들이 많습니다"
|
||||
2. **Cause Explanation** (Education) — Medical terminology with Korean(漢字, English) format
|
||||
3. **Solution** (Jamie's Method) — Specific technique details
|
||||
4. **Advantages** (Differentiation) — Recovery, scars, pain, AS period
|
||||
5. **Expected Results** (Vision) — Realistic outcomes
|
||||
|
||||
## Procedure Copy Reference
|
||||
|
||||
| Category | Procedure | Key Expression |
|
||||
|----------|-----------|---------------|
|
||||
| Eye | Quick Burial | "티 안 나게 예뻐지는", "휴가를 내지 않고도" |
|
||||
| Eye | Hybrid Double Eyelid | "절개법과 매몰법의 장점만을 모은" |
|
||||
| Eye | Ptosis Correction | "졸리고 답답한 눈매를 또렷하고 시원하게" |
|
||||
| Forehead | Endoscopic Forehead Lift | "3점 고정", "흡수성 봉합사 주문 제작" |
|
||||
| Anti-aging | SMAS Lifting | "표정 근막층부터 근본적으로" |
|
||||
| Anti-aging | Fat Grafting | "반영구적 유지", "나무 옮겨 심는 것처럼" |
|
||||
|
||||
## Compliance Rules
|
||||
|
||||
- ❌ No "100% 성공" → "대부분의 경우 좋은 결과를 기대할 수 있습니다"
|
||||
- ❌ No "부작용 없음" → "부작용은 극히 드뭅니다"
|
||||
- ❌ No guarantee or competitor comparison language
|
||||
- ❌ No "전문 병원", "특화" → "중점진료", "풍부한 경험"
|
||||
- ✅ Required disclaimer at bottom:
|
||||
"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다."
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Receive topic + target audience
|
||||
2. Draft article using brand voice + analogy dictionary
|
||||
3. Apply 5-step content structure
|
||||
4. Run validator: `python .../journal_validator.py --input draft.md`
|
||||
5. Fix flagged issues
|
||||
6. Submit to `/jamie-brand-audit` for final review
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(git init:*)",
|
||||
"Bash(unzip:*)",
|
||||
"Bash(git add:*)",
|
||||
"Skill(notion-organizer)",
|
||||
"Skill(ourdigital-seo-audit)",
|
||||
"WebFetch(domain:les.josunhotel.com)",
|
||||
"WebFetch(domain:josunhotel.com)",
|
||||
"WebFetch(domain:pagespeed.web.dev)",
|
||||
"Bash(ln:*)",
|
||||
"Skill(skill-creator)",
|
||||
"Bash(python:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git reset:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(git fetch:*)",
|
||||
"Bash(mysql:*)",
|
||||
"Bash(brew services:*)",
|
||||
"Bash(source ~/.envrc)",
|
||||
"mcp__plugin_Notion_notion__notion-fetch",
|
||||
"mcp__plugin_Notion_notion__notion-update-page",
|
||||
"Skill(settings-audit)",
|
||||
"mcp__plugin_figma_figma__whoami",
|
||||
"mcp__plugin_figma_figma__get_metadata",
|
||||
"mcp__plugin_figma_figma__get_screenshot",
|
||||
"WebSearch",
|
||||
"Bash(git -C ~/Workspace/seo-workspace rm:*)",
|
||||
"Bash(git -C ~/Workspace/seo-workspace check-ignore -q \".claude/commands\")",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(bash:*)",
|
||||
"WebFetch(domain:ourdigital.org)",
|
||||
"WebFetch(domain:blog.ourdigital.org)",
|
||||
"WebFetch(domain:journal.ourdigital.org)",
|
||||
"WebFetch(domain:www.ourdigital.org)",
|
||||
"WebFetch(domain:dataforseo.com)",
|
||||
"mcp__firecrawl__firecrawl_scrape",
|
||||
"mcp__notion__API-retrieve-a-page",
|
||||
"mcp__notion__API-get-block-children",
|
||||
"mcp__claude_ai_Notion__notion-fetch",
|
||||
"Skill(doc-generator)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Jamie Journal Editor Skill
|
||||
|
||||
> **Status**: v1.0.0
|
||||
> **Status**: v1.1.0
|
||||
> **Role**: Journal/blog content editor for Jamie Clinic
|
||||
> **Channel**: "정기호의 성형외과 진료실 이야기" (https://journal.jamie.clinic)
|
||||
> **Partner Skills**: `jamie-brand-audit` (compliance review), `jamie-brand-editor` (general content)
|
||||
@@ -9,16 +9,34 @@
|
||||
|
||||
Creates educational blog content for Jamie Plastic Surgery Clinic's journal channel, written in Dr. Jung Ki-ho's authentic voice. Specializes in long-form educational articles about surgical procedures with Korean medical advertising compliance.
|
||||
|
||||
## Quick Start (Claude Code)
|
||||
|
||||
```bash
|
||||
# Full validation
|
||||
python code/scripts/journal_validator.py --input article.md
|
||||
|
||||
# Specific checks
|
||||
python code/scripts/journal_validator.py --input article.md --check structure,compliance
|
||||
|
||||
# Verbose JSON report
|
||||
python code/scripts/journal_validator.py --input article.md --verbose --output report.json
|
||||
```
|
||||
|
||||
Available checks: `structure`, `compliance`, `spelling`, `links` (default: all)
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
46-jamie-journal-editor/
|
||||
├── README.md # This file
|
||||
├── code/
|
||||
│ └── CLAUDE.md # Claude Code directive
|
||||
│ ├── CLAUDE.md # Claude Code directive (self-contained)
|
||||
│ └── scripts/
|
||||
│ └── journal_validator.py # Article validator (structure, compliance, spelling, links)
|
||||
├── desktop/
|
||||
│ ├── SKILL.md # Claude Desktop skill directive
|
||||
│ ├── skill.yaml # Extended metadata
|
||||
│ ├── jamie-journal-editor.skill # Packaged skill (ZIP)
|
||||
│ └── references/
|
||||
│ ├── brand-voice.md # Dr. Jung's voice characteristics
|
||||
│ ├── content-patterns.md # Article structure patterns
|
||||
@@ -30,6 +48,7 @@ Creates educational blog content for Jamie Plastic Surgery Clinic's journal chan
|
||||
```
|
||||
[Topic/Brief]
|
||||
-> jamie-journal-editor (draft article)
|
||||
-> journal_validator.py (automated checks)
|
||||
-> jamie-brand-audit (compliance review)
|
||||
-> [Published Content]
|
||||
```
|
||||
|
||||
@@ -1,58 +1,171 @@
|
||||
# CLAUDE.md
|
||||
# CLAUDE.md — Jamie Journal Editor (Code)
|
||||
|
||||
## Overview
|
||||
|
||||
Jamie Clinic journal content editor. Creates educational blog posts for "정기호의 성형외과 진료실 이야기" (journal.jamie.clinic) in Dr. Jung's authentic voice with medical ad compliance.
|
||||
Jamie Clinic journal content editor for "정기호의 성형외과 진료실 이야기" (journal.jamie.clinic). Creates educational blog posts in Dr. Jung Ki-ho's authentic voice with Korean medical ad compliance. Supports full drafting (research → outline → article) and manuscript editing.
|
||||
|
||||
## Content Types
|
||||
- **CMS**: Ghost (journal.jamie.clinic)
|
||||
- **Partner skills**: `jamie-brand-audit` (41) — compliance review; `jamie-brand-editor` (40) — general branded content
|
||||
|
||||
- Educational journal articles (교육적 저널 글)
|
||||
- Procedure explanation posts (시술 설명 포스트)
|
||||
- Patient FAQ articles (환자 FAQ 기사)
|
||||
## Quick Start
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Receive topic/brief with target audience
|
||||
2. Draft article using Dr. Jung's voice (see `desktop/references/brand-voice.md`)
|
||||
3. Apply 5-step content structure (see `desktop/references/content-patterns.md`)
|
||||
4. Check compliance against medical ad law (see `desktop/references/medical-compliance.md`)
|
||||
5. Submit to `41-jamie-brand-audit` for final review
|
||||
|
||||
## Brand Voice (Quick Reference)
|
||||
|
||||
- 90% formal endings (~습니다/~입니다)
|
||||
- "환자분" for medical contexts (61%), "고객님" for service (22%)
|
||||
- Standard opening: "안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
- Use Dr. Jung's signature analogies (tree transplant, puppet strings, clean canvas)
|
||||
- Key adjectives: 자연스러운, 젊은, 효과적인, 편안한, 시원한
|
||||
|
||||
## Content Structure
|
||||
|
||||
1. **Problem** (empathy) - Patient concerns
|
||||
2. **Cause** (education) - Why this happens
|
||||
3. **Solution** (Jamie's method) - Procedure intro
|
||||
4. **Advantages** (differentiation) - Recovery, scars, pain
|
||||
5. **Expected Results** (vision) - Outcomes
|
||||
|
||||
## Compliance Rules
|
||||
|
||||
- No exaggerated claims ("100% 성공", "부작용 없음")
|
||||
- No before/after comparisons without disclaimers
|
||||
- No guarantee language
|
||||
- No competitor comparisons
|
||||
- Always include required disclaimer at bottom
|
||||
- Set realistic expectations
|
||||
|
||||
## Required Disclaimer
|
||||
|
||||
```
|
||||
개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니
|
||||
사전에 의료진과 상담 후 결정하시기 바랍니다.
|
||||
```bash
|
||||
python code/scripts/journal_validator.py --input article.md
|
||||
python code/scripts/journal_validator.py --input article.md --check structure,compliance
|
||||
python code/scripts/journal_validator.py --input article.md --verbose --output report.json
|
||||
```
|
||||
|
||||
## References
|
||||
## Workflow Modes
|
||||
|
||||
See `desktop/references/` for:
|
||||
- `brand-voice.md` - Dr. Jung's detailed voice characteristics
|
||||
- `content-patterns.md` - Article structure patterns and examples
|
||||
- `medical-compliance.md` - Korean medical advertising compliance guide
|
||||
**Mode A — Full Drafting** (no manuscript): Receive topic → deep research → outline for approval → draft article → SEO + image specs + schema → compliance review → `jamie-brand-audit`
|
||||
|
||||
**Mode B — Manuscript Editing**: Receive 초안 → preserve original voice/analogies → restructure for web + SEO → add SEO + image specs + schema → compliance review → `jamie-brand-audit`
|
||||
|
||||
## Brand Voice (Dr. Jung)
|
||||
|
||||
### Personality Traits
|
||||
|
||||
| Trait | Expression Example |
|
||||
|-------|-------------------|
|
||||
| Trustworthy Expert | "2008년부터 눈 성형을 전문적으로 시행하고 있고" |
|
||||
| Warm Explainer | "나무 옮겨 심는 거랑 똑같다고 하거든요" |
|
||||
| Honest Advisor | "100% 성공률을 가진 의사는 없어요" |
|
||||
| Patient-Centered | "환자분들이 말씀하시는 졸린 눈은..." |
|
||||
| Humble Confidence | "저희들이 시행하고 있습니다" |
|
||||
|
||||
### Sentence Endings & Honorifics
|
||||
|
||||
| Type | Ratio | | Context | Honorific | Usage |
|
||||
|------|-------|-|---------|-----------|-------|
|
||||
| Formal (~습니다) | 90% | | Medical | 환자분 | 61% |
|
||||
| Service (~드립니다) | 6% | | Service | 고객님 | 22% |
|
||||
| Soft (~거든요) | 4% | | General | 여러분 | 17% |
|
||||
|
||||
**Standard opening**: "안녕하세요. 제이미성형외과 정기호 원장입니다." (시리즈 후속편: "지난 글에서~" 대체 가능)
|
||||
|
||||
## Analogy Dictionary
|
||||
|
||||
| Topic | Signature Metaphor |
|
||||
|-------|--------------------|
|
||||
| Fat graft survival | "나무 옮겨 심는 거랑 똑같다고 하거든요. 한 번 옮겨 심은 나무는 그 자리에서 계속 자라는 거예요." |
|
||||
| 3-point fixation | "인형극 실 비유 - 실이 두 줄인 거랑 세 줄 네 줄인 거랑은 움직임의 자연스러움이 차이가 있겠죠" |
|
||||
| Revision surgery | "깨끗한 도화지에 그림을 그리면 화가의 실력이 100% 발휘가 될 텐데, 재수술은 낙서가 있는 도화지에 덧칠을 하는 것" |
|
||||
| Endotine | "똑딱이 단추와 같은 나사라고 생각하셔도 되겠습니다" |
|
||||
|
||||
## Content Structure (5-Step Body)
|
||||
|
||||
### Opening
|
||||
```
|
||||
안녕하세요. 제이미성형외과 정기호 원장입니다.
|
||||
오늘은 [타겟 고객/고민]을 위한 [시술명]에 대해 [말씀드리겠습니다/소개해 드리겠습니다].
|
||||
[주제에 대한 일반적인 오해나 필요성 언급]
|
||||
```
|
||||
|
||||
| Step | Label | Content |
|
||||
|------|-------|---------|
|
||||
| 1 | Problem (Empathy) | Patient concerns — "~로 고민하시는 분들이 많습니다" |
|
||||
| 2 | Cause (Education) | Why this occurs — use medical term format |
|
||||
| 3 | Solution (Jamie's Method) | Procedure with technique details |
|
||||
| 4 | Advantages (Differentiation) | Recovery, scars, pain, AS period |
|
||||
| 5 | Results (Vision) | Realistic outcomes |
|
||||
|
||||
### Closing
|
||||
```
|
||||
[고민]이시라면 지금 바로 제이미성형외과의 [시술명] 상담을 추천드립니다.
|
||||
언제든지 편안한 마음으로 상담해 주시면 감사하겠습니다.
|
||||
```
|
||||
|
||||
## SEO Metadata
|
||||
|
||||
| 항목 | 형식 |
|
||||
|------|------|
|
||||
| 메타 제목 | [주제] — [핵심키워드] \| 제이미성형외과 (60자 이내) |
|
||||
| 메타 디스크립션 | 120-160자, 핵심 키워드 포함, CTA 암시 |
|
||||
| Slug | 영문 소문자, 하이픈 구분, 핵심 키워드 3-5개 |
|
||||
| 태그 | 핵심 키워드 5-10개 (한글) |
|
||||
|
||||
## Procedure Copy Reference
|
||||
|
||||
| Category | Procedure | Key Expression |
|
||||
|----------|-----------|---------------|
|
||||
| Eye | Quick Burial | "티 안 나게 예뻐지는", "휴가를 내지 않고도" |
|
||||
| Eye | Hybrid Double Eyelid | "절개법과 매몰법의 장점만을 모은" |
|
||||
| Eye | Ptosis Correction | "졸리고 답답한 눈매를 또렷하고 시원하게" |
|
||||
| Eye | Under-eye Fat | "어둡고 칙칙한 눈밑을 환하게" |
|
||||
| Forehead | Endoscopic Forehead Lift | "3점 고정", "흡수성 봉합사 주문 제작" |
|
||||
| Forehead | Endoscopic Brow Lift | "눈썹을 이상적인 위치로 리프팅" |
|
||||
| Forehead | Sub-brow Excision | "티 안 나게 눈꺼풀 처짐을 개선" |
|
||||
| Anti-aging | SMAS Lifting | "표정 근막층부터 근본적으로" |
|
||||
| Anti-aging | Fat Grafting | "반영구적 유지", "나무 옮겨 심는 것처럼" |
|
||||
|
||||
## Numeric Expressions & Medical Term Format
|
||||
|
||||
| Item | Expression |
|
||||
|------|-----------|
|
||||
| Surgery time | "10~15분", "1시간 정도", "4시간 정도" |
|
||||
| Recovery | "다음 날부터", "4~5일", "일주일 정도" |
|
||||
| AS period | "5년간 AS 보장" |
|
||||
| Management | "1년간 무료 리프팅 관리" |
|
||||
| Survival rate | "30% 정도, 많게는 40%까지" |
|
||||
| Duration | "5년 이상", "반영구적" |
|
||||
|
||||
**Medical term format**: `안검하수(眼瞼下垂, Ptosis)는 눈을 뜨는 근육의 힘이 약해져 눈꺼풀이 처지는 현상을 말합니다.`
|
||||
|
||||
영문 병기: 본문 첫 등장 시 적용, 섹션 타이틀은 선택적, 그래픽은 핵심 해부학 용어만, Featured Image는 국문만(예외 없음).
|
||||
|
||||
## Do's & Don'ts
|
||||
|
||||
**Do's**: Patient empathy first, use signature analogies, specific numbers, realistic expectations, 원고 편집 시 원문 표현·비유·직설적 경고 보존
|
||||
|
||||
| Prohibited | Replacement |
|
||||
|-----------|-------------|
|
||||
| "100% 성공" | "대부분의 경우 좋은 결과를 기대할 수 있습니다" |
|
||||
| "부작용 없음" | "부작용은 극히 드뭅니다" |
|
||||
| "전문/전문병원" | "중점진료", "풍부한 경험" |
|
||||
| "완벽/최고/최상" | 사용 불가 |
|
||||
| "무통/완치/해결" | "개선", "지향" |
|
||||
| Competitor comparison | "저희만의 방법으로..." |
|
||||
|
||||
## Required Disclaimer (#알립니다#)
|
||||
|
||||
```
|
||||
※ 본 콘텐츠는 의학 정보 제공을 목적으로 작성되었으며,
|
||||
개인의 상태에 따라 적합한 치료 방법은 다를 수 있습니다.
|
||||
|
||||
※ 모든 수술 및 시술은 개인에 따라 결과가 다를 수 있으며,
|
||||
출혈, 감염, 붓기, 비대칭 등의 부작용이 발생할 수 있습니다.
|
||||
반드시 전문의와 충분한 상담 후 결정하시기 바랍니다.
|
||||
```
|
||||
|
||||
## Image Specifications
|
||||
|
||||
**Color palette**: Background `#E0E5EB` (flat, no gradients), Primary text `#333333`, Accent `#6B8FAF`, Secondary `#8B9BA8`, JAMIE logo `#6d7856`
|
||||
|
||||
**Featured Image** (1200×675px, 16:9): 실사 모델 필수, 국문 카피만 (영문 절대 금지), 포스트 제목 그대로 사용 금지, JAMIE 워터마크는 앱 옵션으로 처리. Ghost Post Settings에서 설정 — 본문에 삽입하지 않음.
|
||||
|
||||
**본문 이미지** (1200×600px): 2번부터 시작. 위치: ②정의/원인 후 해부학 도식, ③시술 설명 후 인포그래픽, ④장점/효과 후 비교 차트, ⑤마무리 전 CTA (선택).
|
||||
|
||||
**금지 요소**: 실제 수술 장면, Before/After 라벨, 수술 도구 사실적 사진, 3D/그라디언트, 장식 테두리, 클립아트.
|
||||
|
||||
**파일명**: `[topic]-[detail]-[type]-[number].png` (type: featured, recovery-timeline, comparison, anatomy, infographic, flowchart)
|
||||
|
||||
## Gemini Image Prompt Templates
|
||||
|
||||
4종 템플릿 사용. 모든 프롬프트에 `[PRIMARY]`/`[SECONDARY]` 블록으로 시각적 위계 구조화, HEX 컬러 명시, 의료 고지문 미포함.
|
||||
|
||||
- **Featured**: 실사 인물 + 국문 카피, `#E0E5EB` 배경, 16:9
|
||||
- **Timeline**: 수평 타임라인 노드 (`#6B8FAF` 활성, `#8B9BA8` 비활성), 1200×600px
|
||||
- **Comparison**: 2-column 레이아웃, `#8B9BA8` 구분선, 1200×600px
|
||||
- **Anatomy**: 미니멀 line-art, `#6B8FAF` 화살표, 핵심 용어만 영문 병기, 1200×600px
|
||||
|
||||
구성 의도가 불명확할 때 임의 해석하지 말고 사용자에게 확인.
|
||||
|
||||
## Schema Data (Ghost CMS)
|
||||
|
||||
FAQPage + BreadcrumbList를 하나의 `<script>` 태그에 `@graph` 배열로 통합. Ghost Post Settings → Code injection → Post Header에 삽입.
|
||||
|
||||
## Output — 3-파일 1세트
|
||||
|
||||
1. **종합 초안** (`jamie-journal_[slug]_comprehensive-draft.md`): SEO + 본문 + 이미지 사양/Gemini 프롬프트 + Schema + 체크리스트 + 편집자 노트
|
||||
2. **Ghost 본문** (`ghost_[slug]_body.md`): 클린 마크다운, 이미지 2번부터, #알립니다# 포함
|
||||
3. **Code Injection** (`ghost_[slug]_code-injection.html`): @graph 통합 스크립트
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
"""journal_validator.py — Jamie Journal Editor validation tool.
|
||||
|
||||
Usage:
|
||||
python journal_validator.py --input article.md
|
||||
python journal_validator.py --input article.md --check structure,compliance
|
||||
python journal_validator.py --input article.md --output report.json --verbose
|
||||
|
||||
Checks available: structure, compliance, spelling, links (default: all)
|
||||
|
||||
Exit codes:
|
||||
0 — validation passed (no critical/high issues)
|
||||
1 — validation failed (critical or high issues found)
|
||||
2 — input error (file not found, bad arguments, etc.)
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from dataclasses import dataclass, asdict, field
|
||||
import argparse
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
checker: str
|
||||
issue_type: str
|
||||
severity: str # critical | high | medium | info
|
||||
line_number: int
|
||||
matched_text: str
|
||||
message: str
|
||||
suggestion: str
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stub checker classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class StructureValidator:
|
||||
"""Validates document structure (headings, sections, word count, etc.)."""
|
||||
|
||||
OPENING = "안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
|
||||
STEP_KEYWORDS = {
|
||||
'problem': ['고민', '증상', '불편', '걱정', '힘드', '어려움'],
|
||||
'cause': ['원인', '발생', '때문에', '이유', '구조적', '해부학적'],
|
||||
'solution': ['시술', '수술', '방법', '제이미', '교정', '개선법'],
|
||||
'advantages': ['장점', '회복', '흉터', '통증', '마취', '보장', 'AS'],
|
||||
'results': ['결과', '효과', '기대', '개선', '자연스러', '만족'],
|
||||
}
|
||||
|
||||
STEP_NAMES = {
|
||||
'problem': '문제 제기 (공감)',
|
||||
'cause': '원인 설명 (교육)',
|
||||
'solution': '해결책 제시 (제이미의 방법)',
|
||||
'advantages': '장점 나열 (차별점)',
|
||||
'results': '기대 효과 (비전)',
|
||||
}
|
||||
|
||||
MEDICAL_NOUNS = ['안검하수', '내시경', '거상술', '눈매교정', '매몰법', '절개법']
|
||||
MEDICAL_TERM_PATTERN = re.compile(r'[가-힣]+\([一-龥々〇]+,\s*[A-Za-z\s\-]+\)')
|
||||
|
||||
CTA_PATTERNS = [
|
||||
re.compile(r'상담을?\s*(추천|받아|예약)'),
|
||||
re.compile(r'제이미성형외과.{0,20}(상담|문의)'),
|
||||
re.compile(r'편안한\s*마음으로\s*상담'),
|
||||
]
|
||||
|
||||
DISCLAIMER_FRAGMENTS = ['부작용', '출혈', '감염', '의료진과 상담']
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 1. Opening format
|
||||
if self.OPENING not in content:
|
||||
issues.append(ValidationIssue(
|
||||
checker='StructureValidator',
|
||||
issue_type='missing_opening',
|
||||
severity='high',
|
||||
line_number=1,
|
||||
matched_text='',
|
||||
message=f'표준 인사말이 없습니다: "{self.OPENING}"',
|
||||
suggestion=f'글 첫 줄을 "{self.OPENING}"으로 시작하세요.',
|
||||
))
|
||||
|
||||
# 2. 5-step body structure
|
||||
detected_steps = []
|
||||
for step_key, keywords in self.STEP_KEYWORDS.items():
|
||||
for kw in keywords:
|
||||
if kw in content:
|
||||
detected_steps.append(step_key)
|
||||
break
|
||||
|
||||
missing_steps = [s for s in self.STEP_KEYWORDS if s not in detected_steps]
|
||||
num_missing = len(missing_steps)
|
||||
|
||||
if num_missing >= 2:
|
||||
missing_names = ', '.join(self.STEP_NAMES[s] for s in missing_steps)
|
||||
issues.append(ValidationIssue(
|
||||
checker='StructureValidator',
|
||||
issue_type='incomplete_body_structure',
|
||||
severity='high',
|
||||
line_number=1,
|
||||
matched_text='',
|
||||
message=f'5단계 본문 구조에서 {num_missing}개 단계가 감지되지 않았습니다: {missing_names}',
|
||||
suggestion='본문에 문제 제기, 원인 설명, 해결책 제시, 장점 나열, 기대 효과 섹션을 모두 포함하세요.',
|
||||
))
|
||||
elif num_missing == 1:
|
||||
missing_names = self.STEP_NAMES[missing_steps[0]]
|
||||
issues.append(ValidationIssue(
|
||||
checker='StructureValidator',
|
||||
issue_type='incomplete_body_structure',
|
||||
severity='medium',
|
||||
line_number=1,
|
||||
matched_text='',
|
||||
message=f'5단계 본문 구조에서 1개 단계가 감지되지 않았습니다: {missing_names}',
|
||||
suggestion=f'"{missing_names}" 섹션에 해당하는 내용을 추가하세요.',
|
||||
))
|
||||
|
||||
# 3. Medical term format
|
||||
has_medical_noun = any(noun in content for noun in self.MEDICAL_NOUNS)
|
||||
if has_medical_noun and not self.MEDICAL_TERM_PATTERN.search(content):
|
||||
issues.append(ValidationIssue(
|
||||
checker='StructureValidator',
|
||||
issue_type='missing_medical_term_format',
|
||||
severity='medium',
|
||||
line_number=1,
|
||||
matched_text='',
|
||||
message='의학 용어가 포함되어 있으나 한자/영문 병기 형식이 없습니다.',
|
||||
suggestion='예: "안검하수(眼瞼下垂, Ptosis)는 ..." 형식으로 첫 의학 용어를 병기하세요.',
|
||||
))
|
||||
|
||||
# 4. Closing CTA (last 20% of content)
|
||||
tail_start = max(0, int(len(content) * 0.8))
|
||||
tail_content = content[tail_start:]
|
||||
has_cta = any(p.search(tail_content) for p in self.CTA_PATTERNS)
|
||||
if not has_cta:
|
||||
total_lines = content.count('\n') + 1
|
||||
issues.append(ValidationIssue(
|
||||
checker='StructureValidator',
|
||||
issue_type='missing_closing_cta',
|
||||
severity='medium',
|
||||
line_number=total_lines,
|
||||
matched_text='',
|
||||
message='글 마지막 20% 구간에 상담 유도(CTA) 문구가 없습니다.',
|
||||
suggestion='"지금 바로 제이미성형외과의 [시술명] 상담을 추천드립니다." 형식의 CTA를 추가하세요.',
|
||||
))
|
||||
|
||||
# 5. Required disclaimer (last 30% of content)
|
||||
disclaimer_start = max(0, int(len(content) * 0.7))
|
||||
disclaimer_content = content[disclaimer_start:]
|
||||
found_fragments = [f for f in self.DISCLAIMER_FRAGMENTS if f in disclaimer_content]
|
||||
if len(found_fragments) < 2:
|
||||
total_lines = content.count('\n') + 1
|
||||
issues.append(ValidationIssue(
|
||||
checker='StructureValidator',
|
||||
issue_type='missing_disclaimer',
|
||||
severity='high',
|
||||
line_number=total_lines,
|
||||
matched_text='',
|
||||
message='글 하단에 부작용 고지 면책 문구가 불충분합니다 (부작용, 출혈, 감염, 의료진과 상담 중 2개 이상 필요).',
|
||||
suggestion='"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다." 문구를 추가하세요.',
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
class ComplianceChecker:
|
||||
"""Checks brand compliance rules for Jamie Clinic journal content."""
|
||||
|
||||
PROHIBITED_PATTERNS = {
|
||||
'effect_guarantee': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'100[%%]\s*(?:만족|효과|성공)',
|
||||
r'반드시\s+(?:효과|개선|만족)',
|
||||
r'완벽한?\s+결과',
|
||||
r'보장합니다',
|
||||
r'확실한?\s+효과',
|
||||
r'틀림없이?\s+(?:효과|개선)',
|
||||
],
|
||||
'message': '효과를 보장하는 표현은 의료법 제56조 제2항 제2호 위반입니다.',
|
||||
'suggestion': '"개선에 도움을 줄 수 있습니다" 또는 "개인에 따라 결과가 다를 수 있습니다"로 변경하세요.',
|
||||
},
|
||||
'comparative_superiority': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'최고의?',
|
||||
r'(?:국내|강남|서울|압구정)\s*(?:최고|1위|최초)',
|
||||
r'1위',
|
||||
r'타\s*병원보다',
|
||||
r'다른\s*(?:병원|의원)보다\s*(?:우수|뛰어|좋)',
|
||||
r'업계\s*최초',
|
||||
r'독보적',
|
||||
],
|
||||
'message': '타 의료기관과의 비교 우위 주장은 의료법 제56조 제2항 제4호 위반입니다.',
|
||||
'suggestion': '"풍부한 경험을 보유한" 또는 "저희만의 방법으로"와 같은 객관적 표현으로 변경하세요.',
|
||||
},
|
||||
'safety_guarantee': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'부작용\s*(?:없|無|제로|zero)',
|
||||
r'(?:100[%%]|완전히?|절대)\s*안전',
|
||||
r'위험\s*(?:없|無)',
|
||||
r'흉터\s*(?:없|無|제로|zero)',
|
||||
r'무통(?:증)?(?:\s|$|,)',
|
||||
],
|
||||
'message': '안전성을 보장하는 표현은 의료법 제56조 제2항 제7호 위반입니다.',
|
||||
'suggestion': '부작용 가능성을 명시하고 "안전한 수술을 위해 최선을 다합니다"로 변경하세요.',
|
||||
},
|
||||
'patient_testimonial': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'(?:환자|고객)\s*[가-힣A-Z]+\s*(?:씨|님)의?\s*(?:후기|경험)',
|
||||
r'실제\s*(?:환자|고객)\s*(?:후기|리뷰|경험담)',
|
||||
r'[""]\s*(?:정말|너무|진짜)\s+(?:만족|좋아요|감사)',
|
||||
r'수술\s*후\s*\d+\s*(?:개월|주일|년)\s*(?:만족|경과)',
|
||||
],
|
||||
'message': '환자 치료경험담은 의료법 제56조 제2항 제2호 위반입니다.',
|
||||
'suggestion': '환자 후기를 제거하고, 일반적인 수술 과정 설명으로 대체하세요.',
|
||||
},
|
||||
'exaggeration': {
|
||||
'severity': 'high',
|
||||
'patterns': [
|
||||
r'(?:놀라운|대박|극적인)\s*(?:변화|효과|결과)',
|
||||
r'마법같은?',
|
||||
r'기적적인?',
|
||||
r'다시\s*태어나',
|
||||
r'완벽\s*변신',
|
||||
],
|
||||
'message': '과장된 표현은 의료법 제56조 제2항 제3호 위반 가능성이 있습니다.',
|
||||
'suggestion': '"자연스러운 개선", "점진적인 효과"와 같은 절제된 표현으로 변경하세요.',
|
||||
},
|
||||
'specialty_claim': {
|
||||
'severity': 'high',
|
||||
'patterns': [
|
||||
r'전문\s*병원',
|
||||
r'특화\s*(?:병원|클리닉|센터)',
|
||||
r'노하우',
|
||||
],
|
||||
'message': '전문병원 지정 없이 "전문", "특화" 표현 사용은 과장 광고에 해당합니다.',
|
||||
'suggestion': '"중점진료", "집중진료", "풍부한 경험"으로 변경하세요.',
|
||||
},
|
||||
}
|
||||
|
||||
PROCEDURE_KEYWORDS = [
|
||||
'수술', '시술', '이마거상', '쌍꺼풀', '리프팅', '보톡스', '필러',
|
||||
'눈매교정', '안검하수', '지방이식', '절개',
|
||||
]
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
# Check prohibited patterns
|
||||
for category, rule in self.PROHIBITED_PATTERNS.items():
|
||||
for raw_pat in rule['patterns']:
|
||||
compiled = re.compile(raw_pat)
|
||||
for m in compiled.finditer(content):
|
||||
line_number = content[:m.start()].count('\n') + 1
|
||||
issues.append(ValidationIssue(
|
||||
checker='ComplianceChecker',
|
||||
issue_type=category,
|
||||
severity=rule['severity'],
|
||||
line_number=line_number,
|
||||
matched_text=m.group(),
|
||||
message=rule['message'],
|
||||
suggestion=rule['suggestion'],
|
||||
))
|
||||
|
||||
# Check for missing disclaimers when procedure keywords present
|
||||
has_procedure = any(kw in content for kw in self.PROCEDURE_KEYWORDS)
|
||||
if has_procedure:
|
||||
side_effect_terms = ['부작용', '합병증', '붓기', '멍', '염증']
|
||||
if not any(t in content for t in side_effect_terms):
|
||||
total_lines = len(lines)
|
||||
issues.append(ValidationIssue(
|
||||
checker='ComplianceChecker',
|
||||
issue_type='missing_side_effect_notice',
|
||||
severity='high',
|
||||
line_number=total_lines,
|
||||
matched_text='',
|
||||
message='시술/수술 내용이 있으나 부작용 안내가 없습니다.',
|
||||
suggestion='"부작용(출혈, 감염, 염증 등)이 있을 수 있습니다" 형태의 안내를 추가하세요.',
|
||||
))
|
||||
|
||||
individual_variation_pattern = re.compile(r'개인(?:에 따라|마다|별|차이|.{0,30}다를 수|.{0,30}다릅니다)')
|
||||
if not individual_variation_pattern.search(content):
|
||||
total_lines = len(lines)
|
||||
issues.append(ValidationIssue(
|
||||
checker='ComplianceChecker',
|
||||
issue_type='missing_individual_variation_notice',
|
||||
severity='medium',
|
||||
line_number=total_lines,
|
||||
matched_text='',
|
||||
message='시술/수술 내용이 있으나 개인차 고지가 없습니다.',
|
||||
suggestion='"개인에 따라 결과가 다를 수 있습니다" 문구를 추가하세요.',
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
class SpellingChecker:
|
||||
"""Checks for common spelling/terminology issues in medical/beauty content."""
|
||||
|
||||
# Format: wrong_term -> (correct_term, category)
|
||||
MEDICAL_DICT: Dict[str, Tuple[str, str]] = {
|
||||
'쌍커풀': ('쌍꺼풀', '눈 성형'),
|
||||
'쌍거풀': ('쌍꺼풀', '눈 성형'),
|
||||
'안겸하수': ('안검하수', '눈 성형'),
|
||||
'눈꺼플': ('눈꺼풀', '눈 성형'),
|
||||
'눈커풀': ('눈꺼풀', '눈 성형'),
|
||||
'네시경': ('내시경', '이마 성형'),
|
||||
'거상 술': ('거상술', '이마 성형'),
|
||||
'앤도타인': ('엔도타인', '이마 성형'),
|
||||
'봉합 사': ('봉합사', '이마 성형'),
|
||||
'히알루론 산': ('히알루론산', '필러'),
|
||||
'히알류론산': ('히알루론산', '필러'),
|
||||
'지방 이식': ('지방이식', '동안 성형'),
|
||||
'눈밑지방': ('눈밑 지방', '눈 성형'),
|
||||
'다크 서클': ('다크서클', '눈 성형'),
|
||||
'팔자주름': ('팔자 주름', '동안 성형'),
|
||||
'절게법': ('절개법', '눈 성형'),
|
||||
'매몰 법': ('매몰법', '눈 성형'),
|
||||
'눈썹밑절개': ('눈썹밑 절개', '이마 성형'),
|
||||
}
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
# Tier 1: built-in medical dictionary scan
|
||||
for line_idx, line in enumerate(lines, start=1):
|
||||
for wrong, (correct, category) in self.MEDICAL_DICT.items():
|
||||
if wrong == correct:
|
||||
continue
|
||||
if wrong in line:
|
||||
issues.append(ValidationIssue(
|
||||
checker='SpellingChecker',
|
||||
issue_type='medical_misspelling',
|
||||
severity='medium',
|
||||
line_number=line_idx,
|
||||
matched_text=wrong,
|
||||
message=f'[{category}] 잘못된 표기: "{wrong}" → "{correct}"',
|
||||
suggestion=f'"{wrong}"을 "{correct}"로 수정하세요.',
|
||||
))
|
||||
|
||||
# Tier 2: optional py-hanspell
|
||||
try:
|
||||
from hanspell import spell_checker # type: ignore
|
||||
for line_idx, line in enumerate(lines, start=1):
|
||||
stripped = line.strip()
|
||||
# Skip code blocks, short lines, headings
|
||||
if stripped.startswith('```') or stripped.startswith('#') or len(stripped) < 5:
|
||||
continue
|
||||
try:
|
||||
result = spell_checker.check(stripped)
|
||||
if result.checked != stripped:
|
||||
issues.append(ValidationIssue(
|
||||
checker='SpellingChecker',
|
||||
issue_type='hanspell_correction',
|
||||
severity='medium',
|
||||
line_number=line_idx,
|
||||
matched_text=stripped[:80],
|
||||
message=f'맞춤법 교정 제안: {result.result}',
|
||||
suggestion=result.checked[:200],
|
||||
))
|
||||
except Exception:
|
||||
pass # Network or parse error — skip this line
|
||||
except ImportError:
|
||||
issues.append(ValidationIssue(
|
||||
checker='SpellingChecker',
|
||||
issue_type='hanspell_unavailable',
|
||||
severity='info',
|
||||
line_number=0,
|
||||
matched_text='',
|
||||
message='py-hanspell이 설치되어 있지 않아 맞춤법 자동 검사를 건너뜁니다.',
|
||||
suggestion='pip install py-hanspell 을 실행하면 한국어 맞춤법 자동 검사가 활성화됩니다.',
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
class LinkRecommender:
|
||||
"""Recommends internal/external links and detects broken or missing ones."""
|
||||
|
||||
PROCEDURE_MAP: Dict[str, Tuple[str, List[str]]] = {
|
||||
'double_eyelid': ('쌍꺼풀 수술', ['쌍꺼풀', '매몰법', '절개법', '퀵매몰']),
|
||||
'ptosis': ('안검하수 눈매교정', ['안검하수', '눈매교정', '졸린 눈', '눈매 교정']),
|
||||
'under_eye_fat': ('눈밑 지방 재배치', ['눈밑 지방', '다크서클', '눈밑지방', '지방 재배치']),
|
||||
'dual_epicanthoplasty': ('듀얼 트임', ['듀얼 트임', '앞트임', '뒤트임', '눈꼬리']),
|
||||
'forehead_lift': ('내시경 이마거상술', ['이마거상', '이마리프팅', '이마 거상', '이마 리프팅']),
|
||||
'brow_lift': ('내시경 눈썹거상술', ['눈썹거상', '눈썹리프팅', '눈썹 거상', '눈썹 리프팅']),
|
||||
'sub_brow': ('눈썹밑 피부절개술', ['눈썹밑', '상안검', '눈꺼풀 처짐']),
|
||||
'smas_lifting': ('스마스 리프팅', ['스마스', 'SMAS', '안면거상', '페이스리프트']),
|
||||
'fat_graft': ('자가지방 이식', ['자가지방', '지방이식', '지방 이식', '지방 생착']),
|
||||
'hifu': ('하이푸 리프팅', ['하이푸', 'HIFU', '초음파 리프팅']),
|
||||
'cheek_lift': ('앞광대 리프팅', ['앞광대', '팔자 주름', '팔자주름', '광대 리프팅']),
|
||||
'endotine': ('엔도타인 고정', ['엔도타인', 'endotine', '3점 고정']),
|
||||
}
|
||||
|
||||
def _detect_main_topic(self, lines: List[str]) -> Optional[str]:
|
||||
"""Scan first 10 lines to find the main procedure topic."""
|
||||
opening_text = '\n'.join(lines[:10])
|
||||
for topic_key, (display_name, keywords) in self.PROCEDURE_MAP.items():
|
||||
for kw in keywords:
|
||||
if kw in opening_text:
|
||||
return topic_key
|
||||
return None
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
main_topic = self._detect_main_topic(lines)
|
||||
|
||||
for topic_key, (display_name, keywords) in self.PROCEDURE_MAP.items():
|
||||
if topic_key == main_topic:
|
||||
continue
|
||||
for line_idx, line in enumerate(lines, start=1):
|
||||
matched_kw = None
|
||||
for kw in keywords:
|
||||
if kw in line:
|
||||
matched_kw = kw
|
||||
break
|
||||
if matched_kw:
|
||||
issues.append(ValidationIssue(
|
||||
checker='LinkRecommender',
|
||||
issue_type='internal_link_opportunity',
|
||||
severity='info',
|
||||
line_number=line_idx,
|
||||
matched_text=matched_kw,
|
||||
message=f'"{matched_kw}" 언급 발견 — "{display_name}" 글로의 내부 링크를 고려하세요.',
|
||||
suggestion=f'"{display_name}" 시술 페이지로 연결되는 앵커 텍스트 링크를 추가하세요.',
|
||||
))
|
||||
break # one recommendation per topic
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Orchestrator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VALID_CHECKS = {"structure", "compliance", "spelling", "links"}
|
||||
|
||||
_CHECKER_MAP = {
|
||||
"structure": StructureValidator,
|
||||
"compliance": ComplianceChecker,
|
||||
"spelling": SpellingChecker,
|
||||
"links": LinkRecommender,
|
||||
}
|
||||
|
||||
|
||||
class JournalValidator:
|
||||
"""Orchestrates all validation checks and returns a consolidated report.
|
||||
|
||||
Args:
|
||||
checks: List of check names to run. Defaults to all valid checks.
|
||||
Valid values: 'structure', 'compliance', 'spelling', 'links'.
|
||||
"""
|
||||
|
||||
def __init__(self, checks: Optional[List[str]] = None):
|
||||
if checks is None:
|
||||
self.checks = list(VALID_CHECKS)
|
||||
else:
|
||||
unknown = set(checks) - VALID_CHECKS
|
||||
if unknown:
|
||||
raise ValueError(
|
||||
f"Unknown check(s): {', '.join(sorted(unknown))}. "
|
||||
f"Valid options: {', '.join(sorted(VALID_CHECKS))}"
|
||||
)
|
||||
self.checks = checks
|
||||
|
||||
def validate(self, content: str) -> Dict:
|
||||
"""Run all enabled checkers against content and return a report dict.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"is_valid": bool, # False if any critical or high issue exists
|
||||
"total_issues": int,
|
||||
"issues_by_severity": {
|
||||
"critical": int,
|
||||
"high": int,
|
||||
"medium": int,
|
||||
"info": int,
|
||||
},
|
||||
"issues": [dict, ...], # serialised ValidationIssue dicts
|
||||
"summary": str,
|
||||
}
|
||||
"""
|
||||
all_issues: List[ValidationIssue] = []
|
||||
|
||||
for check_name in self.checks:
|
||||
checker_cls = _CHECKER_MAP[check_name]
|
||||
checker = checker_cls()
|
||||
issues = checker.check(content)
|
||||
all_issues.extend(issues)
|
||||
|
||||
counts: Dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "info": 0}
|
||||
for issue in all_issues:
|
||||
severity = issue.severity if issue.severity in counts else "info"
|
||||
counts[severity] += 1
|
||||
|
||||
is_valid = counts["critical"] == 0 and counts["high"] == 0
|
||||
|
||||
if not all_issues:
|
||||
summary = "All checks passed."
|
||||
else:
|
||||
parts = []
|
||||
if counts["critical"]:
|
||||
parts.append(f"{counts['critical']} critical")
|
||||
if counts["high"]:
|
||||
parts.append(f"{counts['high']} high")
|
||||
if counts["medium"]:
|
||||
parts.append(f"{counts['medium']} medium")
|
||||
if counts["info"]:
|
||||
parts.append(f"{counts['info']} info")
|
||||
summary = f"Found {len(all_issues)} issue(s): {', '.join(parts)}."
|
||||
|
||||
return {
|
||||
"is_valid": is_valid,
|
||||
"total_issues": len(all_issues),
|
||||
"issues_by_severity": counts,
|
||||
"issues": [asdict(i) for i in all_issues],
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="journal_validator",
|
||||
description="Validate a Jamie Journal article against editorial rules.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input", "-i",
|
||||
required=True,
|
||||
metavar="FILE",
|
||||
help="Path to the markdown article to validate.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check", "-c",
|
||||
default="all",
|
||||
metavar="CHECKS",
|
||||
help=(
|
||||
"Comma-separated list of checks to run "
|
||||
"(structure, compliance, spelling, links). "
|
||||
"Default: all"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o",
|
||||
metavar="FILE",
|
||||
help="Optional path to write the JSON report.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Print detailed issue list.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def _print_summary_table(report: Dict, verbose: bool = False) -> None:
|
||||
sev = report["issues_by_severity"]
|
||||
print("\n=== Journal Validator Report ===")
|
||||
print(f" Status : {'PASS' if report['is_valid'] else 'FAIL'}")
|
||||
print(f" Total : {report['total_issues']}")
|
||||
print(f" Critical: {sev['critical']}")
|
||||
print(f" High : {sev['high']}")
|
||||
print(f" Medium : {sev['medium']}")
|
||||
print(f" Info : {sev['info']}")
|
||||
print(f"\n {report['summary']}")
|
||||
|
||||
if verbose and report["issues"]:
|
||||
print("\n--- Issues ---")
|
||||
for i, issue in enumerate(report["issues"], start=1):
|
||||
print(
|
||||
f" [{i}] [{issue['severity'].upper()}] "
|
||||
f"Line {issue['line_number']} | {issue['checker']} | "
|
||||
f"{issue['message']}"
|
||||
)
|
||||
if issue.get("suggestion"):
|
||||
print(f" Suggestion: {issue['suggestion']}")
|
||||
print()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# --- Resolve checks ---
|
||||
if args.check.strip().lower() == "all":
|
||||
checks = list(VALID_CHECKS)
|
||||
else:
|
||||
checks = [c.strip() for c in args.check.split(",") if c.strip()]
|
||||
|
||||
# --- Read input file ---
|
||||
try:
|
||||
with open(args.input, "r", encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: file not found: {args.input}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
except OSError as exc:
|
||||
print(f"Error reading file: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# --- Run validation ---
|
||||
try:
|
||||
validator = JournalValidator(checks=checks)
|
||||
except ValueError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
report = validator.validate(content)
|
||||
|
||||
# --- Output ---
|
||||
_print_summary_table(report, verbose=args.verbose)
|
||||
|
||||
if args.output:
|
||||
try:
|
||||
with open(args.output, "w", encoding="utf-8") as fh:
|
||||
json.dump(report, fh, ensure_ascii=False, indent=2)
|
||||
print(f"Report written to: {args.output}")
|
||||
except OSError as exc:
|
||||
print(f"Error writing report: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
sys.exit(0 if report["is_valid"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,6 +3,7 @@ name: jamie-journal-editor
|
||||
description: |
|
||||
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.
|
||||
Supports both full drafting (research → outline → article) and editing existing manuscripts.
|
||||
Triggers: Jamie journal, 제이미 저널, 진료실 이야기, journal blog, Jamie blog post, 블로그 콘텐츠.
|
||||
license: Internal-use Only
|
||||
---
|
||||
@@ -11,6 +12,7 @@ license: Internal-use Only
|
||||
|
||||
> **Purpose**: Create and edit educational blog content for Jamie Clinic's journal channel
|
||||
> **Channel**: "정기호의 성형외과 진료실 이야기" (https://journal.jamie.clinic)
|
||||
> **CMS**: Ghost (journal.jamie.clinic)
|
||||
> **Partner Skills**: `jamie-brand-audit` (compliance review), `jamie-brand-editor` (general branded content)
|
||||
|
||||
---
|
||||
@@ -27,6 +29,27 @@ license: Internal-use Only
|
||||
|
||||
---
|
||||
|
||||
## Workflow Modes
|
||||
|
||||
### Mode A: Full Drafting (no manuscript)
|
||||
1. Receive topic/brief with target audience
|
||||
2. Deep research on the given topic
|
||||
3. Develop outline or memo for user approval
|
||||
4. Draft article using brand voice and content structure
|
||||
5. Generate SEO metadata, image specs, and schema data
|
||||
6. Run compliance review
|
||||
7. Submit to `jamie-brand-audit` for final review
|
||||
|
||||
### Mode B: Manuscript Editing (원고 편집)
|
||||
1. Receive manuscript (초안) from Dr. Jung
|
||||
2. Preserve original voice, analogies, and expressions
|
||||
3. Restructure for web readability and SEO
|
||||
4. Add SEO metadata, image specs, and schema data
|
||||
5. Run compliance review
|
||||
6. Submit to `jamie-brand-audit` for final review
|
||||
|
||||
---
|
||||
|
||||
## Brand Voice (Dr. Jung's Journal Voice)
|
||||
|
||||
### Brand Personality (5 Traits)
|
||||
@@ -73,6 +96,8 @@ license: Internal-use Only
|
||||
[주제에 대한 일반적인 오해나 필요성 언급]
|
||||
```
|
||||
|
||||
> 시리즈 후속편의 경우 "지난 글에서~"로 대체 가능.
|
||||
|
||||
### Body (5-Step Structure)
|
||||
1. **Problem Statement** (Empathy) - Patient concerns/symptoms
|
||||
2. **Cause Explanation** (Education) - Why this problem occurs
|
||||
@@ -88,14 +113,31 @@ license: Internal-use Only
|
||||
언제든지 편안한 마음으로 상담해 주시면 감사하겠습니다.
|
||||
```
|
||||
|
||||
### Required Disclaimer
|
||||
### Required Disclaimer (#알립니다#)
|
||||
```
|
||||
"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니
|
||||
사전에 의료진과 상담 후 결정하시기 바랍니다."
|
||||
※ 본 콘텐츠는 의학 정보 제공을 목적으로 작성되었으며,
|
||||
개인의 상태에 따라 적합한 치료 방법은 다를 수 있습니다.
|
||||
|
||||
※ 모든 수술 및 시술은 개인에 따라 결과가 다를 수 있으며,
|
||||
출혈, 감염, 붓기, 비대칭 등의 부작용이 발생할 수 있습니다.
|
||||
반드시 전문의와 충분한 상담 후 결정하시기 바랍니다.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO Metadata
|
||||
|
||||
각 포스트에 다음 SEO 메타데이터를 생성합니다:
|
||||
|
||||
| 항목 | 형식 |
|
||||
|------|------|
|
||||
| 메타 제목 | [주제] — [핵심키워드] \| 제이미성형외과 (60자 이내) |
|
||||
| 메타 디스크립션 | 120-160자, 핵심 키워드 포함, CTA 암시 |
|
||||
| Slug | 영문 소문자, 하이픈 구분, 핵심 키워드 3-5개 |
|
||||
| 태그 | 핵심 키워드 5-10개 (한글) |
|
||||
|
||||
---
|
||||
|
||||
## Analogy Dictionary (Dr. Jung's Signature Metaphors)
|
||||
|
||||
| Topic | Metaphor |
|
||||
@@ -109,11 +151,20 @@ license: Internal-use Only
|
||||
|
||||
## Medical Terminology Pattern
|
||||
|
||||
When introducing medical terms, always use this format:
|
||||
본문 첫 등장 시 다음 형식을 사용합니다:
|
||||
```
|
||||
안검하수(眼瞼下垂, Ptosis)는 눈을 뜨는 근육의 힘이 약해져 눈꺼풀이 처지는 현상을 말합니다.
|
||||
```
|
||||
|
||||
**기본 형식**: 국문(English) — 예: 안검외반(Ectropion)
|
||||
|
||||
| 위치 | 영문 병기 | 예시 |
|
||||
|------|----------|------|
|
||||
| 본문 텍스트 | 첫 등장 시 적용 | "연부조직(Soft Tissue)이 거상(Lifting)되고" |
|
||||
| 섹션 타이틀/H2 | 선택적 — 핵심 시술명에만 | "## 안검외반에 대하여" (영문 생략 가능) |
|
||||
| 그래픽 이미지 | 핵심 해부학 용어에만 | 도식 라벨에 주요 해부학 용어만 병기 |
|
||||
| Featured Image | 사용하지 않음 | 국문만 사용 — 예외 없음 |
|
||||
|
||||
---
|
||||
|
||||
## Procedure Copy Reference
|
||||
@@ -161,6 +212,7 @@ When introducing medical terms, always use this format:
|
||||
- Use Dr. Jung's signature analogies
|
||||
- Include specific numbers: "5년간 AS 보장", "1시간 내외"
|
||||
- Set realistic expectations: "개선에 한계가 있을 수 있습니다"
|
||||
- 원고 편집 시 원문의 독특한 표현·비유·직설적 경고는 최대한 보존
|
||||
|
||||
### Don'ts
|
||||
| Prohibited | Replacement |
|
||||
@@ -168,18 +220,225 @@ When introducing medical terms, always use this format:
|
||||
| "100% 성공" | "대부분의 경우 좋은 결과를 기대할 수 있습니다" |
|
||||
| "부작용 없음" | "부작용은 극히 드뭅니다" |
|
||||
| "반드시 좋아집니다" | "개선을 기대할 수 있겠습니다" |
|
||||
| "전문/전문병원" | "중점진료", "풍부한 경험" |
|
||||
| "완벽/최고/최상" | 사용 불가 |
|
||||
| "무통/완치/해결" | "개선", "지향" |
|
||||
| Competitor comparison | "저희만의 방법으로..." |
|
||||
| Casual tone | Standard formal speech |
|
||||
|
||||
---
|
||||
|
||||
## Image Placement (Recommended)
|
||||
## Image Specifications
|
||||
|
||||
1. After introduction: Hero image (procedure concept)
|
||||
2. After definition/cause: Anatomical diagram
|
||||
3. After procedure explanation: Process infographic
|
||||
4. After advantages/effects: Comparison chart or summary
|
||||
5. Before closing: CTA image (optional)
|
||||
### Visual DNA & Color System
|
||||
|
||||
모든 저널 그래픽의 3가지 원칙:
|
||||
1. **사실적 리얼리티** — Featured Image는 실사 모델 사진 기반. 삽화·AI 캐릭터 지양.
|
||||
2. **정보의 미니멀리즘** — 필요한 텍스트만 배치, 여백으로 신뢰감 표현.
|
||||
3. **의학적 정확성** — 해부학 도식은 실제 수술 부위 기준으로 정확히 표현.
|
||||
|
||||
**컬러 팔레트**:
|
||||
|
||||
| 역할 | HEX | 용도 |
|
||||
|------|-----|------|
|
||||
| Background | `#E0E5EB` | 모든 이미지 배경 — flat only, 그라디언트 금지 |
|
||||
| Primary Text | `#333333` | 본문 라벨, 제목, 설명 |
|
||||
| Accent / Arrow | `#6B8FAF` | 화살표, 활성 노드, 강조 선 |
|
||||
| Secondary | `#8B9BA8` | 보조 라벨, 비활성 단계, 윤곽선 |
|
||||
| Jamie Main Green | `#6d7856` | JAMIE 텍스트 로고 전용 |
|
||||
|
||||
### Image Placement (본문 내 위치)
|
||||
|
||||
> Featured Image는 Ghost Post Settings에서 설정하며, 본문에 삽입하지 않습니다. 본문 이미지는 2번부터 시작합니다.
|
||||
|
||||
| Position | Placement | Content |
|
||||
|----------|-----------|---------|
|
||||
| Featured | Ghost Post Settings (본문 밖) | 실사 모델 + 국문 카피 (1200×675px, 16:9) |
|
||||
| 2 | After definition/cause | 해부학 도식 (1200×600px) |
|
||||
| 3 | After procedure explanation | 프로세스 인포그래픽 |
|
||||
| 4 | After advantages/effects | 비교 차트 또는 요약 |
|
||||
| 5 | Before closing (optional) | CTA 이미지 |
|
||||
|
||||
### Featured Image Rules
|
||||
|
||||
| 규칙 | 상세 |
|
||||
|------|------|
|
||||
| 실사 모델 필수 | 자연스러운 메이크업, 정면 또는 3/4 측면, 균일한 조명 |
|
||||
| 카피 규칙 | 포스트 제목 그대로 사용 금지. 핵심 정보를 전달하는 표현으로 작성 |
|
||||
| 영문 절대 금지 | 국문만 사용 — 예외 없음 |
|
||||
| JAMIE 워터마크 | 이미지 생성 앱의 자체 옵션으로 처리 (우하단, `#6d7856`, 반투명) |
|
||||
|
||||
**카피라이팅 예시**:
|
||||
|
||||
| 포스트 주제 | Featured Image 카피 |
|
||||
|------------|-------------------|
|
||||
| 앞광대리프팅 회복과정과 부작용 | "앞광대 리프팅, 회복 과정과 주의할 부작용" |
|
||||
| 내시경 이마거상술의 원리 | "내시경 이마거상술, 처진 이마와 눈썹을 올리는 원리" |
|
||||
| 눈 수술 다음날 세안 | "눈 수술 후 세안, 언제부터 어떻게 해야 할까" |
|
||||
|
||||
### Image File Naming
|
||||
|
||||
형식: `[topic]-[detail]-[type]-[number].png`
|
||||
|
||||
| Type keyword | 의미 |
|
||||
|-------------|------|
|
||||
| `featured` | 대표 이미지 |
|
||||
| `recovery-timeline` | 회복 타임라인 |
|
||||
| `comparison` | 비교 도식 |
|
||||
| `anatomy` | 해부학 도식 |
|
||||
| `infographic` | 요약 인포그래픽 |
|
||||
| `flowchart` | 플로우차트 |
|
||||
|
||||
### Prohibited Elements (모든 이미지 공통)
|
||||
|
||||
- 실제 수술 장면 (혈액, 절개 과정)
|
||||
- "Before" / "After" 라벨 직접 표기한 전후 비교 사진
|
||||
- 수술 도구의 사실적 사진 (간결한 일러스트는 허용)
|
||||
- 3D 효과, 그림자, 그라디언트 배경
|
||||
- 장식적 테두리, 프레임, 스톡 클립아트, 이모지
|
||||
- 경쟁 병원, 타 브랜드 노출
|
||||
|
||||
---
|
||||
|
||||
## Gemini Image Prompt Templates
|
||||
|
||||
각 이미지 구성안 아래에 Gemini 프롬프트를 코드 스니펫으로 제시합니다.
|
||||
|
||||
**작성 원칙**:
|
||||
- `[PRIMARY]`와 `[SECONDARY]` 블록으로 시각적 위계 구조화
|
||||
- 컬러 코드는 HEX로 명시
|
||||
- 의료 고지문은 프롬프트에 포함하지 않음
|
||||
- 구성 의도가 불명확할 때 임의 해석하지 말고 사용자에게 확인
|
||||
|
||||
### Template 1 — Featured Image
|
||||
```
|
||||
Generate a realistic photograph of an Asian woman [나이대 및 표정 묘사].
|
||||
Studio background: flat light blue-gray (#E0E5EB). Natural lighting, soft left-side.
|
||||
|
||||
[PRIMARY — must be visually dominant]
|
||||
Korean text overlay: "[featured image 카피 — 국문만]"
|
||||
Font: bold Korean gothic, color #333333, lower-third or center-left alignment
|
||||
|
||||
[SECONDARY — subtle supporting]
|
||||
Clean negative space for text breathing room
|
||||
|
||||
Aspect ratio: 16:9, size: 1200x675px
|
||||
Do NOT include: English copy text, decorative borders, gradients
|
||||
```
|
||||
|
||||
### Template 2 — Timeline (회복 과정)
|
||||
```
|
||||
Create a clean medical timeline infographic.
|
||||
Background: flat #E0E5EB, no gradients, no texture.
|
||||
|
||||
[PRIMARY — visually dominant]
|
||||
Horizontal timeline with [N] milestone nodes:
|
||||
- [Day 0] "[라벨]" (node color #6B8FAF, active)
|
||||
- [Day N] "[라벨]" (node color #8B9BA8, transitional)
|
||||
Connecting line: solid #6B8FAF
|
||||
|
||||
[SECONDARY — supporting]
|
||||
Below each node: one-line Korean description, #333333, clean gothic font
|
||||
|
||||
Size: 1200x600px
|
||||
Do NOT include: photographs, 3D effects, decorative borders, unnecessary English labels
|
||||
```
|
||||
|
||||
### Template 3 — Comparison (비교 도식)
|
||||
```
|
||||
Create a clean medical comparison diagram.
|
||||
Background: flat #E0E5EB, no gradients.
|
||||
|
||||
[PRIMARY — visually dominant]
|
||||
Two-column comparison layout:
|
||||
Left: "[옵션 A]" — [주요 특징들]
|
||||
Right: "[옵션 B]" — [주요 특징들]
|
||||
Dividing element: thin line, color #8B9BA8
|
||||
|
||||
[SECONDARY — supporting]
|
||||
Section headers: bold #333333, Accent arrows: #6B8FAF
|
||||
|
||||
Size: 1200x600px
|
||||
Do NOT include: 3D effects, stock clip-art, English labels (except key medical terms in parentheses)
|
||||
```
|
||||
|
||||
### Template 4 — Anatomy (해부학 도식)
|
||||
```
|
||||
Create a clean medical anatomy diagram in minimal line-art style.
|
||||
Background: flat #E0E5EB.
|
||||
|
||||
[PRIMARY — visually dominant]
|
||||
[해부학 구조 묘사 — 위치, 방향, 화살표]
|
||||
Directional arrows: #6B8FAF (bold, clear)
|
||||
Key anatomical labels in Korean with English in parentheses for primary terms only
|
||||
|
||||
[SECONDARY — supporting]
|
||||
Secondary labels: #8B9BA8
|
||||
Body outline or cross-section: thin lines, #8B9BA8
|
||||
|
||||
Size: 1200x600px
|
||||
Do NOT include: realistic tissue rendering, gore, graphic surgical scenes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema Data (Ghost CMS)
|
||||
|
||||
Ghost CMS는 Article Schema를 자동 생성하므로 **FAQPage + BreadcrumbList**만 수동 추가합니다.
|
||||
두 스키마를 하나의 `<script>` 태그 안에 `@graph` 배열로 통합합니다.
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "질문 1",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "답변 1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{ "@type": "ListItem", "position": 1, "name": "정기호의 진료실 이야기", "item": "https://journal.jamie.clinic/" },
|
||||
{ "@type": "ListItem", "position": 2, "name": "[카테고리/태그명]", "item": "https://journal.jamie.clinic/tag/[태그슬러그]/" },
|
||||
{ "@type": "ListItem", "position": 3, "name": "[포스트 제목]", "item": "https://journal.jamie.clinic/[slug]/" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
> 삽입 위치: Ghost Post Settings → Code injection → **Post Header**
|
||||
|
||||
---
|
||||
|
||||
## Output Format — 3-파일 1세트
|
||||
|
||||
최종 출력물은 3개 파일을 1세트로 생성합니다:
|
||||
|
||||
### File 1: 종합 초안 (`jamie-journal_[topic-slug]_comprehensive-draft.md`)
|
||||
- SEO 메타데이터
|
||||
- 블로그 본문 초안 (마크다운, 이미지 위치 표시)
|
||||
- 이미지 사양 (각 이미지별 구성안 + Gemini 프롬프트)
|
||||
- Schema 데이터
|
||||
- 브랜드 보이스 / 의료광고 심의 체크리스트
|
||||
- 편집자 노트 (원문 편집 시)
|
||||
|
||||
### File 2: Ghost 본문 (`ghost_[topic-slug]_body.md`)
|
||||
Ghost 에디터에 바로 붙여넣기 가능한 클린 마크다운. Featured Image 자리표시 없음 (본문 밖). 이미지 2번부터 자리표시. #알립니다# 포함.
|
||||
|
||||
### File 3: Ghost Code Injection (`ghost_[topic-slug]_code-injection.html`)
|
||||
FAQPage + BreadcrumbList 통합 스크립트. 상단 HTML 주석으로 포스트 제목, slug, 삽입 위치 안내.
|
||||
|
||||
---
|
||||
|
||||
@@ -196,12 +455,18 @@ desktop/references/
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Blog Post
|
||||
### Blog Post (Full Drafting)
|
||||
```
|
||||
"내시경 이마거상술에 대한 저널 포스트 작성해줘.
|
||||
타겟: 30-50대 여성, 이마 주름과 눈썹 처짐 고민"
|
||||
```
|
||||
|
||||
### Manuscript Editing
|
||||
```
|
||||
"원장님 초안 편집해줘. 앞광대리프팅 회복과정과 부작용 주제.
|
||||
원문 톤은 최대한 살려줘."
|
||||
```
|
||||
|
||||
### Educational Article
|
||||
```
|
||||
"안검하수 눈매교정에 대한 교육적 콘텐츠 작성.
|
||||
|
||||
@@ -0,0 +1,993 @@
|
||||
# Jamie Journal Editor Code Version — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build a self-contained Claude Code version of skill 46 (jamie-journal-editor) with a 4-module journal validator CLI tool.
|
||||
|
||||
**Architecture:** Single Python file (`journal_validator.py`) containing four validator classes (StructureValidator, ComplianceChecker, SpellingChecker, LinkRecommender) orchestrated by a main JournalValidator class. Self-contained CLAUDE.md directive with all brand voice and compliance rules inline.
|
||||
|
||||
**Tech Stack:** Python 3.8+ (stdlib only: `re`, `json`, `argparse`, `dataclasses`), optional `py-hanspell`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create `journal_validator.py` scaffold with CLI and report structure
|
||||
|
||||
**Files:**
|
||||
- Create: `custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py`
|
||||
|
||||
- [ ] **Step 1: Create the scripts directory**
|
||||
|
||||
```bash
|
||||
mkdir -p custom-skills/46-jamie-journal-editor/code/scripts
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the scaffold with CLI, report structure, and JournalValidator orchestrator**
|
||||
|
||||
Create `custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
Jamie Journal Editor - Journal Validator
|
||||
=========================================
|
||||
|
||||
Validates journal articles for Jamie Clinic's "정기호의 성형외과 진료실 이야기"
|
||||
(journal.jamie.clinic) against structure, compliance, spelling, and internal linking rules.
|
||||
|
||||
Usage:
|
||||
python journal_validator.py --input article.md
|
||||
python journal_validator.py --input article.md --check structure,compliance
|
||||
python journal_validator.py --input article.md --verbose --output report.json
|
||||
|
||||
Available checks: structure, compliance, spelling, links (default: all)
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""A single validation issue found in content."""
|
||||
checker: str # 'structure', 'compliance', 'spelling', 'links'
|
||||
issue_type: str
|
||||
severity: str # 'critical', 'high', 'medium', 'info'
|
||||
line_number: int # -1 if not applicable
|
||||
matched_text: str
|
||||
message: str
|
||||
suggestion: str
|
||||
|
||||
|
||||
class JournalValidator:
|
||||
"""Orchestrates all validation checks on a journal article."""
|
||||
|
||||
VALID_CHECKS = {'structure', 'compliance', 'spelling', 'links'}
|
||||
|
||||
def __init__(self, checks: Optional[List[str]] = None):
|
||||
self.checks = set(checks) if checks else self.VALID_CHECKS
|
||||
invalid = self.checks - self.VALID_CHECKS
|
||||
if invalid:
|
||||
raise ValueError(f"Unknown checks: {invalid}. Valid: {self.VALID_CHECKS}")
|
||||
|
||||
def validate(self, content: str) -> Dict:
|
||||
"""Run selected checks and return a combined report."""
|
||||
all_issues: List[ValidationIssue] = []
|
||||
|
||||
if 'structure' in self.checks:
|
||||
all_issues.extend(StructureValidator().check(content))
|
||||
if 'compliance' in self.checks:
|
||||
all_issues.extend(ComplianceChecker().check(content))
|
||||
if 'spelling' in self.checks:
|
||||
all_issues.extend(SpellingChecker().check(content))
|
||||
|
||||
link_recs = []
|
||||
if 'links' in self.checks:
|
||||
link_recs = LinkRecommender().check(content)
|
||||
all_issues.extend(link_recs)
|
||||
|
||||
severity_counts = {'critical': 0, 'high': 0, 'medium': 0, 'info': 0}
|
||||
for issue in all_issues:
|
||||
severity_counts[issue.severity] += 1
|
||||
|
||||
has_blocking = severity_counts['critical'] > 0 or severity_counts['high'] > 0
|
||||
|
||||
return {
|
||||
'is_valid': not has_blocking,
|
||||
'total_issues': len(all_issues),
|
||||
'issues_by_severity': severity_counts,
|
||||
'issues': [asdict(i) for i in all_issues],
|
||||
'summary': self._summary(severity_counts, has_blocking),
|
||||
}
|
||||
|
||||
def _summary(self, counts: Dict[str, int], has_blocking: bool) -> str:
|
||||
if counts['critical'] == 0 and counts['high'] == 0 and counts['medium'] == 0 and counts['info'] == 0:
|
||||
return "All checks passed. Article is ready for brand audit."
|
||||
parts = []
|
||||
if counts['critical']:
|
||||
parts.append(f"{counts['critical']} critical")
|
||||
if counts['high']:
|
||||
parts.append(f"{counts['high']} high")
|
||||
if counts['medium']:
|
||||
parts.append(f"{counts['medium']} medium")
|
||||
if counts['info']:
|
||||
parts.append(f"{counts['info']} info")
|
||||
status = "BLOCKED — fix critical/high issues" if has_blocking else "Passable — review medium/info items"
|
||||
return f"{status}. Issues: {', '.join(parts)}."
|
||||
|
||||
|
||||
# --- Validator stubs (implemented in subsequent tasks) ---
|
||||
|
||||
class StructureValidator:
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
return []
|
||||
|
||||
class ComplianceChecker:
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
return []
|
||||
|
||||
class SpellingChecker:
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
return []
|
||||
|
||||
class LinkRecommender:
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
return []
|
||||
|
||||
|
||||
# --- CLI ---
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description='Validate Jamie Clinic journal articles')
|
||||
parser.add_argument('--input', '-i', required=True, help='Input article file (markdown)')
|
||||
parser.add_argument('--check', '-c', default='all',
|
||||
help='Comma-separated checks: structure,compliance,spelling,links (default: all)')
|
||||
parser.add_argument('--output', '-o', help='Save JSON report to file')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Print detailed output')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
with open(args.input, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File not found: {args.input}")
|
||||
sys.exit(2)
|
||||
|
||||
checks = None if args.check == 'all' else [c.strip() for c in args.check.split(',')]
|
||||
|
||||
try:
|
||||
validator = JournalValidator(checks=checks)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(2)
|
||||
|
||||
report = validator.validate(content)
|
||||
|
||||
# Print summary
|
||||
print("Jamie Journal Validator")
|
||||
print("=" * 44)
|
||||
print(f"File: {args.input}")
|
||||
print(f"Valid: {'YES' if report['is_valid'] else 'NO'}")
|
||||
print(f"Total Issues: {report['total_issues']}")
|
||||
print(f" Critical: {report['issues_by_severity']['critical']}")
|
||||
print(f" High: {report['issues_by_severity']['high']}")
|
||||
print(f" Medium: {report['issues_by_severity']['medium']}")
|
||||
print(f" Info: {report['issues_by_severity']['info']}")
|
||||
print(f"\n{report['summary']}")
|
||||
|
||||
if args.verbose and report['issues']:
|
||||
print("\nIssues Found:")
|
||||
for issue in report['issues']:
|
||||
line_info = f"L{issue['line_number']}" if issue['line_number'] > 0 else "—"
|
||||
print(f"\n [{issue['severity'].upper()}] {issue['checker']}/{issue['issue_type']} ({line_info})")
|
||||
if issue['matched_text']:
|
||||
print(f" Text: '{issue['matched_text']}'")
|
||||
print(f" {issue['message']}")
|
||||
if issue['suggestion']:
|
||||
print(f" -> {issue['suggestion']}")
|
||||
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(report, f, ensure_ascii=False, indent=2)
|
||||
print(f"\nReport saved to: {args.output}")
|
||||
|
||||
sys.exit(0 if report['is_valid'] else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify scaffold runs**
|
||||
|
||||
```bash
|
||||
cd custom-skills/46-jamie-journal-editor
|
||||
echo "테스트 문서입니다." > /tmp/test_article.md
|
||||
python code/scripts/journal_validator.py --input /tmp/test_article.md
|
||||
```
|
||||
|
||||
Expected output: all zeros, "All checks passed."
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
|
||||
git commit -m "feat(jamie-journal): scaffold journal_validator.py with CLI and orchestrator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Implement StructureValidator
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py` (replace StructureValidator stub)
|
||||
|
||||
- [ ] **Step 1: Replace the StructureValidator stub with full implementation**
|
||||
|
||||
Replace the `class StructureValidator` stub with:
|
||||
|
||||
```python
|
||||
class StructureValidator:
|
||||
"""Validates journal article structure against the 5-step content pattern."""
|
||||
|
||||
STANDARD_OPENING = "안녕하세요. 제이미성형외과 정기호 원장입니다."
|
||||
|
||||
# Keywords that signal each of the 5 body steps
|
||||
STEP_KEYWORDS = {
|
||||
'problem': ['고민', '증상', '불편', '걱정', '힘드', '어려움'],
|
||||
'cause': ['원인', '발생', '때문에', '이유', '구조적', '해부학적'],
|
||||
'solution': ['시술', '수술', '방법', '제이미', '교정', '개선법'],
|
||||
'advantages': ['장점', '회복', '흉터', '통증', '마취', '보장', 'AS'],
|
||||
'results': ['결과', '효과', '기대', '개선', '자연스러', '만족'],
|
||||
}
|
||||
|
||||
STEP_NAMES_KR = {
|
||||
'problem': '문제 제기 (공감)',
|
||||
'cause': '원인 설명 (교육)',
|
||||
'solution': '해결책 제시 (제이미의 방법)',
|
||||
'advantages': '장점 나열 (차별점)',
|
||||
'results': '기대 효과 (비전)',
|
||||
}
|
||||
|
||||
MEDICAL_TERM_PATTERN = re.compile(
|
||||
r'[가-힣]+\([一-龥々〇]+,\s*[A-Za-z\s\-]+\)'
|
||||
)
|
||||
|
||||
CTA_PATTERNS = [
|
||||
re.compile(r'상담을?\s*(추천|받아|예약)'),
|
||||
re.compile(r'제이미성형외과.{0,20}(상담|문의)'),
|
||||
re.compile(r'편안한\s*마음으로\s*상담'),
|
||||
]
|
||||
|
||||
DISCLAIMER_FRAGMENTS = [
|
||||
'부작용',
|
||||
'출혈',
|
||||
'감염',
|
||||
'의료진과 상담',
|
||||
]
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
issues.extend(self._check_opening(content, lines))
|
||||
issues.extend(self._check_body_structure(content))
|
||||
issues.extend(self._check_medical_terms(content, lines))
|
||||
issues.extend(self._check_closing_cta(content, lines))
|
||||
issues.extend(self._check_disclaimer(content, lines))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_opening(self, content: str, lines: List[str]) -> List[ValidationIssue]:
|
||||
if self.STANDARD_OPENING not in content:
|
||||
return [ValidationIssue(
|
||||
checker='structure',
|
||||
issue_type='missing_opening',
|
||||
severity='high',
|
||||
line_number=1,
|
||||
matched_text='',
|
||||
message='표준 인사말이 누락되었습니다.',
|
||||
suggestion=f'글 시작에 "{self.STANDARD_OPENING}" 를 추가하세요.',
|
||||
)]
|
||||
return []
|
||||
|
||||
def _check_body_structure(self, content: str) -> List[ValidationIssue]:
|
||||
detected_steps = []
|
||||
missing_steps = []
|
||||
|
||||
for step, keywords in self.STEP_KEYWORDS.items():
|
||||
found = any(kw in content for kw in keywords)
|
||||
if found:
|
||||
detected_steps.append(step)
|
||||
else:
|
||||
missing_steps.append(step)
|
||||
|
||||
issues = []
|
||||
if len(detected_steps) < 4:
|
||||
missing_names = [self.STEP_NAMES_KR[s] for s in missing_steps]
|
||||
issues.append(ValidationIssue(
|
||||
checker='structure',
|
||||
issue_type='incomplete_body',
|
||||
severity='high',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message=f'5단계 본론 구조 중 {len(missing_steps)}개 단계가 감지되지 않았습니다: {", ".join(missing_names)}',
|
||||
suggestion='누락된 단계를 본문에 추가하세요. 각 단계는 소제목(**굵은 글씨**)이나 관련 키워드로 표현할 수 있습니다.',
|
||||
))
|
||||
elif missing_steps:
|
||||
missing_names = [self.STEP_NAMES_KR[s] for s in missing_steps]
|
||||
issues.append(ValidationIssue(
|
||||
checker='structure',
|
||||
issue_type='partial_body',
|
||||
severity='medium',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message=f'5단계 중 {len(detected_steps)}개 감지됨. 누락 가능: {", ".join(missing_names)}',
|
||||
suggestion='선택적이지만, 모든 5단계를 포함하면 더 완성도 높은 글이 됩니다.',
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_medical_terms(self, content: str, lines: List[str]) -> List[ValidationIssue]:
|
||||
# Check if article mentions medical terms that should use the Korean(漢字, English) format
|
||||
# Only flag if there are procedure-like terms without the format
|
||||
medical_nouns = ['안검하수', '내시경', '거상술', '눈매교정', '매몰법', '절개법']
|
||||
has_formal_term = bool(self.MEDICAL_TERM_PATTERN.search(content))
|
||||
has_medical_noun = any(noun in content for noun in medical_nouns)
|
||||
|
||||
if has_medical_noun and not has_formal_term:
|
||||
return [ValidationIssue(
|
||||
checker='structure',
|
||||
issue_type='medical_term_format',
|
||||
severity='medium',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message='의학 용어가 있지만 "한글(漢字, English)" 형식의 정식 소개가 없습니다.',
|
||||
suggestion='첫 등장 시 "안검하수(眼瞼下垂, Ptosis)는..." 형식으로 소개하세요.',
|
||||
)]
|
||||
return []
|
||||
|
||||
def _check_closing_cta(self, content: str, lines: List[str]) -> List[ValidationIssue]:
|
||||
# Check last 20% of content for CTA
|
||||
tail_start = int(len(content) * 0.8)
|
||||
tail = content[tail_start:]
|
||||
|
||||
has_cta = any(p.search(tail) for p in self.CTA_PATTERNS)
|
||||
if not has_cta:
|
||||
return [ValidationIssue(
|
||||
checker='structure',
|
||||
issue_type='missing_cta',
|
||||
severity='medium',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message='글 마무리에 상담 CTA가 감지되지 않았습니다.',
|
||||
suggestion='"[고민]이시라면 지금 바로 제이미성형외과의 [시술명] 상담을 추천드립니다." 패턴을 추가하세요.',
|
||||
)]
|
||||
return []
|
||||
|
||||
def _check_disclaimer(self, content: str, lines: List[str]) -> List[ValidationIssue]:
|
||||
# Check last 30% of content for disclaimer
|
||||
tail_start = int(len(content) * 0.7)
|
||||
tail = content[tail_start:]
|
||||
|
||||
has_disclaimer = sum(1 for frag in self.DISCLAIMER_FRAGMENTS if frag in tail) >= 2
|
||||
if not has_disclaimer:
|
||||
return [ValidationIssue(
|
||||
checker='structure',
|
||||
issue_type='missing_disclaimer',
|
||||
severity='high',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message='필수 고지문이 누락되었습니다.',
|
||||
suggestion='"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다." 를 글 하단에 추가하세요.',
|
||||
)]
|
||||
return []
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test with a minimal article**
|
||||
|
||||
Create `/tmp/test_journal.md`:
|
||||
|
||||
```markdown
|
||||
안녕하세요. 제이미성형외과 정기호 원장입니다.
|
||||
|
||||
오늘은 눈꺼풀 처짐으로 고민하시는 분들을 위한 안검하수 눈매교정에 대해 말씀드리겠습니다.
|
||||
|
||||
안검하수(眼瞼下垂, Ptosis)는 눈을 뜨는 근육의 힘이 약해져 눈꺼풀이 처지는 현상을 말합니다.
|
||||
|
||||
이러한 증상의 원인은 선천적 또는 후천적으로 발생할 수 있습니다.
|
||||
|
||||
제이미성형외과에서는 정밀 진단을 통해 가장 적합한 수술 방법을 찾아드립니다.
|
||||
|
||||
수술 후 회복 기간은 약 일주일 정도이며, 흉터가 거의 보이지 않는 것이 장점입니다.
|
||||
|
||||
자연스러운 눈매 개선 결과를 기대하실 수 있습니다.
|
||||
|
||||
눈매가 답답하시다면 지금 바로 제이미성형외과의 눈매교정 상담을 추천드립니다.
|
||||
|
||||
개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다.
|
||||
```
|
||||
|
||||
```bash
|
||||
python code/scripts/journal_validator.py --input /tmp/test_journal.md --verbose
|
||||
```
|
||||
|
||||
Expected: Valid = YES, 0 critical/high issues. May show info-level link recommendations.
|
||||
|
||||
- [ ] **Step 3: Test with a broken article (no opening, no disclaimer)**
|
||||
|
||||
Create `/tmp/test_bad.md`:
|
||||
|
||||
```markdown
|
||||
눈 성형에 대해 알아보겠습니다.
|
||||
|
||||
수술 방법은 다양합니다.
|
||||
```
|
||||
|
||||
```bash
|
||||
python code/scripts/journal_validator.py --input /tmp/test_bad.md --verbose
|
||||
```
|
||||
|
||||
Expected: Valid = NO, high issues for missing_opening, incomplete_body, missing_disclaimer.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
|
||||
git commit -m "feat(jamie-journal): implement StructureValidator (5-step body, opening, CTA, disclaimer)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Implement ComplianceChecker
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py` (replace ComplianceChecker stub)
|
||||
|
||||
- [ ] **Step 1: Replace the ComplianceChecker stub with full implementation**
|
||||
|
||||
Replace the `class ComplianceChecker` stub with:
|
||||
|
||||
```python
|
||||
class ComplianceChecker:
|
||||
"""Checks content for Korean medical advertising law violations (의료법 제56조)."""
|
||||
|
||||
PROHIBITED_PATTERNS = {
|
||||
'effect_guarantee': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'100[%%]\s*(?:만족|효과|성공)',
|
||||
r'반드시\s+(?:효과|개선|만족)',
|
||||
r'완벽한?\s+결과',
|
||||
r'보장합니다',
|
||||
r'확실한?\s+효과',
|
||||
r'틀림없이?\s+(?:효과|개선)',
|
||||
],
|
||||
'message': '효과를 보장하는 표현은 의료법 제56조 제2항 제2호 위반입니다.',
|
||||
'suggestion': '"개선에 도움을 줄 수 있습니다" 또는 "개인에 따라 결과가 다를 수 있습니다"로 변경하세요.',
|
||||
},
|
||||
'comparative_superiority': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'최고의?',
|
||||
r'(?:국내|강남|서울|압구정)\s*(?:최고|1위|최초)',
|
||||
r'1위',
|
||||
r'타\s*병원보다',
|
||||
r'다른\s*(?:병원|의원)보다\s*(?:우수|뛰어|좋)',
|
||||
r'업계\s*최초',
|
||||
r'독보적',
|
||||
],
|
||||
'message': '타 의료기관과의 비교 우위 주장은 의료법 제56조 제2항 제4호 위반입니다.',
|
||||
'suggestion': '"풍부한 경험을 보유한" 또는 "저희만의 방법으로"와 같은 객관적 표현으로 변경하세요.',
|
||||
},
|
||||
'safety_guarantee': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'부작용\s*(?:없|無|제로|zero)',
|
||||
r'(?:100[%%]|완전히?|절대)\s*안전',
|
||||
r'위험\s*(?:없|無)',
|
||||
r'흉터\s*(?:없|無|제로|zero)',
|
||||
r'무통(?:증)?(?:\s|$|,)',
|
||||
],
|
||||
'message': '안전성을 보장하는 표현은 의료법 제56조 제2항 제7호 위반입니다.',
|
||||
'suggestion': '부작용 가능성을 명시하고 "안전한 수술을 위해 최선을 다합니다"로 변경하세요.',
|
||||
},
|
||||
'patient_testimonial': {
|
||||
'severity': 'critical',
|
||||
'patterns': [
|
||||
r'(?:환자|고객)\s*[가-힣A-Z]+\s*(?:씨|님)의?\s*(?:후기|경험)',
|
||||
r'실제\s*(?:환자|고객)\s*(?:후기|리뷰|경험담)',
|
||||
r'[""]\s*(?:정말|너무|진짜)\s+(?:만족|좋아요|감사)',
|
||||
r'수술\s*후\s*\d+\s*(?:개월|주일|년)\s*(?:만족|경과)',
|
||||
],
|
||||
'message': '환자 치료경험담은 의료법 제56조 제2항 제2호 위반입니다.',
|
||||
'suggestion': '환자 후기를 제거하고, 일반적인 수술 과정 설명으로 대체하세요.',
|
||||
},
|
||||
'exaggeration': {
|
||||
'severity': 'high',
|
||||
'patterns': [
|
||||
r'(?:놀라운|대박|극적인)\s*(?:변화|효과|결과)',
|
||||
r'마법같은?',
|
||||
r'기적적인?',
|
||||
r'다시\s*태어나',
|
||||
r'완벽\s*변신',
|
||||
],
|
||||
'message': '과장된 표현은 의료법 제56조 제2항 제3호 위반 가능성이 있습니다.',
|
||||
'suggestion': '"자연스러운 개선", "점진적인 효과"와 같은 절제된 표현으로 변경하세요.',
|
||||
},
|
||||
'specialty_claim': {
|
||||
'severity': 'high',
|
||||
'patterns': [
|
||||
r'전문\s*병원',
|
||||
r'특화\s*(?:병원|클리닉|센터)',
|
||||
r'노하우',
|
||||
],
|
||||
'message': '전문병원 지정 없이 "전문", "특화" 표현 사용은 과장 광고에 해당합니다.',
|
||||
'suggestion': '"중점진료", "집중진료", "풍부한 경험"으로 변경하세요.',
|
||||
},
|
||||
}
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
# Check prohibited patterns
|
||||
for vtype, config in self.PROHIBITED_PATTERNS.items():
|
||||
for pattern in config['patterns']:
|
||||
for match in re.finditer(pattern, content, re.IGNORECASE):
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
issues.append(ValidationIssue(
|
||||
checker='compliance',
|
||||
issue_type=vtype,
|
||||
severity=config['severity'],
|
||||
line_number=line_num,
|
||||
matched_text=match.group(),
|
||||
message=config['message'],
|
||||
suggestion=config['suggestion'],
|
||||
))
|
||||
|
||||
# Check missing disclaimers for procedure content
|
||||
procedure_keywords = ['수술', '시술', '이마거상', '쌍꺼풀', '리프팅', '보톡스', '필러',
|
||||
'눈매교정', '안검하수', '지방이식', '절개']
|
||||
has_procedure = any(kw in content for kw in procedure_keywords)
|
||||
|
||||
if has_procedure:
|
||||
has_side_effect = any(t in content for t in ['부작용', '합병증', '붓기', '멍', '염증'])
|
||||
has_individual_var = '개인' in content and any(t in content for t in ['차이', '다를 수', '다릅니다'])
|
||||
|
||||
if not has_side_effect:
|
||||
issues.append(ValidationIssue(
|
||||
checker='compliance',
|
||||
issue_type='missing_side_effect_notice',
|
||||
severity='high',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message='부작용 가능성에 대한 고지가 누락되었습니다.',
|
||||
suggestion='"※ 모든 수술 및 시술은 개인에 따라 붓기, 멍, 염증 등의 부작용이 발생할 수 있습니다." 문구를 추가하세요.',
|
||||
))
|
||||
|
||||
if not has_individual_var:
|
||||
issues.append(ValidationIssue(
|
||||
checker='compliance',
|
||||
issue_type='missing_individual_variation',
|
||||
severity='medium',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message='개인차에 대한 고지가 누락되었습니다.',
|
||||
suggestion='"개인에 따라 결과가 다를 수 있습니다" 문구를 추가하세요.',
|
||||
))
|
||||
|
||||
return issues
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test compliance with a bad article**
|
||||
|
||||
Create `/tmp/test_compliance.md`:
|
||||
|
||||
```markdown
|
||||
안녕하세요. 제이미성형외과 정기호 원장입니다.
|
||||
|
||||
저희 병원은 강남 최고의 눈 성형 전문 병원입니다.
|
||||
100% 만족을 보장합니다. 부작용 없는 안전한 수술!
|
||||
|
||||
실제 환자 김OO님의 후기: "정말 만족합니다!"
|
||||
|
||||
놀라운 변화를 경험하세요. 마법같은 결과!
|
||||
```
|
||||
|
||||
```bash
|
||||
python code/scripts/journal_validator.py --input /tmp/test_compliance.md --check compliance --verbose
|
||||
```
|
||||
|
||||
Expected: Multiple critical violations (effect_guarantee, comparative_superiority, safety_guarantee, patient_testimonial, exaggeration, specialty_claim).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
|
||||
git commit -m "feat(jamie-journal): implement ComplianceChecker (의료법 제56조 pattern matching)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Implement SpellingChecker
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py` (replace SpellingChecker stub)
|
||||
|
||||
- [ ] **Step 1: Replace the SpellingChecker stub with full implementation**
|
||||
|
||||
Replace the `class SpellingChecker` stub with:
|
||||
|
||||
```python
|
||||
class SpellingChecker:
|
||||
"""Two-tier Korean spelling checker for medical/cosmetic content."""
|
||||
|
||||
# Tier 1: Built-in medical term misspelling dictionary
|
||||
# Format: { wrong_spelling: (correct_spelling, category) }
|
||||
MISSPELLINGS = {
|
||||
'쌍커풀': ('쌍꺼풀', '눈 성형'),
|
||||
'쌍거풀': ('쌍꺼풀', '눈 성형'),
|
||||
'안겸하수': ('안검하수', '눈 성형'),
|
||||
'안검하수증': ('안검하수', '눈 성형'),
|
||||
'눈꺼플': ('눈꺼풀', '눈 성형'),
|
||||
'눈커풀': ('눈꺼풀', '눈 성형'),
|
||||
'네시경': ('내시경', '이마 성형'),
|
||||
'거상 술': ('거상술', '이마 성형'),
|
||||
'앤도타인': ('엔도타인', '이마 성형'),
|
||||
'엔도타인': ('엔도타인', '이마 성형'), # correct - for reference only
|
||||
'봉합 사': ('봉합사', '이마 성형'),
|
||||
'스마스층': ('SMAS층', '동안 성형'),
|
||||
'히알루론 산': ('히알루론산', '필러'),
|
||||
'히알류론산': ('히알루론산', '필러'),
|
||||
'보톡스': ('보톡스', '정상'), # correct - skip
|
||||
'보톡스주': ('보톡스 주사', '보톡스'),
|
||||
'리프팅': ('리프팅', '정상'), # correct - skip
|
||||
'리프팅술': ('리프팅', '동안 성형'),
|
||||
'하이푸': ('하이푸', '정상'), # correct - skip
|
||||
'지방이식': ('지방이식', '정상'), # correct - skip
|
||||
'지방 이식': ('지방이식', '동안 성형'),
|
||||
'눈밑지방': ('눈밑 지방', '눈 성형'),
|
||||
'다크 서클': ('다크서클', '눈 성형'),
|
||||
'팔자주름': ('팔자 주름', '동안 성형'),
|
||||
'절개법': ('절개법', '정상'), # correct - skip
|
||||
'절게법': ('절개법', '눈 성형'),
|
||||
'매몰법': ('매몰법', '정상'), # correct - skip
|
||||
'매몰 법': ('매몰법', '눈 성형'),
|
||||
'눈썹밑절개': ('눈썹밑 절개', '이마 성형'),
|
||||
}
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
# Tier 1: Built-in dictionary check
|
||||
issues.extend(self._check_builtin(content, lines))
|
||||
|
||||
# Tier 2: py-hanspell (optional)
|
||||
issues.extend(self._check_hanspell(content, lines))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_builtin(self, content: str, lines: List[str]) -> List[ValidationIssue]:
|
||||
issues = []
|
||||
for wrong, (correct, category) in self.MISSPELLINGS.items():
|
||||
if category == '정상':
|
||||
continue # Skip entries marked as correct reference
|
||||
if wrong == correct:
|
||||
continue
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
if wrong in line:
|
||||
issues.append(ValidationIssue(
|
||||
checker='spelling',
|
||||
issue_type='medical_misspelling',
|
||||
severity='medium',
|
||||
line_number=i,
|
||||
matched_text=wrong,
|
||||
message=f'의료 용어 오타: "{wrong}" → "{correct}" ({category})',
|
||||
suggestion=f'"{wrong}"을(를) "{correct}"(으)로 수정하세요.',
|
||||
))
|
||||
return issues
|
||||
|
||||
def _check_hanspell(self, content: str, lines: List[str]) -> List[ValidationIssue]:
|
||||
try:
|
||||
from hanspell import spell_checker
|
||||
except ImportError:
|
||||
return [ValidationIssue(
|
||||
checker='spelling',
|
||||
issue_type='hanspell_unavailable',
|
||||
severity='info',
|
||||
line_number=-1,
|
||||
matched_text='',
|
||||
message='py-hanspell이 설치되지 않아 맞춤법 검사를 건너뜁니다.',
|
||||
suggestion='더 정밀한 맞춤법 검사를 원하시면: pip install py-hanspell',
|
||||
)]
|
||||
|
||||
issues = []
|
||||
# Check non-empty lines, skip code blocks and short lines
|
||||
in_code_block = False
|
||||
for i, line in enumerate(lines, 1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('```'):
|
||||
in_code_block = not in_code_block
|
||||
continue
|
||||
if in_code_block or len(stripped) < 5 or stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
try:
|
||||
result = spell_checker.check(stripped)
|
||||
if result.errors > 0:
|
||||
for word, correction in zip(result.words.keys(), result.words.values()):
|
||||
if word != correction:
|
||||
issues.append(ValidationIssue(
|
||||
checker='spelling',
|
||||
issue_type='hanspell_correction',
|
||||
severity='medium',
|
||||
line_number=i,
|
||||
matched_text=word,
|
||||
message=f'맞춤법 교정 제안: "{word}" → "{correction}"',
|
||||
suggestion=f'"{word}"을(를) "{correction}"(으)로 수정하세요.',
|
||||
))
|
||||
except Exception:
|
||||
# hanspell can fail on network issues; don't block validation
|
||||
pass
|
||||
|
||||
return issues
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test spelling checker with misspellings**
|
||||
|
||||
Create `/tmp/test_spelling.md`:
|
||||
|
||||
```markdown
|
||||
안녕하세요. 제이미성형외과 정기호 원장입니다.
|
||||
|
||||
쌍커풀 수술과 안겸하수 눈매교정에 대해 설명드리겠습니다.
|
||||
|
||||
네시경을 이용한 거상 술은 앤도타인 고정 방식을 사용합니다.
|
||||
|
||||
개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다.
|
||||
```
|
||||
|
||||
```bash
|
||||
python code/scripts/journal_validator.py --input /tmp/test_spelling.md --check spelling --verbose
|
||||
```
|
||||
|
||||
Expected: Medium issues for 쌍커풀→쌍꺼풀, 안겸하수→안검하수, 네시경→내시경, 거상 술→거상술, 앤도타인→엔도타인. Plus info about hanspell unavailable (unless installed).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
|
||||
git commit -m "feat(jamie-journal): implement SpellingChecker (built-in medical dict + optional hanspell)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Implement LinkRecommender
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py` (replace LinkRecommender stub)
|
||||
|
||||
- [ ] **Step 1: Replace the LinkRecommender stub with full implementation**
|
||||
|
||||
Replace the `class LinkRecommender` stub with:
|
||||
|
||||
```python
|
||||
class LinkRecommender:
|
||||
"""Scans for procedure mentions and suggests internal linking opportunities."""
|
||||
|
||||
# Procedure keyword groups: { topic_key: (display_name, [keywords]) }
|
||||
PROCEDURE_MAP = {
|
||||
'double_eyelid': ('쌍꺼풀 수술', ['쌍꺼풀', '매몰법', '절개법', '퀵매몰']),
|
||||
'ptosis': ('안검하수 눈매교정', ['안검하수', '눈매교정', '졸린 눈', '눈매 교정']),
|
||||
'under_eye_fat': ('눈밑 지방 재배치', ['눈밑 지방', '다크서클', '눈밑지방', '지방 재배치']),
|
||||
'dual_epicanthoplasty': ('듀얼 트임', ['듀얼 트임', '앞트임', '뒤트임', '눈꼬리']),
|
||||
'forehead_lift': ('내시경 이마거상술', ['이마거상', '이마리프팅', '이마 거상', '이마 리프팅']),
|
||||
'brow_lift': ('내시경 눈썹거상술', ['눈썹거상', '눈썹리프팅', '눈썹 거상', '눈썹 리프팅']),
|
||||
'sub_brow': ('눈썹밑 피부절개술', ['눈썹밑', '상안검', '눈꺼풀 처짐']),
|
||||
'smas_lifting': ('스마스 리프팅', ['스마스', 'SMAS', '안면거상', '페이스리프트']),
|
||||
'fat_graft': ('자가지방 이식', ['자가지방', '지방이식', '지방 이식', '지방 생착']),
|
||||
'hifu': ('하이푸 리프팅', ['하이푸', 'HIFU', '초음파 리프팅']),
|
||||
'cheek_lift': ('앞광대 리프팅', ['앞광대', '팔자 주름', '팔자주름', '광대 리프팅']),
|
||||
'endotine': ('엔도타인 고정', ['엔도타인', 'endotine', '3점 고정']),
|
||||
}
|
||||
|
||||
def check(self, content: str) -> List[ValidationIssue]:
|
||||
issues: List[ValidationIssue] = []
|
||||
lines = content.split('\n')
|
||||
|
||||
# Detect the article's main topic (first procedure mentioned in title/opening)
|
||||
main_topic = self._detect_main_topic(lines[:10])
|
||||
|
||||
# Scan for other procedure mentions
|
||||
for topic_key, (display_name, keywords) in self.PROCEDURE_MAP.items():
|
||||
if topic_key == main_topic:
|
||||
continue # Don't recommend linking to self
|
||||
|
||||
for keyword in keywords:
|
||||
for i, line in enumerate(lines, 1):
|
||||
if keyword in line:
|
||||
issues.append(ValidationIssue(
|
||||
checker='links',
|
||||
issue_type='link_opportunity',
|
||||
severity='info',
|
||||
line_number=i,
|
||||
matched_text=keyword,
|
||||
message=f'"{keyword}" 언급 발견 — "{display_name}" 글로의 내부 링크를 고려하세요.',
|
||||
suggestion=f'"{keyword}" 부분에 관련 저널 포스트({display_name}) 링크를 추가하면 독자 탐색에 도움이 됩니다.',
|
||||
))
|
||||
break # One recommendation per topic per keyword match
|
||||
|
||||
return issues
|
||||
|
||||
def _detect_main_topic(self, opening_lines: List[str]) -> Optional[str]:
|
||||
"""Detect the article's main procedure topic from opening lines."""
|
||||
opening_text = '\n'.join(opening_lines)
|
||||
for topic_key, (_, keywords) in self.PROCEDURE_MAP.items():
|
||||
if any(kw in opening_text for kw in keywords):
|
||||
return topic_key
|
||||
return None
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test link recommender**
|
||||
|
||||
Create `/tmp/test_links.md`:
|
||||
|
||||
```markdown
|
||||
안녕하세요. 제이미성형외과 정기호 원장입니다.
|
||||
|
||||
오늘은 안검하수 눈매교정에 대해 말씀드리겠습니다.
|
||||
|
||||
눈매교정과 함께 쌍꺼풀 수술을 병행하는 경우도 있습니다.
|
||||
|
||||
이마거상술과 함께 진행하면 더욱 시원한 눈매를 기대할 수 있습니다.
|
||||
|
||||
자가지방 이식을 통해 볼륨감을 보완하기도 합니다.
|
||||
|
||||
상담을 추천드립니다.
|
||||
|
||||
개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다.
|
||||
```
|
||||
|
||||
```bash
|
||||
python code/scripts/journal_validator.py --input /tmp/test_links.md --check links --verbose
|
||||
```
|
||||
|
||||
Expected: Info-level recommendations for 쌍꺼풀 (link to double eyelid article), 이마거상술 (link to forehead lift article), 자가지방 (link to fat graft article). Should NOT recommend linking 안검하수 since that's the main topic.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
|
||||
git commit -m "feat(jamie-journal): implement LinkRecommender (procedure cross-linking suggestions)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Rewrite `code/CLAUDE.md` as self-contained directive
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/46-jamie-journal-editor/code/CLAUDE.md`
|
||||
|
||||
- [ ] **Step 1: Rewrite CLAUDE.md with all brand voice, compliance, and structure rules inline**
|
||||
|
||||
Replace the entire contents of `custom-skills/46-jamie-journal-editor/code/CLAUDE.md` with the self-contained directive. This must NOT reference `desktop/references/`.
|
||||
|
||||
Content sections:
|
||||
1. Overview + Quick Start (validator CLI usage)
|
||||
2. Brand Voice — personality traits, honorifics, hospital naming, standard opening, CTA patterns
|
||||
3. Analogy Dictionary — Dr. Jung's signature metaphors
|
||||
4. Content Structure — 5-step body, opening/closing templates
|
||||
5. Procedure Copy Reference — eye, forehead, anti-aging key expressions
|
||||
6. Numeric Expressions & Medical Term Pattern
|
||||
7. Compliance Rules — prohibited expressions (table), required disclaimers
|
||||
8. Do's & Don'ts
|
||||
9. Image Placement — 5 positions
|
||||
10. Workflow — Topic → Draft → Validate → Brand Audit
|
||||
|
||||
Word budget: ~1,200 words (under 1,500-word limit).
|
||||
|
||||
Full content provided in implementation (too large for plan — this is a prose document, not code logic).
|
||||
|
||||
- [ ] **Step 2: Verify word count**
|
||||
|
||||
```bash
|
||||
wc -w custom-skills/46-jamie-journal-editor/code/CLAUDE.md
|
||||
```
|
||||
|
||||
Expected: Under 1,500 words.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/code/CLAUDE.md
|
||||
git commit -m "feat(jamie-journal): rewrite code/CLAUDE.md as self-contained directive"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Update README.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `custom-skills/46-jamie-journal-editor/README.md`
|
||||
|
||||
- [ ] **Step 1: Update the Structure section to reflect new code/scripts/ directory**
|
||||
|
||||
Update the structure tree in README.md to include:
|
||||
|
||||
```
|
||||
46-jamie-journal-editor/
|
||||
├── README.md
|
||||
├── code/
|
||||
│ ├── CLAUDE.md # Claude Code directive (self-contained)
|
||||
│ └── scripts/
|
||||
│ └── journal_validator.py # Article validator (structure, compliance, spelling, links)
|
||||
├── desktop/
|
||||
│ ├── SKILL.md # Claude Desktop skill directive
|
||||
│ ├── skill.yaml # Extended metadata
|
||||
│ ├── jamie-journal-editor.skill # Packaged skill (ZIP)
|
||||
│ └── references/
|
||||
│ ├── brand-voice.md
|
||||
│ ├── content-patterns.md
|
||||
│ └── medical-compliance.md
|
||||
```
|
||||
|
||||
Add a "Quick Start" section after the overview:
|
||||
|
||||
```markdown
|
||||
## Quick Start (Claude Code)
|
||||
|
||||
\`\`\`bash
|
||||
# Full validation
|
||||
python code/scripts/journal_validator.py --input article.md
|
||||
|
||||
# Specific checks
|
||||
python code/scripts/journal_validator.py --input article.md --check structure,compliance
|
||||
|
||||
# Verbose JSON report
|
||||
python code/scripts/journal_validator.py --input article.md --verbose --output report.json
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add custom-skills/46-jamie-journal-editor/README.md
|
||||
git commit -m "docs(jamie-journal): update README with code version structure and quick start"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Integration test and push
|
||||
|
||||
- [ ] **Step 1: Run full validation against the good test article**
|
||||
|
||||
```bash
|
||||
python custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input /tmp/test_journal.md --verbose --output /tmp/journal_report.json
|
||||
```
|
||||
|
||||
Expected: Valid = YES or only info-level issues.
|
||||
|
||||
- [ ] **Step 2: Run full validation against the bad compliance article**
|
||||
|
||||
```bash
|
||||
python custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input /tmp/test_compliance.md --verbose
|
||||
```
|
||||
|
||||
Expected: Valid = NO, multiple critical issues.
|
||||
|
||||
- [ ] **Step 3: Push to remote**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Clean up test files**
|
||||
|
||||
```bash
|
||||
rm -f /tmp/test_article.md /tmp/test_journal.md /tmp/test_bad.md /tmp/test_compliance.md /tmp/test_spelling.md /tmp/test_links.md /tmp/journal_report.json
|
||||
```
|
||||
@@ -0,0 +1,175 @@
|
||||
# Jamie Journal Editor (Skill 46) — Code Version Design
|
||||
|
||||
> **Date**: 2026-03-29
|
||||
> **Status**: Approved
|
||||
> **Scope**: Create self-contained Claude Code version for skill 46-jamie-journal-editor
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Build a complete `code/` version of skill 46 that is fully self-contained (no references to `desktop/`), following the dual-platform skill structure. Includes a `journal_validator.py` CLI tool for automated content validation.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
46-jamie-journal-editor/
|
||||
├── code/
|
||||
│ ├── CLAUDE.md # Self-contained directive
|
||||
│ └── scripts/
|
||||
│ └── journal_validator.py # Combined validator (4 modules)
|
||||
├── desktop/ # Unchanged
|
||||
└── README.md # Updated structure section
|
||||
```
|
||||
|
||||
## Component 1: `code/CLAUDE.md`
|
||||
|
||||
Self-contained Claude Code directive incorporating all content currently split across `desktop/SKILL.md` and `desktop/references/`. Sections:
|
||||
|
||||
1. **Overview** — Role definition, channel info, partner skills
|
||||
2. **Quick Start** — CLI usage for journal_validator.py
|
||||
3. **Brand Voice** — Dr. Jung's 5 personality traits, honorific guide, hospital references, standard opening
|
||||
4. **Analogy Dictionary** — Dr. Jung's signature metaphors (tree transplant, puppet strings, clean canvas, endotine button)
|
||||
5. **Content Structure** — 5-step body (Problem → Cause → Solution → Advantages → Results), opening template, closing template
|
||||
6. **Procedure Copy Reference** — Eye, forehead, anti-aging key expressions
|
||||
7. **Numeric Expression Guide** — Surgery time, recovery, AS period, survival rates
|
||||
8. **Medical Terminology Pattern** — Korean(漢字, English) format
|
||||
9. **Image Placement** — 5 recommended positions
|
||||
10. **Compliance Rules** — Prohibited expressions, required disclaimers, Do's & Don'ts
|
||||
11. **Workflow** — Topic → Draft → Validate → Brand Audit → Publish
|
||||
|
||||
Word budget: ~1,200 words (under the 1,500-word directive limit).
|
||||
|
||||
## Component 2: `journal_validator.py`
|
||||
|
||||
Single-file CLI tool with four validator classes.
|
||||
|
||||
### 2A: StructureValidator
|
||||
|
||||
Checks journal article structure against the 5-step content pattern.
|
||||
|
||||
| Check | Method | Pass Criteria |
|
||||
|-------|--------|---------------|
|
||||
| Opening format | Regex for standard greeting | "안녕하세요. 제이미성형외과 정기호 원장입니다." present |
|
||||
| 5-step body | Heading/keyword scan for each step | At least 4 of 5 steps detected |
|
||||
| Medical term format | Regex for `한글(漢字, English)` pattern | Terms introduced with proper format |
|
||||
| Closing CTA | Regex for CTA pattern | CTA pattern present in final section |
|
||||
| Required disclaimer | String match | Disclaimer text present at bottom |
|
||||
|
||||
Detection strategy for 5-step body: scan for heading markers (##, **bold**) or keyword clusters:
|
||||
- Problem: 고민, 증상, 불편, 걱정
|
||||
- Cause: 원인, 발생, 때문에, 이유
|
||||
- Solution: 시술, 수술, 방법, 제이미
|
||||
- Advantages: 장점, 회복, 흉터, 통증, 마취
|
||||
- Results: 결과, 효과, 기대, 개선
|
||||
|
||||
### 2B: ComplianceChecker
|
||||
|
||||
Adapted from skill 47's `compliance_checker.py`. Self-contained copy, not imported.
|
||||
|
||||
| Category | Example Patterns | Severity |
|
||||
|----------|-----------------|----------|
|
||||
| Effect guarantee | `100% 성공`, `반드시 효과`, `보장합니다` | Critical |
|
||||
| Comparative superiority | `최고`, `1위`, `타 병원보다` | Critical |
|
||||
| Safety guarantee | `부작용 없음`, `완전 안전` | Critical |
|
||||
| Patient testimonial | `실제 환자 후기`, quoted testimonials | Critical |
|
||||
| Exaggeration | `놀라운 변화`, `마법같은`, `기적적` | High |
|
||||
| Missing disclaimer | Side-effect notice absent | High |
|
||||
| Missing individual variation | "개인 차이" notice absent | Medium |
|
||||
|
||||
### 2C: SpellingChecker
|
||||
|
||||
Two-tier approach:
|
||||
|
||||
**Tier 1 (built-in, no dependencies):** Curated dictionary of common medical/cosmetic misspellings.
|
||||
|
||||
| Wrong | Correct | Category |
|
||||
|-------|---------|----------|
|
||||
| 쌍커풀 | 쌍꺼풀 | Eye surgery |
|
||||
| 안겸하수 | 안검하수 | Eye surgery |
|
||||
| 눈꺼플 | 눈꺼풀 | Eye surgery |
|
||||
| 보톡스 | 보톡스 | Correct (common) |
|
||||
| 히알루론산 | 히알루론산 | Correct (common) |
|
||||
| 거상술 vs 거상 술 | 거상술 | Forehead |
|
||||
| 내시경 vs 네시경 | 내시경 | Forehead |
|
||||
| 스마스 vs SMAS | Both OK | Anti-aging |
|
||||
| 엔도타인 vs 앤도타인 | 엔도타인 | Forehead |
|
||||
| 리프팅 vs 리프팅 | 리프팅 | Anti-aging |
|
||||
|
||||
Dictionary stored as a Python dict within the file (~30-50 entries).
|
||||
|
||||
**Tier 2 (optional, requires `py-hanspell`):** Broader Korean spelling/spacing check via Naver spell checker API wrapper. Gracefully degrades if library not installed — prints a note suggesting `pip install py-hanspell` for fuller coverage.
|
||||
|
||||
### 2D: LinkRecommender
|
||||
|
||||
Scans article text for procedure name mentions and suggests internal linking opportunities.
|
||||
|
||||
**Procedure keyword map** (built-in dict):
|
||||
|
||||
| Keywords | Suggested Link Topic |
|
||||
|----------|---------------------|
|
||||
| 쌍꺼풀, 매몰법, 절개법 | Double eyelid surgery |
|
||||
| 안검하수, 눈매교정 | Ptosis correction |
|
||||
| 눈밑 지방, 다크서클 | Under-eye fat repositioning |
|
||||
| 이마거상, 이마리프팅 | Endoscopic forehead lift |
|
||||
| 눈썹거상, 눈썹리프팅 | Endoscopic brow lift |
|
||||
| 눈썹밑, 상안검 | Sub-brow excision |
|
||||
| 스마스, SMAS, 안면거상 | SMAS lifting |
|
||||
| 자가지방, 지방이식 | Fat grafting |
|
||||
| 하이푸, HIFU | HIFU lifting |
|
||||
|
||||
Logic: If article topic is X and mentions procedure Y (where Y != X), suggest "Consider adding an internal link to your [Y] article where you mention '[matched text]'."
|
||||
|
||||
Output: List of recommendations with matched text and line number.
|
||||
|
||||
### CLI Interface
|
||||
|
||||
```bash
|
||||
# Full validation (all 4 checks)
|
||||
python code/scripts/journal_validator.py --input article.md
|
||||
|
||||
# Specific checks
|
||||
python code/scripts/journal_validator.py --input article.md --check structure,compliance
|
||||
python code/scripts/journal_validator.py --input article.md --check spelling,links
|
||||
|
||||
# JSON report output
|
||||
python code/scripts/journal_validator.py --input article.md --verbose --output report.json
|
||||
```
|
||||
|
||||
**Available `--check` values:** `structure`, `compliance`, `spelling`, `links` (comma-separated, default: all)
|
||||
|
||||
**Exit codes:** 0 = pass, 1 = critical/high violations found, 2 = input error
|
||||
|
||||
**Report structure:**
|
||||
```json
|
||||
{
|
||||
"is_valid": false,
|
||||
"total_issues": 5,
|
||||
"issues_by_severity": {"critical": 1, "high": 2, "medium": 1, "info": 1},
|
||||
"structure": { ... },
|
||||
"compliance": { ... },
|
||||
"spelling": { ... },
|
||||
"link_recommendations": [ ... ],
|
||||
"summary": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Component 3: `README.md` Update
|
||||
|
||||
Update the structure section to reflect the new `code/scripts/` directory. No other changes.
|
||||
|
||||
## What Stays Unchanged
|
||||
|
||||
- `desktop/SKILL.md` — no changes
|
||||
- `desktop/skill.yaml` — no changes
|
||||
- `desktop/references/*` — no changes
|
||||
- `desktop/jamie-journal-editor.skill` — not regenerated (Desktop packaging step)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Required:** Python 3.8+ (stdlib only: `re`, `json`, `argparse`, `dataclasses`)
|
||||
- **Optional:** `py-hanspell` for tier-2 spelling checks
|
||||
|
||||
## Testing
|
||||
|
||||
Manual validation: run the validator against a sample journal article draft to confirm all 4 checkers produce sensible output. No automated test suite needed for v1.
|
||||
Reference in New Issue
Block a user