mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 13:26:23 +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)
|
||
|
||
---
|
||
|
||
**문서 끝**
|