kt-event-marketing-fe/design/구현방안-AI이벤트설계.md
cherry2250 3f6e005026 초기 프로젝트 설정 및 설계 문서 추가
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 10:10:16 +09:00

1613 lines
45 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AI 기반 이벤트 추천 시스템 구현방안
**작성일**: 2025-10-21
**버전**: 1.0
**작성자**: 프로젝트 팀 전체
---
## 목차
1. [개요](#개요)
2. [데이터 확보 및 처리 방안](#데이터-확보-및-처리-방안)
3. [Claude API 연동 구조](#claude-api-연동-구조)
4. [시스템 아키텍처](#시스템-아키텍처)
5. [성능 최적화 전략](#성능-최적화-전략)
6. [구현 로드맵](#구현-로드맵)
---
## 개요
### 목적
소상공인이 이벤트 목적을 선택하면, AI가 업종/지역/시즌 트렌드를 분석하고 3가지 예산별 이벤트 기획안(각 온라인/오프라인 2개씩 총 6개)을 추천하는 시스템 구현
### 핵심 요구사항
- **응답 시간**: 10초 이내
- **추천 개수**: 6개 (저/중/고 예산 × 온라인/오프라인)
- **포함 정보**: 트렌드 분석, 이벤트 제목, 경품, 참여방법, 예상 참여자, 비용, 투자대비수익률
### 기술 스택 결정
- **AI 모델**: Claude 3.5 Sonnet API
- **벡터 DB**: Pinecone (관리형 서비스)
- **임베딩**: OpenAI text-embedding-3-large
- **캐싱**: Redis Cluster
- **백엔드**: Node.js (또는 Spring Boot)
- **메시지 큐**: RabbitMQ (비동기 처리)
---
## 데이터 확보 및 처리 방안
### 1. 데이터 소스
#### 1.1 외부 데이터 (초기 학습용)
**공공데이터**
- 소상공인진흥공단 API
- 업종별 사업체 수
- 지역별 매출 통계
- 시즌별 소비 트렌드
- 통계청 데이터
- 업종별 월별 매출액
- 지역별 소비자 특성
- 연령대별 소비 패턴
**SNS 및 블로그 데이터**
- 네이버 블로그 검색 API
- 키워드: "소상공인 이벤트", "매장 프로모션"
- 수집 항목: 이벤트명, 경품, 참여방법, 후기
- Instagram Graph API
- 해시태그: #소상공인이벤트, #가게이벤트
- 수집 항목: 이미지, 캡션, 좋아요/댓글 수
**경쟁사/벤치마크 데이터**
- 유사 서비스 공개 사례 분석
- 성공 사례 DB 구축 (100~500건)
#### 1.2 자사 데이터 (운영 데이터)
**사용자 프로필**
- 매장명, 업종, 주소, 영업시간
- 사업자번호 (업종 분류 용)
**이벤트 생성 데이터**
- 사용자가 생성한 이벤트 정보
- AI 추천 중 선택한 옵션
- 커스텀 수정 내용 (제목, 경품 변경)
**이벤트 성과 데이터**
- 참여자 수
- 실제 비용
- 실제 투자대비수익률
- 배포 채널별 성과
**사용자 피드백**
- "다시 추천받기" 클릭 (부정적 피드백)
- 이벤트 선택 (긍정적 피드백)
- 추천과 실제 성과 차이
### 2. 데이터 수집 프로세스
#### 2.1 초기 데이터 수집 (프로젝트 시작 시)
```python
# ETL 파이프라인 (Apache Airflow DAG)
# 일일 배치 작업
@dag(schedule_interval='0 2 * * *') # 매일 새벽 2시
def collect_external_data():
# Task 1: 공공데이터 수집
@task
def fetch_public_data():
# 소상공인진흥공단 API 호출
# 통계청 데이터 수집
return data
# Task 2: SNS 크롤링
@task
def crawl_sns_data():
# 네이버 블로그 검색
# Instagram 해시태그 검색
return data
# Task 3: 데이터 정제
@task
def clean_data(raw_data):
# 중복 제거
# 이상치 탐지 및 제거
# 업종/지역/시즌 태깅
# 텍스트 정규화
return cleaned_data
# Task 4: 데이터베이스 저장
@task
def save_to_db(cleaned_data):
# PostgreSQL에 저장
# events 테이블에 insert
pass
# Task 5: 벡터 임베딩 생성
@task
def generate_embeddings(cleaned_data):
# OpenAI Embeddings API 호출
# Pinecone에 저장
pass
```
#### 2.2 실시간 데이터 수집
```javascript
// 이벤트 생성 시
async function onEventCreated(eventData) {
// 1. 데이터베이스 저장
await db.events.create(eventData);
// 2. 벡터 임베딩 생성 (비동기)
await queue.publish('embedding', {
eventId: eventData.id,
text: `${eventData.title} ${eventData.prize} ${eventData.participation}`
});
}
// 이벤트 성과 수집
async function onEventCompleted(eventId, performanceData) {
// 성과 데이터 저장
await db.eventPerformance.create({
eventId,
actualParticipants: performanceData.participants,
actualCost: performanceData.cost,
actualRoi: performanceData.roi
});
// 추천 정확도 계산
const prediction = await db.eventRecommendations.findOne({ eventId });
const accuracy = calculateAccuracy(prediction, performanceData);
// 모델 성능 모니터링
await logAccuracy(accuracy);
}
```
### 3. 데이터 정제 프로세스
#### 3.1 데이터 정제 규칙
**텍스트 정규화**
```python
def normalize_event_data(raw_event):
return {
'title': clean_text(raw_event['title']), # 특수문자 제거, 소문자 변환
'prize': normalize_prize_name(raw_event['prize']), # 경품명 표준화
'participation': normalize_participation(raw_event['participation']),
'industry': classify_industry(raw_event['industry']), # 업종 분류
'location': parse_location(raw_event['location']), # 지역 파싱
'season': extract_season(raw_event['date']), # 시즌 추출
'cost': parse_cost(raw_event['cost']), # 비용 숫자 변환
'roi': parse_roi(raw_event['roi']) # 투자대비수익률 숫자 변환
}
```
**업종 분류 표준화**
```python
INDUSTRY_MAPPING = {
'음식점': ['한식', '중식', '일식', '양식', '카페', '베이커리', '치킨', '피자'],
'소매점': ['편의점', '슈퍼마켓', '화장품', '의류', '잡화'],
'서비스': ['미용실', '네일샵', 'PC방', '노래방', '헬스장'],
'숙박': ['모텔', '호텔', '게스트하우스', '펜션']
}
def classify_industry(raw_industry):
for category, subcategories in INDUSTRY_MAPPING.items():
if raw_industry in subcategories:
return category
return '기타'
```
**지역 파싱**
```python
def parse_location(address):
# "서울특별시 강남구 역삼동" -> {"city": "서울", "district": "강남구"}
import re
city_pattern = r'(서울|부산|대구|인천|광주|대전|울산|세종|경기|강원|충북|충남|전북|전남|경북|경남|제주)'
district_pattern = r'([가-힣]+구)'
city = re.search(city_pattern, address)
district = re.search(district_pattern, address)
return {
'city': city.group(1) if city else None,
'district': district.group(1) if district else None
}
```
**시즌 추출**
```python
def extract_season(date):
month = date.month
if month in [12, 1, 2]:
return '겨울'
elif month in [3, 4, 5]:
return '봄'
elif month in [6, 7, 8]:
return '여름'
else:
return '가을'
```
#### 3.2 이상치 탐지
```python
def detect_outliers(events_df):
# IQR 방식으로 이상치 탐지
Q1 = events_df['roi'].quantile(0.25)
Q3 = events_df['roi'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 이상치 제거
filtered_df = events_df[
(events_df['roi'] >= lower_bound) &
(events_df['roi'] <= upper_bound)
]
return filtered_df
```
### 4. 벡터라이징 전략
#### 4.1 임베딩 생성
```python
import openai
import pinecone
# OpenAI 임베딩 생성
def generate_embedding(text):
response = openai.embeddings.create(
model="text-embedding-3-large", # 3072 차원
input=text
)
return response.data[0].embedding
# 이벤트 데이터를 텍스트로 변환
def event_to_text(event):
return f"""
이벤트명: {event['title']}
업종: {event['industry']}
경품: {event['prize']}
참여방법: {event['participation']}
지역: {event['location']['city']} {event['location']['district']}
시즌: {event['season']}
예산: {event['cost']}
투자대비수익률: {event['roi']}%
"""
# Pinecone에 저장
def save_to_pinecone(event):
text = event_to_text(event)
embedding = generate_embedding(text)
pinecone_index.upsert(vectors=[{
'id': event['id'],
'values': embedding,
'metadata': {
'title': event['title'],
'industry': event['industry'],
'location': event['location']['district'],
'season': event['season'],
'budget': event['cost'],
'roi': event['roi'],
'prize': event['prize'],
'participation': event['participation']
}
}])
```
#### 4.2 벡터 검색
```python
# 유사 이벤트 검색
def search_similar_events(query_event, top_k=5):
# 쿼리 텍스트 생성
query_text = event_to_text(query_event)
# 쿼리 임베딩 생성
query_embedding = generate_embedding(query_text)
# 필터 조건 구성
filters = {
'industry': query_event['industry'],
'budget': {'$gte': query_event['cost'] * 0.5, '$lte': query_event['cost'] * 1.5}
}
# Pinecone 검색
results = pinecone_index.query(
vector=query_embedding,
filter=filters,
top_k=top_k,
include_metadata=True
)
return results['matches']
```
#### 4.3 Pinecone 인덱스 설정
```python
import pinecone
# Pinecone 초기화
pinecone.init(
api_key="YOUR_API_KEY",
environment="us-west1-gcp"
)
# 인덱스 생성
index_name = "kt-event-recommendations"
if index_name not in pinecone.list_indexes():
pinecone.create_index(
name=index_name,
dimension=3072, # text-embedding-3-large 차원
metric='cosine',
pods=1,
pod_type='p1.x1' # 성능 요구사항에 따라 조정
)
# 인덱스 연결
pinecone_index = pinecone.Index(index_name)
```
### 5. 데이터 통계 및 분포 분석
#### 5.1 초기 목표 데이터셋 규모
| 카테고리 | 목표 건수 | 수집 방법 |
|---------|----------|----------|
| 외부 데이터 (공공/SNS) | 300건 | 크롤링 + API |
| 벤치마크 사례 | 100건 | 수동 수집 |
| 자사 데이터 (초기) | 0건 | - |
| **총계** | **400건** | - |
#### 5.2 데이터 분포 목표
**업종별 분포**
- 음식점: 40%
- 소매점: 25%
- 서비스: 20%
- 숙박: 10%
- 기타: 5%
**예산별 분포**
- 저비용 (50만원 이하): 40%
- 중비용 (50~200만원): 35%
- 고비용 (200만원 이상): 25%
**지역별 분포**
- 서울: 30%
- 경기: 25%
- 부산/대구/인천: 20%
- 기타 지역: 25%
---
## Claude API 연동 구조
### 1. API 호출 전략
#### 1.1 단일 호출 + Structured Output 방식 (최종 선택)
**선택 이유**
- 응답 시간 단축 (10초 내 보장)
- API 비용 절감
- 트렌드와 추천의 일관성 유지
- Structured Output으로 JSON 파싱 안정성 향상
**호출 플로우**
```
1. 사용자 요청 → AI 서비스
2. 유사 이벤트 벡터 검색 (Pinecone)
3. 캐시 확인 (Redis)
4. Claude API 단일 호출 (트렌드 + 6개 추천)
5. 응답 파싱 및 검증
6. 캐시 저장
7. 프론트엔드 응답
```
### 2. JSON 요청/응답 구조
#### 2.1 Claude API 요청 구조
```json
{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 4096,
"temperature": 0.7,
"system": "당신은 소상공인을 위한 이벤트 마케팅 전문가입니다. 주어진 매장 정보와 이벤트 목적을 바탕으로 효과적인 이벤트 기획안을 제안합니다.",
"messages": [
{
"role": "user",
"content": "다음 정보를 바탕으로 이벤트를 추천해주세요:\n\n[매장 정보]\n- 업종: 음식점 (고깃집)\n- 지역: 서울 강남구\n- 이벤트 목적: 신규 고객 유치\n- 현재 시즌: 2025년 1월 (겨울)\n\n[참고 데이터]\n과거 유사한 매장에서 성공한 이벤트:\n1. SNS 팔로우 이벤트 - 참여자 200명, 비용 30만원, ROI 450%\n2. 리뷰 작성 이벤트 - 참여자 180명, 비용 120만원, ROI 380%\n...\n\n다음 형식으로 응답해주세요:\n1. 업종/지역/시즌 트렌드 분석\n2. 3가지 예산별 이벤트 추천 (각 온라인/오프라인 1개씩)"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "event_recommendations",
"strict": true,
"schema": {
"type": "object",
"properties": {
"trends": {
"type": "object",
"properties": {
"industry": {"type": "string"},
"location": {"type": "string"},
"season": {"type": "string"}
},
"required": ["industry", "location", "season"]
},
"recommendations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"budget": {"type": "string", "enum": ["low", "medium", "high"]},
"type": {"type": "string", "enum": ["online", "offline"]},
"title": {"type": "string"},
"prize": {"type": "string"},
"participation": {"type": "string"},
"expectedParticipants": {"type": "integer"},
"cost": {"type": "integer"},
"roi": {"type": "integer"}
},
"required": ["budget", "type", "title", "prize", "participation", "expectedParticipants", "cost", "roi"]
},
"minItems": 6,
"maxItems": 6
}
},
"required": ["trends", "recommendations"]
}
}
}
}
```
#### 2.2 Claude API 응답 구조
```json
{
"id": "msg_01...",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "{\"trends\":{\"industry\":\"음식점업 신년 프로모션 트렌드: 1월은 새해 신규 고객 유치를 위한 할인 이벤트와 SNS 바이럴 마케팅이 효과적입니다. 특히 고깃집은 단체 할인 및 재방문 쿠폰 제공이 인기입니다.\",\"location\":\"강남구는 직장인 및 MZ세대 고객이 많아 SNS 기반 이벤트와 점심 특가 이벤트가 효과적입니다. 특히 Instagram과 네이버 블로그를 활용한 바이럴 마케팅이 유리합니다.\",\"season\":\"겨울 시즌에는 따뜻한 실내 이벤트와 재방문 유도 프로모션이 효과적입니다. 설 연휴 대비 가족 단위 고객 타겟팅이 중요합니다.\"},\"recommendations\":[{\"budget\":\"low\",\"type\":\"online\",\"title\":\"SNS 팔로우 이벤트\",\"prize\":\"커피 쿠폰\",\"participation\":\"Instagram 팔로우 + 게시물 공유\",\"expectedParticipants\":180,\"cost\":250000,\"roi\":520},{\"budget\":\"low\",\"type\":\"offline\",\"title\":\"전화번호 등록 이벤트\",\"prize\":\"커피 쿠폰\",\"participation\":\"매장 방문 시 전화번호 등록\",\"expectedParticipants\":150,\"cost\":300000,\"roi\":450},{\"budget\":\"medium\",\"type\":\"online\",\"title\":\"리뷰 작성 이벤트\",\"prize\":\"5천원 상품권\",\"participation\":\"네이버 블로그 리뷰 작성\",\"expectedParticipants\":250,\"cost\":1500000,\"roi\":380},{\"budget\":\"medium\",\"type\":\"offline\",\"title\":\"방문 도장 적립 이벤트\",\"prize\":\"무료 식사권\",\"participation\":\"5회 방문 시 도장 적립\",\"expectedParticipants\":200,\"cost\":1800000,\"roi\":320},{\"budget\":\"high\",\"type\":\"online\",\"title\":\"인플루언서 협업 이벤트\",\"prize\":\"1만원 할인권\",\"participation\":\"인플루언서 게시물 좋아요 + 팔로우\",\"expectedParticipants\":400,\"cost\":5000000,\"roi\":280},{\"budget\":\"high\",\"type\":\"offline\",\"title\":\"VIP 고객 초대 이벤트\",\"prize\":\"특별 메뉴 제공\",\"participation\":\"VIP 초대장 발송\",\"expectedParticipants\":300,\"cost\":6000000,\"roi\":240}]}"
}
],
"model": "claude-3-5-sonnet-20241022",
"usage": {
"input_tokens": 1205,
"output_tokens": 856
}
}
```
#### 2.3 서비스 레이어 응답 구조 (프론트엔드로 전달)
```json
{
"success": true,
"data": {
"trends": {
"industry": "음식점업 신년 프로모션 트렌드: 1월은 새해 신규 고객 유치를 위한 할인 이벤트와 SNS 바이럴 마케팅이 효과적입니다...",
"location": "강남구는 직장인 및 MZ세대 고객이 많아 SNS 기반 이벤트와 점심 특가 이벤트가 효과적입니다...",
"season": "겨울 시즌에는 따뜻한 실내 이벤트와 재방문 유도 프로모션이 효과적입니다..."
},
"recommendations": [
{
"id": "low-online",
"budget": "low",
"type": "online",
"title": "SNS 팔로우 이벤트",
"prize": "커피 쿠폰",
"participation": "Instagram 팔로우 + 게시물 공유",
"expectedParticipants": 180,
"cost": 250000,
"roi": 520
},
{
"id": "low-offline",
"budget": "low",
"type": "offline",
"title": "전화번호 등록 이벤트",
"prize": "커피 쿠폰",
"participation": "매장 방문 시 전화번호 등록",
"expectedParticipants": 150,
"cost": 300000,
"roi": 450
},
{
"id": "medium-online",
"budget": "medium",
"type": "online",
"title": "리뷰 작성 이벤트",
"prize": "5천원 상품권",
"participation": "네이버 블로그 리뷰 작성",
"expectedParticipants": 250,
"cost": 1500000,
"roi": 380
},
{
"id": "medium-offline",
"budget": "medium",
"type": "offline",
"title": "방문 도장 적립 이벤트",
"prize": "무료 식사권",
"participation": "5회 방문 시 도장 적립",
"expectedParticipants": 200,
"cost": 1800000,
"roi": 320
},
{
"id": "high-online",
"budget": "high",
"type": "online",
"title": "인플루언서 협업 이벤트",
"prize": "1만원 할인권",
"participation": "인플루언서 게시물 좋아요 + 팔로우",
"expectedParticipants": 400,
"cost": 5000000,
"roi": 280
},
{
"id": "high-offline",
"budget": "high",
"type": "offline",
"title": "VIP 고객 초대 이벤트",
"prize": "특별 메뉴 제공",
"participation": "VIP 초대장 발송",
"expectedParticipants": 300,
"cost": 6000000,
"roi": 240
}
]
},
"metadata": {
"cacheHit": false,
"processingTime": 8.5,
"modelUsage": {
"inputTokens": 1205,
"outputTokens": 856
}
}
}
```
### 3. 프롬프트 엔지니어링
#### 3.1 System Prompt
```
당신은 소상공인을 위한 이벤트 마케팅 전문가입니다.
[역할]
- 매장 정보와 이벤트 목적을 바탕으로 효과적인 이벤트 기획안 제안
- 업종별, 지역별, 시즌별 트렌드 분석
- 예산별 차별화된 이벤트 추천
[제약사항]
- 추천은 반드시 6개 (저/중/고 예산 × 온라인/오프라인)
- 모든 추천은 실현 가능하고 구체적이어야 함
- 예상 참여자, 비용, 투자대비수익률은 과거 데이터 기반 현실적 수치
- 경품은 예산 범위 내에서 실현 가능한 것
[응답 형식]
- JSON 형식으로 응답
- trends: 업종/지역/시즌 트렌드 분석 (각 100자 내외)
- recommendations: 6개 이벤트 기획안 배열
```
#### 3.2 User Prompt 템플릿
```python
def build_user_prompt(store_info, event_purpose, similar_events):
return f"""
다음 정보를 바탕으로 이벤트를 추천해주세요:
[매장 정보]
- 업종: {store_info['industry']} ({store_info['businessType']})
- 지역: {store_info['location']['city']} {store_info['location']['district']}
- 이벤트 목적: {event_purpose}
- 현재 시즌: {get_current_season()}
[참고 데이터]
과거 유사한 매장에서 성공한 이벤트:
{format_similar_events(similar_events)}
[요구사항]
1. 업종/지역/시즌 트렌드 분석 (각 100자 내외)
2. 3가지 예산별 이벤트 추천:
- 저비용 (25~30만원): 온라인 1개, 오프라인 1개
- 중비용 (150~180만원): 온라인 1개, 오프라인 1개
- 고비용 (500~600만원): 온라인 1개, 오프라인 1개
각 추천에는 다음 정보를 포함:
- 이벤트 제목
- 경품명
- 참여 방법
- 예상 참여자 수
- 예상 비용 (원 단위)
- 예상 투자대비수익률 (%)
"""
def format_similar_events(events):
formatted = []
for i, event in enumerate(events, 1):
formatted.append(f"{i}. {event['title']} - 참여자 {event['participants']}명, 비용 {event['cost']:,}원, ROI {event['roi']}%")
return "\n".join(formatted)
```
#### 3.3 Few-shot 예제 (필요 시)
```python
FEW_SHOT_EXAMPLES = [
{
"input": {
"industry": "음식점",
"location": "서울 강남구",
"purpose": "신규 고객 유치",
"season": "겨울"
},
"output": {
"trends": {
"industry": "음식점업 신년 프로모션 트렌드...",
"location": "강남구는 직장인 및 MZ세대 고객이 많아...",
"season": "겨울 시즌에는 따뜻한 실내 이벤트..."
},
"recommendations": [...]
}
}
]
```
### 4. 백엔드 구현 (Node.js)
#### 4.1 AI 서비스 컨트롤러
```javascript
// controllers/aiController.js
const aiService = require('../services/aiService');
const cacheService = require('../services/cacheService');
exports.getEventRecommendations = async (req, res) => {
try {
const { eventPurpose, storeInfo } = req.body;
const userId = req.user.id;
// 1. 캐시 키 생성
const cacheKey = `recommendations:${storeInfo.industry}:${storeInfo.location.district}:${eventPurpose}`;
// 2. 캐시 확인
const cached = await cacheService.get(cacheKey);
if (cached) {
return res.json({
success: true,
data: cached,
metadata: { cacheHit: true }
});
}
// 3. AI 추천 생성
const startTime = Date.now();
const recommendations = await aiService.generateRecommendations({
eventPurpose,
storeInfo,
season: getCurrentSeason()
});
const processingTime = (Date.now() - startTime) / 1000;
// 4. 캐시 저장 (15분 TTL)
await cacheService.set(cacheKey, recommendations, 900);
// 5. 응답
res.json({
success: true,
data: recommendations,
metadata: {
cacheHit: false,
processingTime,
modelUsage: recommendations.usage
}
});
} catch (error) {
console.error('AI recommendation error:', error);
res.status(500).json({
success: false,
error: 'AI 추천 생성 중 오류가 발생했습니다.'
});
}
};
function getCurrentSeason() {
const month = new Date().getMonth() + 1;
if ([12, 1, 2].includes(month)) return '겨울';
if ([3, 4, 5].includes(month)) return '봄';
if ([6, 7, 8].includes(month)) return '여름';
return '가을';
}
```
#### 4.2 AI 서비스 레이어
```javascript
// services/aiService.js
const Anthropic = require('@anthropic-ai/sdk');
const pineconeService = require('./pineconeService');
const anthropic = new Anthropic({
apiKey: process.env.CLAUDE_API_KEY
});
exports.generateRecommendations = async ({ eventPurpose, storeInfo, season }) => {
try {
// 1. 유사 이벤트 검색
const similarEvents = await pineconeService.searchSimilarEvents({
industry: storeInfo.industry,
location: storeInfo.location.district,
season
});
// 2. 프롬프트 생성
const userPrompt = buildUserPrompt(storeInfo, eventPurpose, season, similarEvents);
// 3. Claude API 호출
const message = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4096,
temperature: 0.7,
system: getSystemPrompt(),
messages: [{ role: 'user', content: userPrompt }],
response_format: {
type: 'json_schema',
json_schema: getResponseSchema()
}
});
// 4. 응답 파싱
const content = message.content[0].text;
const result = JSON.parse(content);
// 5. ID 추가 및 검증
result.recommendations = result.recommendations.map((rec, idx) => ({
id: `${rec.budget}-${rec.type}`,
...rec
}));
// 6. 검증
validateRecommendations(result);
return {
...result,
usage: message.usage
};
} catch (error) {
console.error('Claude API error:', error);
// Fallback: 기본 추천 반환
return getFallbackRecommendations(storeInfo);
}
};
function buildUserPrompt(storeInfo, eventPurpose, season, similarEvents) {
const purposeMap = {
'신규고객유치': '신규 고객 유치',
'재방문유도': '재방문 유도',
'매출증대': '매출 증대',
'인지도향상': '인지도 향상'
};
return `
다음 정보를 바탕으로 이벤트를 추천해주세요:
[매장 정보]
- 업종: ${storeInfo.industry} (${storeInfo.businessType})
- 지역: ${storeInfo.location.city} ${storeInfo.location.district}
- 이벤트 목적: ${purposeMap[eventPurpose]}
- 현재 시즌: ${season}
[참고 데이터]
과거 유사한 매장에서 성공한 이벤트:
${formatSimilarEvents(similarEvents)}
[요구사항]
1. 업종/지역/시즌 트렌드 분석 (각 100자 내외)
2. 3가지 예산별 이벤트 추천:
- 저비용 (25~30만원): 온라인 1개, 오프라인 1개
- 중비용 (150~180만원): 온라인 1개, 오프라인 1개
- 고비용 (500~600만원): 온라인 1개, 오프라인 1개
각 추천에는 다음 정보를 포함:
- 이벤트 제목 (30자 이내)
- 경품명 (20자 이내)
- 참여 방법 (간결하게)
- 예상 참여자 수 (정수)
- 예상 비용 (원 단위, 정수)
- 예상 투자대비수익률 (%, 정수)
`.trim();
}
function formatSimilarEvents(events) {
return events.map((e, i) =>
`${i + 1}. ${e.metadata.title} - 참여자 ${e.metadata.participants}명, 비용 ${e.metadata.cost.toLocaleString()}원, ROI ${e.metadata.roi}%`
).join('\n');
}
function getSystemPrompt() {
return `당신은 소상공인을 위한 이벤트 마케팅 전문가입니다.
[역할]
- 매장 정보와 이벤트 목적을 바탕으로 효과적인 이벤트 기획안 제안
- 업종별, 지역별, 시즌별 트렌드 분석
- 예산별 차별화된 이벤트 추천
[제약사항]
- 추천은 반드시 6개 (저/중/고 예산 × 온라인/오프라인)
- 모든 추천은 실현 가능하고 구체적이어야 함
- 예상 참여자, 비용, 투자대비수익률은 과거 데이터 기반 현실적 수치
- 경품은 예산 범위 내에서 실현 가능한 것
- 투자대비수익률은 저예산일수록 높고, 고예산일수록 낮게 설정`;
}
function getResponseSchema() {
return {
name: 'event_recommendations',
strict: true,
schema: {
type: 'object',
properties: {
trends: {
type: 'object',
properties: {
industry: { type: 'string' },
location: { type: 'string' },
season: { type: 'string' }
},
required: ['industry', 'location', 'season']
},
recommendations: {
type: 'array',
items: {
type: 'object',
properties: {
budget: { type: 'string', enum: ['low', 'medium', 'high'] },
type: { type: 'string', enum: ['online', 'offline'] },
title: { type: 'string' },
prize: { type: 'string' },
participation: { type: 'string' },
expectedParticipants: { type: 'integer' },
cost: { type: 'integer' },
roi: { type: 'integer' }
},
required: ['budget', 'type', 'title', 'prize', 'participation', 'expectedParticipants', 'cost', 'roi']
},
minItems: 6,
maxItems: 6
}
},
required: ['trends', 'recommendations']
}
};
}
function validateRecommendations(result) {
// 6개 추천 검증
if (result.recommendations.length !== 6) {
throw new Error('추천 개수가 6개가 아닙니다');
}
// 예산별, 타입별 개수 검증
const counts = {};
result.recommendations.forEach(rec => {
const key = `${rec.budget}-${rec.type}`;
counts[key] = (counts[key] || 0) + 1;
});
const expected = {
'low-online': 1,
'low-offline': 1,
'medium-online': 1,
'medium-offline': 1,
'high-online': 1,
'high-offline': 1
};
for (const [key, count] of Object.entries(expected)) {
if (counts[key] !== count) {
throw new Error(`${key} 추천 개수가 올바르지 않습니다`);
}
}
}
function getFallbackRecommendations(storeInfo) {
// API 실패 시 기본 추천 반환
return {
trends: {
industry: `${storeInfo.industry} 업종의 일반적인 트렌드입니다.`,
location: `${storeInfo.location.district} 지역의 일반적인 특성입니다.`,
season: '계절별 일반적인 이벤트 특성입니다.'
},
recommendations: [
{
id: 'low-online',
budget: 'low',
type: 'online',
title: 'SNS 팔로우 이벤트',
prize: '커피 쿠폰',
participation: 'SNS 팔로우',
expectedParticipants: 150,
cost: 250000,
roi: 500
},
// ... 나머지 5개
]
};
}
```
#### 4.3 Pinecone 서비스
```javascript
// services/pineconeService.js
const { PineconeClient } = require('@pinecone-database/pinecone');
const openai = require('openai');
const pinecone = new PineconeClient();
await pinecone.init({
apiKey: process.env.PINECONE_API_KEY,
environment: process.env.PINECONE_ENV
});
const index = pinecone.Index('kt-event-recommendations');
exports.searchSimilarEvents = async ({ industry, location, season }) => {
try {
// 1. 쿼리 텍스트 생성
const queryText = `업종: ${industry}, 지역: ${location}, 시즌: ${season}`;
// 2. 임베딩 생성
const embedding = await generateEmbedding(queryText);
// 3. 필터 조건
const filter = {
industry: { $eq: industry }
};
// 4. 벡터 검색
const results = await index.query({
vector: embedding,
filter,
topK: 5,
includeMetadata: true
});
return results.matches;
} catch (error) {
console.error('Pinecone search error:', error);
return [];
}
};
async function generateEmbedding(text) {
const response = await openai.embeddings.create({
model: 'text-embedding-3-large',
input: text
});
return response.data[0].embedding;
}
```
### 5. 타임아웃 및 에러 처리
```javascript
// utils/timeout.js
exports.withTimeout = (promise, timeoutMs) => {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
)
]);
};
// 사용 예시
const recommendations = await withTimeout(
aiService.generateRecommendations(params),
10000 // 10초
);
```
---
## 시스템 아키텍처
### 1. 전체 아키텍처 다이어그램
```
┌─────────────────┐
│ Frontend │
│ (React) │
└────────┬────────┘
│ HTTP/REST
┌─────────────────────────────────────────────────────┐
│ API Gateway (Express) │
└────────┬────────────────────────────────────────────┘
├─────────────────┬──────────────────┬─────────────┐
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐
│User Service │ │Event Service │ │AI Service │ │Analytics │
└──────────────┘ └──────────────┘ └──────┬───────┘ └──────────┘
┌─────────────────────────┼────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────────┐ ┌──────────┐
│Redis Cache │ │Pinecone │ │Claude API│
│- Trends (1h) │ │Vector DB │ └──────────┘
│- Similar (30m) │ │- Event Embeddings│
│- Results (15m) │ └──────────────────┘
└────────────────┘
┌────────────────┐
│PostgreSQL │
│- Users │
│- Events │
│- Performance │
└────────────────┘
```
### 2. AI 서비스 상세 플로우
```
사용자 요청
[1] 요청 수신
[2] 캐시 확인 (Redis)
├─ Hit → 즉시 응답 (< 100ms)
└─ Miss
[3] 유사 이벤트 검색 (Pinecone)
│ - 쿼리 임베딩 생성 (OpenAI API)
│ - 벡터 검색 (코사인 유사도)
│ - Top 5 반환
[4] 프롬프트 생성
│ - 매장 정보 + 이벤트 목적
│ - 유사 이벤트 컨텍스트
│ - Few-shot 예제
[5] Claude API 호출
│ - Structured Output
│ - 타임아웃: 8초
[6] 응답 파싱 및 검증
│ - JSON 파싱
│ - 6개 추천 검증
│ - 예산/타입 검증
[7] 캐시 저장 (Redis)
│ - TTL: 15분
[8] 프론트엔드 응답
│ - 총 소요시간: < 10초
```
### 3. 데이터 파이프라인
```
[일일 배치 작업] (Airflow)
├─ [외부 데이터 수집]
│ ├─ 공공데이터 API
│ ├─ SNS 크롤링
│ └─ 벤치마크 사례
[데이터 정제]
│ - 중복 제거
│ - 이상치 탐지
│ - 태깅
[PostgreSQL 저장]
[벡터 임베딩 생성]
│ - OpenAI API
│ - 배치 처리
[Pinecone 저장]
[실시간 이벤트 생성]
[PostgreSQL 저장]
[비동기 큐 (RabbitMQ)]
[벡터 임베딩 생성]
[Pinecone 업데이트]
```
---
## 성능 최적화 전략
### 1. Redis 캐싱 전략
#### 1.1 3단계 캐싱
```javascript
// services/cacheService.js
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// 레벨 1: 트렌드 분석 캐싱 (1시간 TTL)
exports.getTrendCache = async (industry, location, season) => {
const key = `trend:${industry}:${location}:${season}`;
const cached = await client.get(key);
return cached ? JSON.parse(cached) : null;
};
exports.setTrendCache = async (industry, location, season, data) => {
const key = `trend:${industry}:${location}:${season}`;
await client.setex(key, 3600, JSON.stringify(data));
};
// 레벨 2: 유사 이벤트 검색 캐싱 (30분 TTL)
exports.getSimilarEventsCache = async (industry, location, season) => {
const key = `similar:${industry}:${location}:${season}`;
const cached = await client.get(key);
return cached ? JSON.parse(cached) : null;
};
exports.setSimilarEventsCache = async (industry, location, season, data) => {
const key = `similar:${industry}:${location}:${season}`;
await client.setex(key, 1800, JSON.stringify(data));
};
// 레벨 3: 전체 추천 결과 캐싱 (15분 TTL)
exports.getRecommendationCache = async (industry, location, purpose) => {
const key = `recommendations:${industry}:${location}:${purpose}`;
const cached = await client.get(key);
return cached ? JSON.parse(cached) : null;
};
exports.setRecommendationCache = async (industry, location, purpose, data) => {
const key = `recommendations:${industry}:${location}:${purpose}`;
await client.setex(key, 900, JSON.stringify(data));
};
```
#### 1.2 캐시 무효화 전략
```javascript
// 새로운 이벤트 성과 데이터 수집 시 관련 캐시 무효화
exports.invalidateRelatedCache = async (eventData) => {
const patterns = [
`trend:${eventData.industry}:*`,
`similar:${eventData.industry}:*`,
`recommendations:${eventData.industry}:*`
];
for (const pattern of patterns) {
const keys = await client.keys(pattern);
if (keys.length > 0) {
await client.del(...keys);
}
}
};
```
### 2. 병렬 처리 최적화
```javascript
// services/aiService.js
exports.generateRecommendations = async ({ eventPurpose, storeInfo, season }) => {
// 병렬 실행
const [similarEvents, trendCache] = await Promise.all([
// 유사 이벤트 검색
pineconeService.searchSimilarEvents({
industry: storeInfo.industry,
location: storeInfo.location.district,
season
}),
// 트렌드 캐시 확인
cacheService.getTrendCache(
storeInfo.industry,
storeInfo.location.district,
season
)
]);
// ... 나머지 로직
};
```
### 3. Rate Limiting 대응
```javascript
// utils/rateLimiter.js
const rateLimit = require('express-rate-limit');
// Claude API Rate Limit 대응
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 50, // 분당 최대 50건 (Claude API 제한에 맞춤)
message: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
handler: async (req, res) => {
// Rate limit 초과 시 캐시된 기본 추천 반환
const fallback = await getFallbackRecommendations(req.body.storeInfo);
res.status(200).json({
success: true,
data: fallback,
metadata: {
rateLimited: true,
message: '일시적으로 기본 추천을 제공합니다'
}
});
}
});
module.exports = apiLimiter;
```
### 4. 벡터 검색 최적화
```python
# Pinecone 인덱스 파티셔닝
# 업종별로 네임스페이스 분리하여 검색 성능 향상
def save_to_pinecone_with_namespace(event):
industry_namespace = event['industry']
pinecone_index.upsert(
vectors=[{
'id': event['id'],
'values': embedding,
'metadata': {...}
}],
namespace=industry_namespace # 업종별 네임스페이스
)
def search_with_namespace(query_event):
industry_namespace = query_event['industry']
results = pinecone_index.query(
vector=query_embedding,
namespace=industry_namespace, # 동일 업종 내에서만 검색
top_k=5
)
return results
```
### 5. 응답 시간 모니터링
```javascript
// middleware/performanceMonitor.js
exports.trackPerformance = async (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
// 10초 초과 시 경고
if (duration > 10000) {
console.warn(`Slow request: ${req.path} took ${duration}ms`);
// 모니터링 시스템에 전송 (예: Datadog, New Relic)
sendMetric('ai.recommendation.slow', {
path: req.path,
duration,
industry: req.body.storeInfo?.industry
});
}
// 평균 응답 시간 추적
sendMetric('ai.recommendation.duration', duration);
});
next();
};
```
---
## 구현 로드맵
### Phase 1: 기본 인프라 구축 (2주)
**Week 1**
- [ ] Pinecone 계정 생성 및 인덱스 설정
- [ ] Redis 클러스터 구축
- [ ] PostgreSQL 스키마 설계
- [ ] Claude API 키 발급 및 테스트
**Week 2**
- [ ] 외부 데이터 수집 스크립트 개발
- [ ] ETL 파이프라인 (Airflow DAG) 구축
- [ ] 데이터 정제 로직 구현
- [ ] 초기 데이터셋 구축 (300건)
### Phase 2: AI 서비스 개발 (3주)
**Week 3**
- [ ] Claude API 연동 기본 구조
- [ ] 프롬프트 템플릿 개발
- [ ] Structured Output 스키마 설계
- [ ] 응답 파싱 및 검증 로직
**Week 4**
- [ ] 벡터 임베딩 생성 로직
- [ ] Pinecone 연동 (저장/검색)
- [ ] 유사 이벤트 검색 알고리즘
- [ ] 캐싱 레이어 구현
**Week 5**
- [ ] AI 서비스 통합 테스트
- [ ] 성능 최적화 (병렬 처리)
- [ ] 에러 처리 및 Fallback 구현
- [ ] 응답 시간 모니터링
### Phase 3: 프론트엔드 연동 (1주)
**Week 6**
- [ ] API 엔드포인트 연동
- [ ] 로딩 상태 UI 구현
- [ ] 에러 핸들링 UI
- [ ] E2E 테스트
### Phase 4: 운영 및 개선 (지속적)
**Week 7+**
- [ ] 실사용 데이터 수집 시작
- [ ] 추천 정확도 모니터링
- [ ] 프롬프트 최적화
- [ ] A/B 테스트 진행
- [ ] 사용자 피드백 반영
---
## 비용 예측
### 1. Claude API 비용
**가정**
- 월 사용자 수: 1,000명
- 사용자당 월 평균 이벤트 생성: 3회
- 월 총 API 호출: 3,000회
**토큰 사용량 (예상)**
- Input: 1,200 tokens/호출
- Output: 800 tokens/호출
**비용 계산** (Claude 3.5 Sonnet 기준)
- Input: $3 / 1M tokens = $0.003 / 1K tokens
- Output: $15 / 1M tokens = $0.015 / 1K tokens
```
월 비용 = (1,200 * 0.003 + 800 * 0.015) * 3,000
= (3.6 + 12) * 3,000
= 15.6 * 3,000
= $46,800 / 월
```
### 2. OpenAI Embeddings API 비용
**가정**
- 일일 외부 데이터 수집: 10건
- 월 수집: 300건
- 사용자 이벤트 생성: 3,000건/월
**비용 계산** (text-embedding-3-large 기준)
- $0.13 / 1M tokens
```
월 비용 = (300 + 3,000) * 100 tokens * 0.13 / 1,000,000
= 3,300 * 100 * 0.00000013
= $0.04 / 월
```
### 3. Pinecone 비용
**가정**
- 벡터 차원: 3,072
- 저장 벡터 수: 10,000건
- 월 쿼리 수: 3,000회
**비용 계산** (Starter Plan)
- $70 / 월 (100,000 벡터 포함)
### 4. Redis 비용
**가정**
- AWS ElastiCache (cache.t3.micro)
**비용 계산**
- $0.017 / 시간 = $12.24 / 월
### 5. 총 비용
| 항목 | 월 비용 |
|------|--------|
| Claude API | $46,800 |
| OpenAI Embeddings | $0.04 |
| Pinecone | $70 |
| Redis | $12.24 |
| **총계** | **$46,882** |
**비용 절감 방안**
1. 캐싱 활용률 향상 (목표: 50% 캐시 히트율)
- Claude API 비용 50% 절감 → $23,400
2. 배치 처리 최적화
3. 사용량 기반 동적 스케일링
---
## 모니터링 및 개선
### 1. 핵심 지표
**성능 지표**
- 평균 응답 시간: < 10초
- 캐시 히트율: > 50%
- API 성공률: > 99%
**품질 지표**
- 추천 선택률: > 70% (사용자가 6개 중 1개 이상 선택)
- "다시 추천받기" 비율: < 30%
- 예측 정확도: 실제 ROI와 예측 ROI 차이 < 20%
### 2. 개선 사이클
```
[데이터 수집]
[정확도 분석]
│ - 예측 vs 실제 비교
│ - 업종별/지역별 분석
[프롬프트 최적화]
│ - Few-shot 예제 개선
│ - 시스템 프롬프트 조정
[A/B 테스트]
│ - 새 버전 vs 기존 버전
│ - 승자 선택
[배포 및 모니터링]
```
---
## 부록
### A. 데이터 스키마
#### PostgreSQL 테이블 구조
```sql
-- 이벤트 테이블
CREATE TABLE events (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title VARCHAR(100),
prize VARCHAR(50),
participation VARCHAR(200),
industry VARCHAR(50),
location_city VARCHAR(50),
location_district VARCHAR(50),
season VARCHAR(10),
cost INTEGER,
expected_roi INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);
-- 이벤트 성과 테이블
CREATE TABLE event_performance (
id SERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
actual_participants INTEGER,
actual_cost INTEGER,
actual_roi INTEGER,
completed_at TIMESTAMP DEFAULT NOW()
);
-- AI 추천 이력 테이블
CREATE TABLE ai_recommendations (
id SERIAL PRIMARY KEY,
event_id INTEGER REFERENCES events(id),
recommendation_data JSONB, -- Claude API 응답 전체
selected_option VARCHAR(20), -- 예: "low-online"
created_at TIMESTAMP DEFAULT NOW()
);
-- 인덱스
CREATE INDEX idx_events_industry ON events(industry);
CREATE INDEX idx_events_location ON events(location_district);
CREATE INDEX idx_events_season ON events(season);
CREATE INDEX idx_performance_event ON event_performance(event_id);
```
### B. 환경 변수 설정
```bash
# .env
# Claude API
CLAUDE_API_KEY=sk-ant-xxx
# OpenAI
OPENAI_API_KEY=sk-xxx
# Pinecone
PINECONE_API_KEY=xxx
PINECONE_ENV=us-west1-gcp
PINECONE_INDEX=kt-event-recommendations
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=xxx
# PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_NAME=kt_events
DB_USER=postgres
DB_PASSWORD=xxx
```
### C. 참고 자료
**Claude API**
- [Anthropic Documentation](https://docs.anthropic.com/)
- [Structured Outputs Guide](https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs)
**Pinecone**
- [Pinecone Documentation](https://docs.pinecone.io/)
- [Vector Search Best Practices](https://www.pinecone.io/learn/vector-search/)
**OpenAI Embeddings**
- [OpenAI Embeddings Guide](https://platform.openai.com/docs/guides/embeddings)
---
**문서 끝**