mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2026-06-13 15:39:12 +00:00
1613 lines
45 KiB
Markdown
1613 lines
45 KiB
Markdown
# 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)
|
|
|
|
---
|
|
|
|
**문서 끝**
|