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

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

45 KiB
Raw Permalink Blame History

AI 기반 이벤트 추천 시스템 구현방안

작성일: 2025-10-21 버전: 1.0 작성자: 프로젝트 팀 전체


목차

  1. 개요
  2. 데이터 확보 및 처리 방안
  3. 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 초기 데이터 수집 (프로젝트 시작 시)

# ETL 파이프라인 (Apache Airflow DAG)

# 일일 배치 작업
@dag(schedule_interval='0 2 * * *')  # 매일 새벽 2시
def collect_external_data():

    # Task 1: 공공데이터 수집
    @task
    def fetch_public_data():
        # 소상공인진흥공단 API 호출
        # 통계청 데이터 수집
        return data

    # Task 2: SNS 크롤링
    @task
    def crawl_sns_data():
        # 네이버 블로그 검색
        # Instagram 해시태그 검색
        return data

    # Task 3: 데이터 정제
    @task
    def clean_data(raw_data):
        # 중복 제거
        # 이상치 탐지 및 제거
        # 업종/지역/시즌 태깅
        # 텍스트 정규화
        return cleaned_data

    # Task 4: 데이터베이스 저장
    @task
    def save_to_db(cleaned_data):
        # PostgreSQL에 저장
        # events 테이블에 insert
        pass

    # Task 5: 벡터 임베딩 생성
    @task
    def generate_embeddings(cleaned_data):
        # OpenAI Embeddings API 호출
        # Pinecone에 저장
        pass

2.2 실시간 데이터 수집

// 이벤트 생성 시
async function onEventCreated(eventData) {
  // 1. 데이터베이스 저장
  await db.events.create(eventData);

  // 2. 벡터 임베딩 생성 (비동기)
  await queue.publish('embedding', {
    eventId: eventData.id,
    text: `${eventData.title} ${eventData.prize} ${eventData.participation}`
  });
}

// 이벤트 성과 수집
async function onEventCompleted(eventId, performanceData) {
  // 성과 데이터 저장
  await db.eventPerformance.create({
    eventId,
    actualParticipants: performanceData.participants,
    actualCost: performanceData.cost,
    actualRoi: performanceData.roi
  });

  // 추천 정확도 계산
  const prediction = await db.eventRecommendations.findOne({ eventId });
  const accuracy = calculateAccuracy(prediction, performanceData);

  // 모델 성능 모니터링
  await logAccuracy(accuracy);
}

3. 데이터 정제 프로세스

3.1 데이터 정제 규칙

텍스트 정규화

def normalize_event_data(raw_event):
    return {
        'title': clean_text(raw_event['title']),  # 특수문자 제거, 소문자 변환
        'prize': normalize_prize_name(raw_event['prize']),  # 경품명 표준화
        'participation': normalize_participation(raw_event['participation']),
        'industry': classify_industry(raw_event['industry']),  # 업종 분류
        'location': parse_location(raw_event['location']),  # 지역 파싱
        'season': extract_season(raw_event['date']),  # 시즌 추출
        'cost': parse_cost(raw_event['cost']),  # 비용 숫자 변환
        'roi': parse_roi(raw_event['roi'])  # 투자대비수익률 숫자 변환
    }

업종 분류 표준화

INDUSTRY_MAPPING = {
    '음식점': ['한식', '중식', '일식', '양식', '카페', '베이커리', '치킨', '피자'],
    '소매점': ['편의점', '슈퍼마켓', '화장품', '의류', '잡화'],
    '서비스': ['미용실', '네일샵', 'PC방', '노래방', '헬스장'],
    '숙박': ['모텔', '호텔', '게스트하우스', '펜션']
}

def classify_industry(raw_industry):
    for category, subcategories in INDUSTRY_MAPPING.items():
        if raw_industry in subcategories:
            return category
    return '기타'

지역 파싱

def parse_location(address):
    # "서울특별시 강남구 역삼동" -> {"city": "서울", "district": "강남구"}
    import re

    city_pattern = r'(서울|부산|대구|인천|광주|대전|울산|세종|경기|강원|충북|충남|전북|전남|경북|경남|제주)'
    district_pattern = r'([가-힣]+구)'

    city = re.search(city_pattern, address)
    district = re.search(district_pattern, address)

    return {
        'city': city.group(1) if city else None,
        'district': district.group(1) if district else None
    }

시즌 추출

def extract_season(date):
    month = date.month
    if month in [12, 1, 2]:
        return '겨울'
    elif month in [3, 4, 5]:
        return '봄'
    elif month in [6, 7, 8]:
        return '여름'
    else:
        return '가을'

3.2 이상치 탐지

def detect_outliers(events_df):
    # IQR 방식으로 이상치 탐지
    Q1 = events_df['roi'].quantile(0.25)
    Q3 = events_df['roi'].quantile(0.75)
    IQR = Q3 - Q1

    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR

    # 이상치 제거
    filtered_df = events_df[
        (events_df['roi'] >= lower_bound) &
        (events_df['roi'] <= upper_bound)
    ]

    return filtered_df

4. 벡터라이징 전략

4.1 임베딩 생성

import openai
import pinecone

# OpenAI 임베딩 생성
def generate_embedding(text):
    response = openai.embeddings.create(
        model="text-embedding-3-large",  # 3072 차원
        input=text
    )
    return response.data[0].embedding

