feat(jamie-journal): implement ComplianceChecker (의료법 제56조 pattern matching)
This commit is contained in:
@@ -172,8 +172,136 @@ class StructureValidator:
|
|||||||
class ComplianceChecker:
|
class ComplianceChecker:
|
||||||
"""Checks brand compliance rules for Jamie Clinic journal content."""
|
"""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]:
|
def check(self, content: str) -> List[ValidationIssue]:
|
||||||
return []
|
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,5}(?:차이|다를 수|다릅니다)')
|
||||||
|
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:
|
class SpellingChecker:
|
||||||
|
|||||||
Reference in New Issue
Block a user