# 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) --- **문서 끝**