# 이벤트 데이터를 텍스트로 변환
def event_to_text(event):
    return f"""
이벤트명: {event['title']}
업종: {event['industry']}
경품: {event['prize']}
참여방법: {event['participation']}
지역: {event['location']['city']} {event['location']['district']}
시즌: {event['season']}
예산: {event['cost']}투자대비수익률: {event['roi']}%
"""

# Pinecone에 저장
def save_to_pinecone(event):
    text = event_to_text(event)
    embedding = generate_embedding(text)

    pinecone_index.upsert(vectors=[{
        'id': event['id'],
        'values': embedding,
        'metadata': {
            'title': event['title'],
            'industry': event['industry'],
            'location': event['location']['district'],
            'season': event['season'],
            'budget': event['cost'],
            'roi': event['roi'],
            'prize': event['prize'],
            'participation': event['participation']
        }
    }])

4.2 벡터 검색

# 유사 이벤트 검색
def search_similar_events(query_event, top_k=5):
    # 쿼리 텍스트 생성
    query_text = event_to_text(query_event)

    # 쿼리 임베딩 생성
    query_embedding = generate_embedding(query_text)

    # 필터 조건 구성
    filters = {
        'industry': query_event['industry'],
        'budget': {'$gte': query_event['cost'] * 0.5, '$lte': query_event['cost'] * 1.5}
    }

    # Pinecone 검색
    results = pinecone_index.query(
        vector=query_embedding,
        filter=filters,
        top_k=top_k,
        include_metadata=True
    )

    return results['matches']

4.3 Pinecone 인덱스 설정

import pinecone

# Pinecone 초기화
pinecone.init(
    api_key="YOUR_API_KEY",
    environment="us-west1-gcp"
)

# 인덱스 생성
index_name = "kt-event-recommendations"

if index_name not in pinecone.list_indexes():
    pinecone.create_index(
        name=index_name,
        dimension=3072,  # text-embedding-3-large 차원
        metric='cosine',
        pods=1,
        pod_type='p1.x1'  # 성능 요구사항에 따라 조정
    )

# 인덱스 연결
pinecone_index = pinecone.Index(index_name)

5. 데이터 통계 및 분포 분석

5.1 초기 목표 데이터셋 규모

카테고리 목표 건수 수집 방법
외부 데이터 (공공/SNS) 300건 크롤링 + API
벤치마크 사례 100건 수동 수집
자사 데이터 (초기) 0건 -
총계 400건 -

5.2 데이터 분포 목표

업종별 분포

  • 음식점: 40%
  • 소매점: 25%
  • 서비스: 20%
  • 숙박: 10%
  • 기타: 5%

예산별 분포

  • 저비용 (50만원 이하): 40%
  • 중비용 (50~200만원): 35%
  • 고비용 (200만원 이상): 25%

지역별 분포

  • 서울: 30%
  • 경기: 25%
  • 부산/대구/인천: 20%
  • 기타 지역: 25%

Claude API 연동 구조

1. API 호출 전략

1.1 단일 호출 + Structured Output 방식 (최종 선택)

선택 이유

  • 응답 시간 단축 (10초 내 보장)
  • API 비용 절감
  • 트렌드와 추천의 일관성 유지
  • Structured Output으로 JSON 파싱 안정성 향상

호출 플로우

1. 사용자 요청 → AI 서비스
2. 유사 이벤트 벡터 검색 (Pinecone)
3. 캐시 확인 (Redis)
4. Claude API 단일 호출 (트렌드 + 6개 추천)
5. 응답 파싱 및 검증
6. 캐시 저장
7. 프론트엔드 응답

2. JSON 요청/응답 구조

2.1 Claude API 요청 구조

{
  "model": "claude-3-5-sonnet-20241022",
  "max_tokens": 4096,
  "temperature": 0.7,
  "system": "당신은 소상공인을 위한 이벤트 마케팅 전문가입니다. 주어진 매장 정보와 이벤트 목적을 바탕으로 효과적인 이벤트 기획안을 제안합니다.",
  "messages": [
    {
      "role": "user",
      "content": "다음 정보를 바탕으로 이벤트를 추천해주세요:\n\n[매장 정보]\n- 업종: 음식점 (고깃집)\n- 지역: 서울 강남구\n- 이벤트 목적: 신규 고객 유치\n- 현재 시즌: 2025년 1월 (겨울)\n\n[참고 데이터]\n과거 유사한 매장에서 성공한 이벤트:\n1. SNS 팔로우 이벤트 - 참여자 200명, 비용 30만원, ROI 450%\n2. 리뷰 작성 이벤트 - 참여자 180명, 비용 120만원, ROI 380%\n...\n\n다음 형식으로 응답해주세요:\n1. 업종/지역/시즌 트렌드 분석\n2. 3가지 예산별 이벤트 추천 (각 온라인/오프라인 1개씩)"
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "event_recommendations",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "trends": {
            "type": "object",
            "properties": {
              "industry": {"type": "string"},
              "location": {"type": "string"},
              "season": {"type": "string"}
            },
            "required": ["industry", "location", "season"]
          },
          "recommendations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "budget": {"type": "string", "enum": ["low", "medium", "high"]},
                "type": {"type": "string", "enum": ["online", "offline"]},
                "title": {"type": "string"},
                "prize": {"type": "string"},
                "participation": {"type": "string"},
                "expectedParticipants": {"type": "integer"},
                "cost": {"type": "integer"},
                "roi": {"type": "integer"}
              },
              "required": ["budget", "type", "title", "prize", "participation", "expectedParticipants", "cost", "roi"]
            },
            "minItems": 6,
            "maxItems": 6
          }
        },
        "required": ["trends", "recommendations"]
      }
    }
  }
}

