45 KiB
45 KiB
AI 기반 이벤트 추천 시스템 구현방안
작성일: 2025-10-21 버전: 1.0 작성자: 프로젝트 팀 전체
목차
개요
목적
소상공인이 이벤트 목적을 선택하면, 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 초기 데이터 수집 (프로젝트 시작 시)
# 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 실시간 데이터 수집
// 이벤트 생성 시
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 데이터 정제 규칙
텍스트 정규화
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']) # 투자대비수익률 숫자 변환
}
업종 분류 표준화
INDUSTRY_MAPPING = {
'음식점': ['한식', '중식', '일식', '양식', '카페', '베이커리', '치킨', '피자'],
'소매점': ['편의점', '슈퍼마켓', '화장품', '의류', '잡화'],
'서비스': ['미용실', '네일샵', 'PC방', '노래방', '헬스장'],
'숙박': ['모텔', '호텔', '게스트하우스', '펜션']
}
def classify_industry(raw_industry):
for category, subcategories in INDUSTRY_MAPPING.items():
if raw_industry in subcategories:
return category
return '기타'
지역 파싱
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
}
시즌 추출
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 이상치 탐지
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 임베딩 생성
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 벡터 검색
# 유사 이벤트 검색
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 인덱스 설정
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 요청 구조
{
"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 응답 구조
{
"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 서비스 레이어 응답 구조 (프론트엔드로 전달)
{
"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 템플릿
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 예제 (필요 시)
FEW_SHOT_EXAMPLES = [
{
"input": {
"industry": "음식점",
"location": "서울 강남구",
"purpose": "신규 고객 유치",
"season": "겨울"
},
"output": {
"trends": {
"industry": "음식점업 신년 프로모션 트렌드...",
"location": "강남구는 직장인 및 MZ세대 고객이 많아...",
"season": "겨울 시즌에는 따뜻한 실내 이벤트..."
},
"recommendations": [...]
}
}
]
4. 백엔드 구현 (Node.js)
4.1 AI 서비스 컨트롤러
// 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 서비스 레이어
// 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 서비스
// 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. 타임아웃 및 에러 처리
// 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단계 캐싱
// 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 캐시 무효화 전략
// 새로운 이벤트 성과 데이터 수집 시 관련 캐시 무효화
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. 병렬 처리 최적화
// 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 대응
// 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. 벡터 검색 최적화
# 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. 응답 시간 모니터링
// 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 |
비용 절감 방안
- 캐싱 활용률 향상 (목표: 50% 캐시 히트율)
- Claude API 비용 50% 절감 → $23,400
- 배치 처리 최적화
- 사용량 기반 동적 스케일링
모니터링 및 개선
1. 핵심 지표
성능 지표
- 평균 응답 시간: < 10초
- 캐시 히트율: > 50%
- API 성공률: > 99%
품질 지표
- 추천 선택률: > 70% (사용자가 6개 중 1개 이상 선택)
- "다시 추천받기" 비율: < 30%
- 예측 정확도: 실제 ROI와 예측 ROI 차이 < 20%
2. 개선 사이클
[데이터 수집]
│
▼
[정확도 분석]
│ - 예측 vs 실제 비교
│ - 업종별/지역별 분석
▼
[프롬프트 최적화]
│ - Few-shot 예제 개선
│ - 시스템 프롬프트 조정
▼
[A/B 테스트]
│ - 새 버전 vs 기존 버전
│ - 승자 선택
▼
[배포 및 모니터링]
부록
A. 데이터 스키마
PostgreSQL 테이블 구조
-- 이벤트 테이블
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. 환경 변수 설정
# .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
Pinecone
OpenAI Embeddings
문서 끝