2.2 Claude API 응답 구조

{
  "id": "msg_01...",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "{\"trends\":{\"industry\":\"음식점업 신년 프로모션 트렌드: 1월은 새해 신규 고객 유치를 위한 할인 이벤트와 SNS 바이럴 마케팅이 효과적입니다. 특히 고깃집은 단체 할인 및 재방문 쿠폰 제공이 인기입니다.\",\"location\":\"강남구는 직장인 및 MZ세대 고객이 많아 SNS 기반 이벤트와 점심 특가 이벤트가 효과적입니다. 특히 Instagram과 네이버 블로그를 활용한 바이럴 마케팅이 유리합니다.\",\"season\":\"겨울 시즌에는 따뜻한 실내 이벤트와 재방문 유도 프로모션이 효과적입니다. 설 연휴 대비 가족 단위 고객 타겟팅이 중요합니다.\"},\"recommendations\":[{\"budget\":\"low\",\"type\":\"online\",\"title\":\"SNS 팔로우 이벤트\",\"prize\":\"커피 쿠폰\",\"participation\":\"Instagram 팔로우 + 게시물 공유\",\"expectedParticipants\":180,\"cost\":250000,\"roi\":520},{\"budget\":\"low\",\"type\":\"offline\",\"title\":\"전화번호 등록 이벤트\",\"prize\":\"커피 쿠폰\",\"participation\":\"매장 방문 시 전화번호 등록\",\"expectedParticipants\":150,\"cost\":300000,\"roi\":450},{\"budget\":\"medium\",\"type\":\"online\",\"title\":\"리뷰 작성 이벤트\",\"prize\":\"5천원 상품권\",\"participation\":\"네이버 블로그 리뷰 작성\",\"expectedParticipants\":250,\"cost\":1500000,\"roi\":380},{\"budget\":\"medium\",\"type\":\"offline\",\"title\":\"방문 도장 적립 이벤트\",\"prize\":\"무료 식사권\",\"participation\":\"5회 방문 시 도장 적립\",\"expectedParticipants\":200,\"cost\":1800000,\"roi\":320},{\"budget\":\"high\",\"type\":\"online\",\"title\":\"인플루언서 협업 이벤트\",\"prize\":\"1만원 할인권\",\"participation\":\"인플루언서 게시물 좋아요 + 팔로우\",\"expectedParticipants\":400,\"cost\":5000000,\"roi\":280},{\"budget\":\"high\",\"type\":\"offline\",\"title\":\"VIP 고객 초대 이벤트\",\"prize\":\"특별 메뉴 제공\",\"participation\":\"VIP 초대장 발송\",\"expectedParticipants\":300,\"cost\":6000000,\"roi\":240}]}"
    }
  ],
  "model": "claude-3-5-sonnet-20241022",
  "usage": {
    "input_tokens": 1205,
    "output_tokens": 856
  }
}

2.3 서비스 레이어 응답 구조 (프론트엔드로 전달)

{
  "success": true,
  "data": {
    "trends": {
      "industry": "음식점업 신년 프로모션 트렌드: 1월은 새해 신규 고객 유치를 위한 할인 이벤트와 SNS 바이럴 마케팅이 효과적입니다...",
      "location": "강남구는 직장인 및 MZ세대 고객이 많아 SNS 기반 이벤트와 점심 특가 이벤트가 효과적입니다...",
      "season": "겨울 시즌에는 따뜻한 실내 이벤트와 재방문 유도 프로모션이 효과적입니다..."
    },
    "recommendations": [
      {
        "id": "low-online",
        "budget": "low",
        "type": "online",
        "title": "SNS 팔로우 이벤트",
        "prize": "커피 쿠폰",
        "participation": "Instagram 팔로우 + 게시물 공유",
        "expectedParticipants": 180,
        "cost": 250000,
        "roi": 520
      },
      {
        "id": "low-offline",
        "budget": "low",
        "type": "offline",
        "title": "전화번호 등록 이벤트",
        "prize": "커피 쿠폰",
        "participation": "매장 방문 시 전화번호 등록",
        "expectedParticipants": 150,
        "cost": 300000,
        "roi": 450
      },
      {
        "id": "medium-online",
        "budget": "medium",
        "type": "online",
        "title": "리뷰 작성 이벤트",
        "prize": "5천원 상품권",
        "participation": "네이버 블로그 리뷰 작성",
        "expectedParticipants": 250,
        "cost": 1500000,
        "roi": 380
      },
      {
        "id": "medium-offline",
        "budget": "medium",
        "type": "offline",
        "title": "방문 도장 적립 이벤트",
        "prize": "무료 식사권",
        "participation": "5회 방문 시 도장 적립",
        "expectedParticipants": 200,
        "cost": 1800000,
        "roi": 320
      },
      {
        "id": "high-online",
        "budget": "high",
        "type": "online",
        "title": "인플루언서 협업 이벤트",
        "prize": "1만원 할인권",
        "participation": "인플루언서 게시물 좋아요 + 팔로우",
        "expectedParticipants": 400,
        "cost": 5000000,
        "roi": 280
      },
      {
        "id": "high-offline",
        "budget": "high",
        "type": "offline",
        "title": "VIP 고객 초대 이벤트",
        "prize": "특별 메뉴 제공",
        "participation": "VIP 초대장 발송",
        "expectedParticipants": 300,
        "cost": 6000000,
        "roi": 240
      }
    ]
  },
  "metadata": {
    "cacheHit": false,
    "processingTime": 8.5,
    "modelUsage": {
      "inputTokens": 1205,
      "outputTokens": 856
    }
  }
}

3. 프롬프트 엔지니어링

3.1 System Prompt

당신은 소상공인을 위한 이벤트 마케팅 전문가입니다.

[역할]
- 매장 정보와 이벤트 목적을 바탕으로 효과적인 이벤트 기획안 제안
- 업종별, 지역별, 시즌별 트렌드 분석
- 예산별 차별화된 이벤트 추천

[제약사항]
- 추천은 반드시 6개 (저/중/고 예산 × 온라인/오프라인)
- 모든 추천은 실현 가능하고 구체적이어야 함
- 예상 참여자, 비용, 투자대비수익률은 과거 데이터 기반 현실적 수치
- 경품은 예산 범위 내에서 실현 가능한 것

[응답 형식]
- JSON 형식으로 응답
- trends: 업종/지역/시즌 트렌드 분석 (각 100자 내외)
- recommendations: 6개 이벤트 기획안 배열

3.2 User Prompt 템플릿

def build_user_prompt(store_info, event_purpose, similar_events):
    return f"""
다음 정보를 바탕으로 이벤트를 추천해주세요:

[매장 정보]
- 업종: {store_info['industry']} ({store_info['businessType']})
- 지역: {store_info['location']['city']} {store_info['location']['district']}
- 이벤트 목적: {event_purpose}
- 현재 시즌: {get_current_season()}

[참고 데이터]
과거 유사한 매장에서 성공한 이벤트:
{format_similar_events(similar_events)}

[요구사항]
1. 업종/지역/시즌 트렌드 분석 (각 100자 내외)
2. 3가지 예산별 이벤트 추천:
   - 저비용 (25~30만원): 온라인 1개, 오프라인 1개
   - 중비용 (150~180만원): 온라인 1개, 오프라인 1개
   - 고비용 (500~600만원): 온라인 1개, 오프라인 1개

각 추천에는 다음 정보를 포함:
- 이벤트 제목
- 경품명
- 참여 방법
- 예상 참여자 수
- 예상 비용 (원 단위)
- 예상 투자대비수익률 (%)
"""

def format_similar_events(events):
    formatted = []
    for i, event in enumerate(events, 1):
        formatted.append(f"{i}. {event['title']} - 참여자 {event['participants']}명, 비용 {event['cost']:,}원, ROI {event['roi']}%")
    return "\n".join(formatted)

3.3 Few-shot 예제 (필요 시)

FEW_SHOT_EXAMPLES = [
    {
        "input": {
            "industry": "음식점",
            "location": "서울 강남구",
            "purpose": "신규 고객 유치",
            "season": "겨울"
        },
        "output": {
            "trends": {
                "industry": "음식점업 신년 프로모션 트렌드...",
                "location": "강남구는 직장인 및 MZ세대 고객이 많아...",
                "season": "겨울 시즌에는 따뜻한 실내 이벤트..."
            },
            "recommendations": [...]
        }
    }
]

4. 백엔드 구현 (Node.js)

4.1 AI 서비스 컨트롤러

// controllers/aiController.js
const aiService = require('../services/aiService');
const cacheService = require('../services/cacheService');

exports.getEventRecommendations = async (req, res) => {
  try {
    const { eventPurpose, storeInfo } = req.body;
    const userId = req.user.id;

    // 1. 캐시 키 생성
    const cacheKey = `recommendations:${storeInfo.industry}:${storeInfo.location.district}:${eventPurpose}`;

    // 2. 캐시 확인
    const cached = await cacheService.get(cacheKey);
    if (cached) {
      return res.json({
        success: true,
        data: cached,
        metadata: { cacheHit: true }
      });
    }

    // 3. AI 추천 생성
    const startTime = Date.now();
    const recommendations = await aiService.generateRecommendations({
      eventPurpose,
      storeInfo,
      season: getCurrentSeason()
    });
    const processingTime = (Date.now() - startTime) / 1000;

    // 4. 캐시 저장 (15분 TTL)
    await cacheService.set(cacheKey, recommendations, 900);

    // 5. 응답
    res.json({
      success: true,
      data: recommendations,
      metadata: {
        cacheHit: false,
        processingTime,
        modelUsage: recommendations.usage
      }
    });

  } catch (error) {
    console.error('AI recommendation error:', error);
    res.status(500).json({
      success: false,
      error: 'AI 추천 생성 중 오류가 발생했습니다.'
    });
  }
};

function getCurrentSeason() {
  const month = new Date().getMonth() + 1;
  if ([12, 1, 2].includes(month)) return '겨울';
  if ([3, 4, 5].includes(month)) return '봄';
  if ([6, 7, 8].includes(month)) return '여름';
  return '가을';
}

4.2 AI 서비스 레이어

// services/aiService.js
const Anthropic = require('@anthropic-ai/sdk');
const pineconeService = require('./pineconeService');

const anthropic = new Anthropic({
  apiKey: process.env.CLAUDE_API_KEY
});

exports.generateRecommendations = async ({ eventPurpose, storeInfo, season }) => {
  try {
    // 1. 유사 이벤트 검색
    const similarEvents = await pineconeService.searchSimilarEvents({
      industry: storeInfo.industry,
      location: storeInfo.location.district,
      season
    });

    // 2. 프롬프트 생성
    const userPrompt = buildUserPrompt(storeInfo, eventPurpose, season, similarEvents);

    // 3. Claude API 호출
    const message = await anthropic.messages.create({
      model: 'claude-3-5-sonnet-20241022',
      max_tokens: 4096,
      temperature: 0.7,
      system: getSystemPrompt(),
      messages: [{ role: 'user', content: userPrompt }],
      response_format: {
        type: 'json_schema',
        json_schema: getResponseSchema()
      }
    });

    // 4. 응답 파싱
    const content = message.content[0].text;
    const result = JSON.parse(content);

    // 5. ID 추가 및 검증
    result.recommendations = result.recommendations.map((rec, idx) => ({
      id: `${rec.budget}-${rec.type}`,
      ...rec
    }));

    // 6. 검증
    validateRecommendations(result);

    return {
      ...result,
      usage: message.usage
    };

  } catch (error) {
    console.error('Claude API error:', error);

    // Fallback: 기본 추천 반환
    return getFallbackRecommendations(storeInfo);
  }
};

function buildUserPrompt(storeInfo, eventPurpose, season, similarEvents) {
  const purposeMap = {
    '신규고객유치': '신규 고객 유치',
    '재방문유도': '재방문 유도',
    '매출증대': '매출 증대',
    '인지도향상': '인지도 향상'
  };

  return `
다음 정보를 바탕으로 이벤트를 추천해주세요:

[매장 정보]
- 업종: ${storeInfo.industry} (${storeInfo.businessType})
- 지역: ${storeInfo.location.city} ${storeInfo.location.district}
- 이벤트 목적: ${purposeMap[eventPurpose]}
- 현재 시즌: ${season}

[참고 데이터]
과거 유사한 매장에서 성공한 이벤트:
${formatSimilarEvents(similarEvents)}

[요구사항]
1. 업종/지역/시즌 트렌드 분석 (각 100자 내외)
2. 3가지 예산별 이벤트 추천:
   - 저비용 (25~30만원): 온라인 1개, 오프라인 1개
   - 중비용 (150~180만원): 온라인 1개, 오프라인 1개
   - 고비용 (500~600만원): 온라인 1개, 오프라인 1개

각 추천에는 다음 정보를 포함:
- 이벤트 제목 (30자 이내)
- 경품명 (20자 이내)
- 참여 방법 (간결하게)
- 예상 참여자 수 (정수)
- 예상 비용 (원 단위, 정수)
- 예상 투자대비수익률 (%, 정수)
`.trim();
}

function formatSimilarEvents(events) {
  return events.map((e, i) =>
    `${i + 1}. ${e.metadata.title} - 참여자 ${e.metadata.participants}명, 비용 ${e.metadata.cost.toLocaleString()}원, ROI ${e.metadata.roi}%`
  ).join('\n');
}

function getSystemPrompt() {
  return `당신은 소상공인을 위한 이벤트 마케팅 전문가입니다.

[역할]
- 매장 정보와 이벤트 목적을 바탕으로 효과적인 이벤트 기획안 제안
- 업종별, 지역별, 시즌별 트렌드 분석
- 예산별 차별화된 이벤트 추천

[제약사항]
- 추천은 반드시 6개 (저/중/고 예산 × 온라인/오프라인)
- 모든 추천은 실현 가능하고 구체적이어야 함
- 예상 참여자, 비용, 투자대비수익률은 과거 데이터 기반 현실적 수치
- 경품은 예산 범위 내에서 실현 가능한 것
- 투자대비수익률은 저예산일수록 높고, 고예산일수록 낮게 설정`;
}

function getResponseSchema() {
  return {
    name: 'event_recommendations',
    strict: true,
    schema: {
      type: 'object',
      properties: {
        trends: {
          type: 'object',
          properties: {
            industry: { type: 'string' },
            location: { type: 'string' },
            season: { type: 'string' }
          },
          required: ['industry', 'location', 'season']
        },
        recommendations: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              budget: { type: 'string', enum: ['low', 'medium', 'high'] },
              type: { type: 'string', enum: ['online', 'offline'] },
              title: { type: 'string' },
              prize: { type: 'string' },
              participation: { type: 'string' },
              expectedParticipants: { type: 'integer' },
              cost: { type: 'integer' },
              roi: { type: 'integer' }
            },
            required: ['budget', 'type', 'title', 'prize', 'participation', 'expectedParticipants', 'cost', 'roi']
          },
          minItems: 6,
          maxItems: 6
        }
      },
      required: ['trends', 'recommendations']
    }
  };
}

function validateRecommendations(result) {
  // 6개 추천 검증
  if (result.recommendations.length !== 6) {
    throw new Error('추천 개수가 6개가 아닙니다');
  }

  // 예산별, 타입별 개수 검증
  const counts = {};
  result.recommendations.forEach(rec => {
    const key = `${rec.budget}-${rec.type}`;
    counts[key] = (counts[key] || 0) + 1;
  });

  const expected = {
    'low-online': 1,
    'low-offline': 1,
    'medium-online': 1,
    'medium-offline': 1,
    'high-online': 1,
    'high-offline': 1
  };

  for (const [key, count] of Object.entries(expected)) {
    if (counts[key] !== count) {
      throw new Error(`${key} 추천 개수가 올바르지 않습니다`);
    }
  }
}

function getFallbackRecommendations(storeInfo) {
  // API 실패 시 기본 추천 반환
  return {
    trends: {
      industry: `${storeInfo.industry} 업종의 일반적인 트렌드입니다.`,
      location: `${storeInfo.location.district} 지역의 일반적인 특성입니다.`,
      season: '계절별 일반적인 이벤트 특성입니다.'
    },
    recommendations: [
      {
        id: 'low-online',
        budget: 'low',
        type: 'online',
        title: 'SNS 팔로우 이벤트',
        prize: '커피 쿠폰',
        participation: 'SNS 팔로우',
        expectedParticipants: 150,
        cost: 250000,
        roi: 500
      },
      // ... 나머지 5개
    ]
  };
}

4.3 Pinecone 서비스

// services/pineconeService.js
const { PineconeClient } = require('@pinecone-database/pinecone');
const openai = require('openai');

const pinecone = new PineconeClient();
await pinecone.init({
  apiKey: process.env.PINECONE_API_KEY,
  environment: process.env.PINECONE_ENV
});

const index = pinecone.Index('kt-event-recommendations');

exports.searchSimilarEvents = async ({ industry, location, season }) => {
  try {
    // 1. 쿼리 텍스트 생성
    const queryText = `업종: ${industry}, 지역: ${location}, 시즌: ${season}`;

    // 2. 임베딩 생성
    const embedding = await generateEmbedding(queryText);

    // 3. 필터 조건
    const filter = {
      industry: { $eq: industry }
    };

    // 4. 벡터 검색
    const results = await index.query({
      vector: embedding,
      filter,
      topK: 5,
      includeMetadata: true
    });

    return results.matches;

  } catch (error) {
    console.error('Pinecone search error:', error);
    return [];
  }
};

async function generateEmbedding(text) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-large',
    input: text
  });
  return response.data[0].embedding;
}

5. 타임아웃 및 에러 처리

// utils/timeout.js
exports.withTimeout = (promise, timeoutMs) => {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), timeoutMs)
    )
  ]);
};

// 사용 예시
const recommendations = await withTimeout(
  aiService.generateRecommendations(params),
  10000  // 10초
);

시스템 아키텍처

1. 전체 아키텍처 다이어그램

┌─────────────────┐
│   Frontend      │
│   (React)       │
└────────┬────────┘
         │ HTTP/REST
         ▼
┌─────────────────────────────────────────────────────┐
│             API Gateway (Express)                   │
└────────┬────────────────────────────────────────────┘
         │
         ├─────────────────┬──────────────────┬─────────────┐
         ▼                 ▼                  ▼             ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────┐
│User Service  │  │Event Service │  │AI Service    │  │Analytics │
└──────────────┘  └──────────────┘  └──────┬───────┘  └──────────┘
                                            │
                  ┌─────────────────────────┼────────────────┐
                  ▼                         ▼                ▼
         ┌────────────────┐      ┌──────────────────┐  ┌──────────┐
         │Redis Cache     │      │Pinecone          │  │Claude API│
         │- Trends (1h)   │      │Vector DB         │  └──────────┘
         │- Similar (30m) │      │- Event Embeddings│
         │- Results (15m) │      └──────────────────┘
         └────────────────┘
                  │
                  ▼
         ┌────────────────┐
         │PostgreSQL      │
         │- Users         │
         │- Events        │
         │- Performance   │
         └────────────────┘

2. AI 서비스 상세 플로우

사용자 요청
    │
    ▼
[1] 요청 수신
    │
    ▼
[2] 캐시 확인 (Redis)
    │
    ├─ Hit → 즉시 응답 (< 100ms)
    │
    └─ Miss
        │
        ▼
    [3] 유사 이벤트 검색 (Pinecone)
        │ - 쿼리 임베딩 생성 (OpenAI API)
        │ - 벡터 검색 (코사인 유사도)
        │ - Top 5 반환
        ▼
    [4] 프롬프트 생성
        │ - 매장 정보 + 이벤트 목적
        │ - 유사 이벤트 컨텍스트
        │ - Few-shot 예제
        ▼
    [5] Claude API 호출
        │ - Structured Output
        │ - 타임아웃: 8초
        ▼
    [6] 응답 파싱 및 검증
        │ - JSON 파싱
        │ - 6개 추천 검증
        │ - 예산/타입 검증
        ▼
    [7] 캐시 저장 (Redis)
        │ - TTL: 15분
        ▼
    [8] 프론트엔드 응답
        │ - 총 소요시간: < 10초

3. 데이터 파이프라인

[일일 배치 작업] (Airflow)
    │
    ├─ [외부 데이터 수집]
    │   ├─ 공공데이터 API
    │   ├─ SNS 크롤링
    │   └─ 벤치마크 사례
    │
    ▼
[데이터 정제]
    │ - 중복 제거
    │ - 이상치 탐지
    │ - 태깅
    ▼
[PostgreSQL 저장]
    │
    ▼
[벡터 임베딩 생성]
    │ - OpenAI API
    │ - 배치 처리
    ▼
[Pinecone 저장]


[실시간 이벤트 생성]
    │
    ▼
[PostgreSQL 저장]
    │
    ▼
[비동기 큐 (RabbitMQ)]
    │
    ▼
[벡터 임베딩 생성]
    │
    ▼
[Pinecone 업데이트]

성능 최적화 전략

1. Redis 캐싱 전략

1.1 3단계 캐싱

// services/cacheService.js
const redis = require('redis');
const client = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
  password: process.env.REDIS_PASSWORD
});

// 레벨 1: 트렌드 분석 캐싱 (1시간 TTL)
exports.getTrendCache = async (industry, location, season) => {
  const key = `trend:${industry}:${location}:${season}`;
  const cached = await client.get(key);
  return cached ? JSON.parse(cached) : null;
};

exports.setTrendCache = async (industry, location, season, data) => {
  const key = `trend:${industry}:${location}:${season}`;
  await client.setex(key, 3600, JSON.stringify(data));
};

// 레벨 2: 유사 이벤트 검색 캐싱 (30분 TTL)
exports.getSimilarEventsCache = async (industry, location, season) => {
  const key = `similar:${industry}:${location}:${season}`;
  const cached = await client.get(key);
  return cached ? JSON.parse(cached) : null;
};

exports.setSimilarEventsCache = async (industry, location, season, data) => {
  const key = `similar:${industry}:${location}:${season}`;
  await client.setex(key, 1800, JSON.stringify(data));
};

// 레벨 3: 전체 추천 결과 캐싱 (15분 TTL)
exports.getRecommendationCache = async (industry, location, purpose) => {
  const key = `recommendations:${industry}:${location}:${purpose}`;
  const cached = await client.get(key);
  return cached ? JSON.parse(cached) : null;
};

exports.setRecommendationCache = async (industry, location, purpose, data) => {
  const key = `recommendations:${industry}:${location}:${purpose}`;
  await client.setex(key, 900, JSON.stringify(data));
};

1.2 캐시 무효화 전략

// 새로운 이벤트 성과 데이터 수집 시 관련 캐시 무효화
exports.invalidateRelatedCache = async (eventData) => {
  const patterns = [
    `trend:${eventData.industry}:*`,
    `similar:${eventData.industry}:*`,
    `recommendations:${eventData.industry}:*`
  ];

  for (const pattern of patterns) {
    const keys = await client.keys(pattern);
    if (keys.length > 0) {
      await client.del(...keys);
    }
  }
};

2. 병렬 처리 최적화

// services/aiService.js
exports.generateRecommendations = async ({ eventPurpose, storeInfo, season }) => {
  // 병렬 실행
  const [similarEvents, trendCache] = await Promise.all([
    // 유사 이벤트 검색
    pineconeService.searchSimilarEvents({
      industry: storeInfo.industry,
      location: storeInfo.location.district,
      season
    }),

    // 트렌드 캐시 확인
    cacheService.getTrendCache(
      storeInfo.industry,
      storeInfo.location.district,
      season
    )
  ]);

  // ... 나머지 로직
};

3. Rate Limiting 대응

// utils/rateLimiter.js
const rateLimit = require('express-rate-limit');

// Claude API Rate Limit 대응
const apiLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1분
  max: 50,  // 분당 최대 50건 (Claude API 제한에 맞춤)
  message: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.',
  handler: async (req, res) => {
    // Rate limit 초과 시 캐시된 기본 추천 반환
    const fallback = await getFallbackRecommendations(req.body.storeInfo);
    res.status(200).json({
      success: true,
      data: fallback,
      metadata: {
        rateLimited: true,
        message: '일시적으로 기본 추천을 제공합니다'
      }
    });
  }
});

module.exports = apiLimiter;

4. 벡터 검색 최적화

# Pinecone 인덱스 파티셔닝
# 업종별로 네임스페이스 분리하여 검색 성능 향상

def save_to_pinecone_with_namespace(event):
    industry_namespace = event['industry']

    pinecone_index.upsert(
        vectors=[{
            'id': event['id'],
            'values': embedding,
            'metadata': {...}
        }],
        namespace=industry_namespace  # 업종별 네임스페이스
    )

def search_with_namespace(query_event):
    industry_namespace = query_event['industry']

    results = pinecone_index.query(
        vector=query_embedding,
        namespace=industry_namespace,  # 동일 업종 내에서만 검색
        top_k=5
    )
    return results

5. 응답 시간 모니터링

// middleware/performanceMonitor.js
exports.trackPerformance = async (req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;

    // 10초 초과 시 경고
    if (duration > 10000) {
      console.warn(`Slow request: ${req.path} took ${duration}ms`);

      // 모니터링 시스템에 전송 (예: Datadog, New Relic)
      sendMetric('ai.recommendation.slow', {
        path: req.path,
        duration,
        industry: req.body.storeInfo?.industry
      });
    }

    // 평균 응답 시간 추적
    sendMetric('ai.recommendation.duration', duration);
  });

  next();
};

구현 로드맵

Phase 1: 기본 인프라 구축 (2주)

Week 1

  • Pinecone 계정 생성 및 인덱스 설정
  • Redis 클러스터 구축
  • PostgreSQL 스키마 설계
  • Claude API 키 발급 및 테스트

Week 2

  • 외부 데이터 수집 스크립트 개발
  • ETL 파이프라인 (Airflow DAG) 구축
  • 데이터 정제 로직 구현
  • 초기 데이터셋 구축 (300건)

Phase 2: AI 서비스 개발 (3주)

Week 3

  • Claude API 연동 기본 구조
  • 프롬프트 템플릿 개발
  • Structured Output 스키마 설계
  • 응답 파싱 및 검증 로직

Week 4

  • 벡터 임베딩 생성 로직
  • Pinecone 연동 (저장/검색)
  • 유사 이벤트 검색 알고리즘
  • 캐싱 레이어 구현

Week 5

  • AI 서비스 통합 테스트
  • 성능 최적화 (병렬 처리)
  • 에러 처리 및 Fallback 구현
  • 응답 시간 모니터링

Phase 3: 프론트엔드 연동 (1주)

Week 6

  • API 엔드포인트 연동
  • 로딩 상태 UI 구현
  • 에러 핸들링 UI
  • E2E 테스트

Phase 4: 운영 및 개선 (지속적)

Week 7+

  • 실사용 데이터 수집 시작
  • 추천 정확도 모니터링
  • 프롬프트 최적화
  • A/B 테스트 진행
  • 사용자 피드백 반영

비용 예측

1. Claude API 비용

가정

  • 월 사용자 수: 1,000명
  • 사용자당 월 평균 이벤트 생성: 3회
  • 월 총 API 호출: 3,000회

토큰 사용량 (예상)

  • Input: 1,200 tokens/호출
  • Output: 800 tokens/호출

비용 계산 (Claude 3.5 Sonnet 기준)

  • Input: $3 / 1M tokens = $0.003 / 1K tokens
  • Output: $15 / 1M tokens = $0.015 / 1K tokens
월 비용 = (1,200 * 0.003 + 800 * 0.015) * 3,000
        = (3.6 + 12) * 3,000
        = 15.6 * 3,000
        = $46,800 / 월

2. OpenAI Embeddings API 비용

가정

  • 일일 외부 데이터 수집: 10건
  • 월 수집: 300건
  • 사용자 이벤트 생성: 3,000건/월

비용 계산 (text-embedding-3-large 기준)

  • $0.13 / 1M tokens
월 비용 = (300 + 3,000) * 100 tokens * 0.13 / 1,000,000
        = 3,300 * 100 * 0.00000013
        = $0.04 / 월

3. Pinecone 비용

가정

  • 벡터 차원: 3,072
  • 저장 벡터 수: 10,000건
  • 월 쿼리 수: 3,000회

비용 계산 (Starter Plan)

  • $70 / 월 (100,000 벡터 포함)

4. Redis 비용

가정

  • AWS ElastiCache (cache.t3.micro)

비용 계산

  • $0.017 / 시간 = $12.24 / 월

5. 총 비용

항목 월 비용
Claude API $46,800
OpenAI Embeddings $0.04
Pinecone $70
Redis $12.24
총계 $46,882

비용 절감 방안

  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 테이블 구조

-- 이벤트 테이블
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  user_id INTEGER REFERENCES users(id),
  title VARCHAR(100),
  prize VARCHAR(50),
  participation VARCHAR(200),
  industry VARCHAR(50),
  location_city VARCHAR(50),
  location_district VARCHAR(50),
  season VARCHAR(10),
  cost INTEGER,
  expected_roi INTEGER,
  created_at TIMESTAMP DEFAULT NOW()
);

-- 이벤트 성과 테이블
CREATE TABLE event_performance (
  id SERIAL PRIMARY KEY,
  event_id INTEGER REFERENCES events(id),
  actual_participants INTEGER,
  actual_cost INTEGER,
  actual_roi INTEGER,
  completed_at TIMESTAMP DEFAULT NOW()
);

-- AI 추천 이력 테이블
CREATE TABLE ai_recommendations (
  id SERIAL PRIMARY KEY,
  event_id INTEGER REFERENCES events(id),
  recommendation_data JSONB,  -- Claude API 응답 전체
  selected_option VARCHAR(20),  -- 예: "low-online"
  created_at TIMESTAMP DEFAULT NOW()
);

-- 인덱스
CREATE INDEX idx_events_industry ON events(industry);
CREATE INDEX idx_events_location ON events(location_district);
CREATE INDEX idx_events_season ON events(season);
CREATE INDEX idx_performance_event ON event_performance(event_id);

B. 환경 변수 설정

# .env
# Claude API
CLAUDE_API_KEY=sk-ant-xxx

# OpenAI
OPENAI_API_KEY=sk-xxx

# Pinecone
PINECONE_API_KEY=xxx
PINECONE_ENV=us-west1-gcp
PINECONE_INDEX=kt-event-recommendations

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=xxx

# PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_NAME=kt_events
DB_USER=postgres
DB_PASSWORD=xxx

C. 참고 자료

Claude API

Pinecone

OpenAI Embeddings


문서 끝