commit 3f6e0050265633a697824f0d6020d4be3e4de0d5 Author: cherry2250 Date: Fri Oct 24 10:10:16 2025 +0900 초기 프로젝트 설정 및 설계 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ba319b4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# 프론트엔드 가이드 + +[Git 연동] + +- "pull" 명령어 입력 시 Git pull 명령을 수행하고 충돌이 있을 때 최신 파일로 병합 수행 +- "push" 또는 "푸시" 명령어 입력 시 git add, commit, push를 수행 +- Commit Message는 한글로 함 + +[URL링크 참조] + +- URL링크는 WebFetch가 아닌 'curl {URL} > claude/{filename}'명령으로 저장 +- 동일한 파일이 있으면 덮어 씀 +- 'claude'디렉토리가 없으면 생성하고 다운로드 +- 저장된 파일을 읽어 사용함 + +## 산출물 디렉토리 + +- 프로토타입: design/prototype/\* +- API명세서: design/api/\*.json +- UI/UX설계서: design/frontend/uiux-design.md +- 스타일가이드: design/frontend/style-guide.md +- 정보아키텍처: design/frontend/ia.md +- API매핑설계서: design/frontend/api-mapping.md +- 유저스토리: design/userstory.md + +## 가이드 + +- 프론트엔드설계가이드 + - 설명: 프론트엔드 설계 방법 안내 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/design/frontend-design.md + - 파일명: frontend-design.md +- 프론트엔드개발가이드 + - 설명: 프론트엔드 개발 가이드 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/develop/dev-frontend.md + - 파일명: dev-frontend.md +- 프론트엔드컨테이너이미지작성가이드 + - 설명: 프론트엔드 컨테이너 이미지 작성 가이드 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/build-image-front.md + - 파일명: build-image-front.md +- 프론트엔드컨테이너실행방법가이드 + - 설명: 프론트엔드 컨테이너 실행방법 가이드 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/run-container-guide-front.md + - 파일명: run-container-guide-front.md +- 프론트엔드배포가이드 + - 설명: 프론트엔드 서비스를 쿠버네티스 클러스터에 배포하는 가이드 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/deploy-k8s-front.md + - 파일명: deploy-k8s-front.md +- 프론트엔드Jenkins파이프라인작성가이드 + - 설명: 프론트엔드 서비스를 Jenkins를 이용하여 CI/CD하는 배포 가이드 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/deploy-jenkins-cicd-front.md + - 파일명: deploy-jenkins-cicd-front.md +- 프론트엔드GitHubActions파이프라인작성가이드 + - 설명: 프론트엔드 서비스를 GitHub Actions를 이용하여 CI/CD하는 배포 가이드 + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/deploy-actions-cicd-front.md + - 파일명: deploy-actions-cicd-front.md + +## 프롬프트 약어 + +### 역할 약어 + +- "@front": "--persona-front" +- "@devops": "--persona-devops" + +### 작업 약어 + +- "@complex-flag": --seq --c7 --uc --wave-mode auto --wave-strategy systematic --delegate auto + +- "@plan": --plan --think +- "@dev-front": /sc:implement @front --think-hard @complex-flag +- "@cicd": /sc:implement @devops --think @complex-flag +- "@document": /sc:document --think @scribe @complex-flag +- "@fix": /sc:troubleshoot --think @complex-flag +- "@estimate": /sc:estimate --think-hard @complex-flag +- "@improve": /sc:improve --think @complex-flag +- "@analyze": /sc:analyze --think --seq +- "@explain": /sc:explain --think --seq --answer-only + +## Lessons Learned + +**프론트엔드 개발 절차**: + +- 개발가이드의 "6. 각 페이지별 구현" 단계에서는 빌드 및 에러 해결까지만 수행 +- 개발서버(`npm run dev`) 실행은 항상 사용자가 직접 수행 +- 개발자는 빌드(`npm run build`) 성공까지만 확인하고 서버 실행을 사용자에게 요청 +- 개발자가 임의로 서버를 실행하고 테스트하지 않고 사용자 확인 후 진행 + +**프로토타입 분석 및 테스트**: + +- 프로토타입 HTML 파일은 반드시 Playwright MCP를 사용하여 모바일 화면(375x812)에서 확인 +- 프로토타입의 모든 인터랙션과 액션을 실제로 클릭하여 동작 확인 필요 + +**서비스 재배포 가이드** +서비스 수정 후 재배포 시 다음 절차를 따릅니다: + +1. 이미지 빌드: deployment/container/build-image.md 참조하여 빌드 +2. 이미지를 ACR형식으로 태깅 +3. 컨테이너 실행: deployment/container/run-container-guide.md의 '8. 재배포 방법' 참조하여 실행 + - 컨테이너 중단 + - 이미지 삭제 + - 컨테이너 실행 + +- 테스트는 사용자에게 요청 diff --git a/design/.DS_Store b/design/.DS_Store new file mode 100644 index 0000000..e1d54c9 Binary files /dev/null and b/design/.DS_Store differ diff --git a/design/aidata/매장_프로필_데이터.md b/design/aidata/매장_프로필_데이터.md new file mode 100644 index 0000000..dfd1d4d --- /dev/null +++ b/design/aidata/매장_프로필_데이터.md @@ -0,0 +1,1938 @@ +# 매장 프로필 데이터 샘플 + +**작성일**: 2025-10-21 +**버전**: 1.0 +**목적**: AI 기반 이벤트 자동 생성 서비스 학습 데이터 + +--- + +## 데이터 구조 + +각 매장 프로필은 다음 정보를 포함합니다: + +- **기본정보**: 매장ID, 매장명, 업종, 지역, 주소, 영업시간 +- **운영정보**: 직원 수, 좌석 수, 월 평균 매출, 객단가 +- **메뉴정보**: 주요 메뉴 및 가격 +- **타겟정보**: 주요 고객층, 상권 특성 +- **특성**: 강점 및 경쟁력 + +--- + +## 1. Korean Restaurant (한국 음식점) + +### 1-1. 정가네한정식 +```json +{ + "storeId": "KR001", + "storeName": "정가네한정식", + "category": "korean_restaurant", + "categoryKo": "한국 음식점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 테헤란로 123" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-21:00", + "holiday": "매주 일요일 휴무" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 45000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "한정식 A", "price": 15000}, + {"name": "한정식 B", "price": 20000}, + {"name": "불고기정식", "price": 12000}, + {"name": "갈비탕", "price": 13000} + ], + "targetCustomers": "직장인, 40-60대, 가족 모임", + "strongPoints": "전통 한정식, 정갈한 상차림, 점심 특선 메뉴", + "locationCharacter": "강남 업무지구, 점심 수요 높음" +} +``` + +### 1-2. 우리집밥상 +```json +{ + "storeId": "KR002", + "storeName": "우리집밥상", + "category": "korean_restaurant", + "categoryKo": "한국 음식점", + "location": { + "city": "부산", + "district": "해운대구", + "address": "부산시 해운대구 우동 456" + }, + "businessHours": { + "weekday": "10:00-21:00", + "weekend": "10:00-21:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 28000000, + "avgCheckSize": 11000, + "menu": [ + {"name": "김치찌개", "price": 9000}, + {"name": "된장찌개", "price": 9000}, + {"name": "제육볶음", "price": 10000}, + {"name": "생선구이", "price": 12000} + ], + "targetCustomers": "지역 주민, 30-50대, 1인 식사 고객", + "strongPoints": "가성비, 넉넉한 반찬, 빠른 서빙", + "locationCharacter": "주택가 상권, 단골 고객 많음" +} +``` + +### 1-3. 전주본가 +```json +{ + "storeId": "KR003", + "storeName": "전주본가", + "category": "korean_restaurant", + "categoryKo": "한국 음식점", + "location": { + "city": "서울", + "district": "중구", + "address": "서울시 중구 명동길 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "연중무휴" + }, + "staff": 8, + "seats": 60, + "monthlyRevenue": 72000000, + "avgCheckSize": 24000, + "menu": [ + {"name": "전주비빔밥", "price": 13000}, + {"name": "돌솥비빔밥", "price": 15000}, + {"name": "콩나물국밥", "price": 10000}, + {"name": "육회비빔밥", "price": 18000} + ], + "targetCustomers": "관광객, 외국인, 전 연령대", + "strongPoints": "전통 비빔밥 전문점, 관광명소 위치, 외국어 메뉴 제공", + "locationCharacter": "명동 관광지, 주말/공휴일 매출 높음" +} +``` + +--- + +## 2. Cafe (카페) + +### 2-1. 감성공간 +```json +{ + "storeId": "CF001", + "storeName": "감성공간", + "category": "cafe", + "categoryKo": "카페", + "location": { + "city": "서울", + "district": "성수동", + "address": "서울시 성동구 성수이로 234" + }, + "businessHours": { + "weekday": "09:00-22:00", + "weekend": "10:00-23:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 45, + "monthlyRevenue": 38000000, + "avgCheckSize": 8500, + "menu": [ + {"name": "아메리카노", "price": 4500}, + {"name": "카페라떼", "price": 5000}, + {"name": "크루아상", "price": 4000}, + {"name": "시그니처 케이크", "price": 7500} + ], + "targetCustomers": "20-30대 여성, 인스타그래머, 프리랜서", + "strongPoints": "감성 인테리어, 포토존, SNS 마케팅 강점", + "locationCharacter": "성수동 핫플레이스, 주말 방문객 많음" +} +``` + +### 2-2. 북카페 책그리고 +```json +{ + "storeId": "CF002", + "storeName": "북카페 책그리고", + "category": "cafe", + "categoryKo": "카페", + "location": { + "city": "대전", + "district": "유성구", + "address": "대전시 유성구 대학로 456" + }, + "businessHours": { + "weekday": "10:00-23:00", + "weekend": "10:00-23:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 2, + "seats": 30, + "monthlyRevenue": 18000000, + "avgCheckSize": 7000, + "menu": [ + {"name": "아메리카노", "price": 4000}, + {"name": "허브티", "price": 5000}, + {"name": "와플", "price": 6000}, + {"name": "샌드위치", "price": 7500} + ], + "targetCustomers": "대학생, 20-30대, 스터디 그룹", + "strongPoints": "조용한 분위기, 장시간 이용 가능, 도서 대여", + "locationCharacter": "대학가 상권, 시험 기간 매출 증가" +} +``` + +### 2-3. 더블랙커피 +```json +{ + "storeId": "CF003", + "storeName": "더블랙커피", + "category": "cafe", + "categoryKo": "카페", + "location": { + "city": "경기", + "district": "수원시", + "address": "경기도 수원시 팔달구 행궁로 789" + }, + "businessHours": { + "weekday": "08:00-20:00", + "weekend": "09:00-20:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 20, + "monthlyRevenue": 25000000, + "avgCheckSize": 6500, + "menu": [ + {"name": "아메리카노", "price": 3500}, + {"name": "카페라떼", "price": 4000}, + {"name": "핸드드립 커피", "price": 6000}, + {"name": "디저트 세트", "price": 8000} + ], + "targetCustomers": "직장인, 30-50대, 테이크아웃 고객", + "strongPoints": "합리적 가격, 커피 품질, 테이크아웃 활성화", + "locationCharacter": "수원 행궁동, 관광객 및 지역 주민 혼재" +} +``` + +--- + +## 3. Chinese Restaurant (중국 음식점) + +### 3-1. 용궁짜장 +```json +{ + "storeId": "CN001", + "storeName": "용궁짜장", + "category": "chinese_restaurant", + "categoryKo": "중국 음식점", + "location": { + "city": "서울", + "district": "영등포구", + "address": "서울시 영등포구 여의대로 123" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "12:00-21:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 35, + "monthlyRevenue": 32000000, + "avgCheckSize": 10000, + "menu": [ + {"name": "짜장면", "price": 6000}, + {"name": "짬뽕", "price": 7000}, + {"name": "탕수육", "price": 18000}, + {"name": "깐풍기", "price": 20000} + ], + "targetCustomers": "직장인, 가족, 배달 고객", + "strongPoints": "배달 빠름, 푸짐한 양, 점심 특선", + "locationCharacter": "여의도 업무지구, 배달 주문 많음" +} +``` + +### 3-2. 황제루 +```json +{ + "storeId": "CN002", + "storeName": "황제루", + "category": "chinese_restaurant", + "categoryKo": "중국 음식점", + "location": { + "city": "인천", + "district": "중구", + "address": "인천시 중구 차이나타운로 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-22:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 55000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "코스 A", "price": 35000}, + {"name": "코스 B", "price": 50000}, + {"name": "북경오리", "price": 45000}, + {"name": "마라샹궈", "price": 28000} + ], + "targetCustomers": "관광객, 30-50대, 단체 모임", + "strongPoints": "정통 중식, 단체석 완비, 관광지 위치", + "locationCharacter": "차이나타운, 주말 매출 높음" +} +``` + +### 3-3. 동네중국집 +```json +{ + "storeId": "CN003", + "storeName": "동네중국집", + "category": "chinese_restaurant", + "categoryKo": "중국 음식점", + "location": { + "city": "대구", + "district": "수성구", + "address": "대구시 수성구 범어동 789" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 3, + "seats": 20, + "monthlyRevenue": 24000000, + "avgCheckSize": 9000, + "menu": [ + {"name": "짜장면", "price": 5500}, + {"name": "짬뽕", "price": 6500}, + {"name": "우동", "price": 6000}, + {"name": "볶음밥", "price": 7000} + ], + "targetCustomers": "지역 주민, 1인 고객, 배달 고객", + "strongPoints": "빠른 배달, 가성비, 단골 고객 많음", + "locationCharacter": "주택가 상권, 저녁 배달 많음" +} +``` + +--- + +## 4. Japanese Restaurant (일본 음식점) + +### 4-1. 스시마루 +```json +{ + "storeId": "JP001", + "storeName": "스시마루", + "category": "japanese_restaurant", + "categoryKo": "일본 음식점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 논현로 234" + }, + "businessHours": { + "weekday": "12:00-22:00", + "weekend": "12:00-23:00", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 30, + "monthlyRevenue": 62000000, + "avgCheckSize": 35000, + "menu": [ + {"name": "초밥 세트 A", "price": 28000}, + {"name": "초밥 세트 B", "price": 45000}, + {"name": "회덮밥", "price": 18000}, + {"name": "우동정식", "price": 15000} + ], + "targetCustomers": "30-40대, 고소득층, 데이트 커플", + "strongPoints": "신선한 재료, 오마카세, 프리미엄 이미지", + "locationCharacter": "강남 핵심 상권, 저녁 매출 높음" +} +``` + +### 4-2. 라멘집 +```json +{ + "storeId": "JP002", + "storeName": "라멘집", + "category": "ramen_restaurant", + "categoryKo": "라멘 전문점", + "location": { + "city": "서울", + "district": "홍대", + "address": "서울시 마포구 홍대입구역 456" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-23:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 18, + "monthlyRevenue": 35000000, + "avgCheckSize": 12000, + "menu": [ + {"name": "돈코츠라멘", "price": 9500}, + {"name": "쇼유라멘", "price": 9000}, + {"name": "차슈덮밥", "price": 8000}, + {"name": "교자", "price": 6000} + ], + "targetCustomers": "20-30대, 대학생, 직장인", + "strongPoints": "진한 국물, 빠른 회전율, 1인 식사 최적화", + "locationCharacter": "홍대 번화가, 점심·저녁 피크타임 웨이팅" +} +``` + +### 4-3. 텐동야 +```json +{ + "storeId": "JP003", + "storeName": "텐동야", + "category": "japanese_restaurant", + "categoryKo": "일본 음식점", + "location": { + "city": "부산", + "district": "서면", + "address": "부산시 부산진구 서면로 789" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "매주 일요일 휴무" + }, + "staff": 4, + "seats": 25, + "monthlyRevenue": 28000000, + "avgCheckSize": 11000, + "menu": [ + {"name": "텐동", "price": 9500}, + {"name": "특텐동", "price": 12000}, + {"name": "카츠동", "price": 10000}, + {"name": "우동세트", "price": 13000} + ], + "targetCustomers": "20-40대, 직장인, 점심 고객", + "strongPoints": "빠른 서빙, 합리적 가격, 점심 특선", + "locationCharacter": "서면 상업지구, 점심 매출 집중" +} +``` + +--- + +## 5. Barbecue Restaurant (바베큐 음식점) + +### 5-1. 한우마을 +```json +{ + "storeId": "BBQ001", + "storeName": "한우마을", + "category": "barbecue_restaurant", + "categoryKo": "바베큐 음식점", + "location": { + "city": "서울", + "district": "송파구", + "address": "서울시 송파구 올림픽로 123" + }, + "businessHours": { + "weekday": "17:00-24:00", + "weekend": "16:00-24:00", + "holiday": "연중무휴" + }, + "staff": 7, + "seats": 60, + "monthlyRevenue": 85000000, + "avgCheckSize": 45000, + "menu": [ + {"name": "한우 등심 (100g)", "price": 25000}, + {"name": "한우 안심 (100g)", "price": 28000}, + {"name": "갈비살 (100g)", "price": 22000}, + {"name": "육회", "price": 35000} + ], + "targetCustomers": "30-50대, 가족 모임, 회식", + "strongPoints": "1++등급 한우, 넓은 룸, 주차 편리", + "locationCharacter": "잠실 상권, 저녁·주말 예약 필수" +} +``` + +### 5-2. 돼지랑소랑 +```json +{ + "storeId": "BBQ002", + "storeName": "돼지랑소랑", + "category": "barbecue_restaurant", + "categoryKo": "바베큐 음식점", + "location": { + "city": "대전", + "district": "서구", + "address": "대전시 서구 둔산로 456" + }, + "businessHours": { + "weekday": "16:00-23:00", + "weekend": "15:00-23:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 35, + "monthlyRevenue": 42000000, + "avgCheckSize": 28000, + "menu": [ + {"name": "삼겹살 (200g)", "price": 12000}, + {"name": "목살 (200g)", "price": 13000}, + {"name": "항정살 (200g)", "price": 15000}, + {"name": "소고기 (100g)", "price": 18000} + ], + "targetCustomers": "20-40대, 회식, 모임", + "strongPoints": "무한 리필 반찬, 직접 구워주는 서비스, 저렴한 가격", + "locationCharacter": "둔산동 상권, 저녁 매출 집중" +} +``` + +### 5-3. 숯불갈비 +```json +{ + "storeId": "BBQ003", + "storeName": "숯불갈비", + "category": "barbecue_restaurant", + "categoryKo": "바베큐 음식점", + "location": { + "city": "수원", + "district": "장안구", + "address": "경기도 수원시 장안구 정조로 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 48000000, + "avgCheckSize": 32000, + "menu": [ + {"name": "LA갈비", "price": 35000}, + {"name": "양념갈비", "price": 32000}, + {"name": "소갈비살", "price": 38000}, + {"name": "냉면", "price": 9000} + ], + "targetCustomers": "가족, 40-60대, 지역 주민", + "strongPoints": "숯불 직화, 오래된 단골 고객, 점심 갈비탕", + "locationCharacter": "수원 구도심, 점심·저녁 모두 활성화" +} +``` + +--- + +## 6. Chicken Restaurant (치킨 전문점) + +### 6-1. 자담치킨 역삼점 +```json +{ + "storeId": "CK001", + "storeName": "자담치킨 역삼점", + "category": "chicken_restaurant", + "categoryKo": "치킨 전문점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 역삼동 123" + }, + "businessHours": { + "weekday": "15:00-03:00", + "weekend": "15:00-04:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 0, + "monthlyRevenue": 35000000, + "avgCheckSize": 24000, + "menu": [ + {"name": "후라이드", "price": 18000}, + {"name": "양념치킨", "price": 19000}, + {"name": "간장치킨", "price": 19000}, + {"name": "순살치킨", "price": 20000} + ], + "targetCustomers": "20-40대, 배달 주문 고객, 야간 주문", + "strongPoints": "배달 전문, 빠른 배달, 마케팅 지원", + "locationCharacter": "역삼 상업지구, 배달 주문 95%" +} +``` + +### 6-2. 크치치킨 +```json +{ + "storeId": "CK002", + "storeName": "크치치킨", + "category": "chicken_restaurant", + "categoryKo": "치킨 전문점", + "location": { + "city": "서울", + "district": "관악구", + "address": "서울시 관악구 신림동 456" + }, + "businessHours": { + "weekday": "16:00-02:00", + "weekend": "14:00-03:00", + "holiday": "연중무휴" + }, + "staff": 2, + "seats": 15, + "monthlyRevenue": 28000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "순살치킨 2마리", "price": 20000}, + {"name": "크치데이 특가", "price": 10000}, + {"name": "양념치킨", "price": 18000}, + {"name": "후라이드", "price": 17000} + ], + "targetCustomers": "대학생, 20-30대, 지역 주민", + "strongPoints": "감성 인테리어, 저렴한 가격, 크치데이 이벤트", + "locationCharacter": "대학가 상권, 저녁·야간 매출 집중" +} +``` + +### 6-3. 동네치킨 +```json +{ + "storeId": "CK003", + "storeName": "동네치킨", + "category": "chicken_restaurant", + "categoryKo": "치킨 전문점", + "location": { + "city": "인천", + "district": "남동구", + "address": "인천시 남동구 구월동 789" + }, + "businessHours": { + "weekday": "15:00-01:00", + "weekend": "15:00-02:00", + "holiday": "연중무휴" + }, + "staff": 2, + "seats": 0, + "monthlyRevenue": 22000000, + "avgCheckSize": 20000, + "menu": [ + {"name": "후라이드", "price": 16000}, + {"name": "양념치킨", "price": 17000}, + {"name": "반반치킨", "price": 17000}, + {"name": "치킨무", "price": 0} + ], + "targetCustomers": "지역 주민, 가족, 배달 고객", + "strongPoints": "단골 고객, 신속 배달, 프렌치프라이 서비스", + "locationCharacter": "주택가 상권, 저녁 주문 많음" +} +``` + +--- + +## 7. Pizza Restaurant (피자 전문점) + +### 7-1. 피자스쿨 +```json +{ + "storeId": "PZ001", + "storeName": "피자스쿨", + "category": "pizza_restaurant", + "categoryKo": "피자 전문점", + "location": { + "city": "서울", + "district": "노원구", + "address": "서울시 노원구 상계동 123" + }, + "businessHours": { + "weekday": "11:00-23:00", + "weekend": "11:00-23:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 10, + "monthlyRevenue": 26000000, + "avgCheckSize": 15000, + "menu": [ + {"name": "콤비네이션 피자", "price": 12000}, + {"name": "불고기 피자", "price": 13000}, + {"name": "포테이토 피자", "price": 11000}, + {"name": "치즈 크러스트", "price": 2000} + ], + "targetCustomers": "가족, 학생, 배달 고객", + "strongPoints": "합리적 가격, 1+1 이벤트, 빠른 배달", + "locationCharacter": "주택가 상권, 주말 주문 많음" +} +``` + +### 7-2. 이태리아노 +```json +{ + "storeId": "PZ002", + "storeName": "이태리아노", + "category": "pizza_restaurant", + "categoryKo": "피자 전문점", + "location": { + "city": "서울", + "district": "이태원", + "address": "서울시 용산구 이태원로 456" + }, + "businessHours": { + "weekday": "12:00-23:00", + "weekend": "12:00-24:00", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 52000000, + "avgCheckSize": 32000, + "menu": [ + {"name": "마르게리타", "price": 22000}, + {"name": "디아볼라", "price": 25000}, + {"name": "콰트로 포르마지", "price": 28000}, + {"name": "파스타", "price": 18000} + ], + "targetCustomers": "20-40대, 데이트 커플, 외국인", + "strongPoints": "화덕 피자, 정통 이탈리안, 와인 페어링", + "locationCharacter": "이태원 핫플레이스, 저녁·주말 매출 높음" +} +``` + +### 7-3. 파파존스 +```json +{ + "storeId": "PZ003", + "storeName": "파파존스", + "category": "pizza_restaurant", + "categoryKo": "피자 전문점", + "location": { + "city": "부산", + "district": "해운대구", + "address": "부산시 해운대구 센텀시티 789" + }, + "businessHours": { + "weekday": "11:00-23:00", + "weekend": "11:00-24:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 20, + "monthlyRevenue": 38000000, + "avgCheckSize": 28000, + "menu": [ + {"name": "슈퍼파파스", "price": 25000}, + {"name": "하와이안", "price": 23000}, + {"name": "페퍼로니", "price": 24000}, + {"name": "치즈스틱", "price": 8000} + ], + "targetCustomers": "가족, 20-40대, 파티 주문", + "strongPoints": "프리미엄 브랜드, 다양한 사이드 메뉴, 앱 주문 편리", + "locationCharacter": "센텀시티 상권, 주말 매출 높음" +} +``` + +--- + +## 8. Bakery (제과점) + +### 8-1. 아뜨베 베이커리 +```json +{ + "storeId": "BK001", + "storeName": "아뜨베 베이커리", + "category": "bakery", + "categoryKo": "제과점", + "location": { + "city": "서울", + "district": "성동구", + "address": "서울시 성동구 왕십리로 123" + }, + "businessHours": { + "weekday": "00:00-24:00", + "weekend": "00:00-24:00", + "holiday": "24시간 연중무휴" + }, + "staff": 6, + "seats": 25, + "monthlyRevenue": 65000000, + "avgCheckSize": 12000, + "menu": [ + {"name": "크루아상", "price": 3500}, + {"name": "소금빵", "price": 2500}, + {"name": "베이글", "price": 3000}, + {"name": "케이크", "price": 35000} + ], + "targetCustomers": "전 연령대, 야간 고객, 배달 주문", + "strongPoints": "24시간 운영, 신선한 빵, 다양한 메뉴", + "locationCharacter": "왕십리 역세권, 야간 매출 30%" +} +``` + +### 8-2. 동네빵집 +```json +{ + "storeId": "BK002", + "storeName": "동네빵집", + "category": "bakery", + "categoryKo": "제과점", + "location": { + "city": "대구", + "district": "중구", + "address": "대구시 중구 동성로 456" + }, + "businessHours": { + "weekday": "07:00-22:00", + "weekend": "08:00-22:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 0, + "monthlyRevenue": 24000000, + "avgCheckSize": 8000, + "menu": [ + {"name": "식빵", "price": 4000}, + {"name": "단팥빵", "price": 2000}, + {"name": "크림빵", "price": 2500}, + {"name": "생일 케이크", "price": 25000} + ], + "targetCustomers": "지역 주민, 40-60대, 아침 고객", + "strongPoints": "오랜 역사, 단골 고객, 합리적 가격", + "locationCharacter": "동성로 상권, 아침 매출 높음" +} +``` + +### 8-3. 파리바게트 +```json +{ + "storeId": "BK003", + "storeName": "파리바게트", + "category": "bakery", + "categoryKo": "제과점", + "location": { + "city": "인천", + "district": "연수구", + "address": "인천시 연수구 송도동 789" + }, + "businessHours": { + "weekday": "07:00-23:00", + "weekend": "07:00-23:00", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 15, + "monthlyRevenue": 48000000, + "avgCheckSize": 10000, + "menu": [ + {"name": "크루아상", "price": 3000}, + {"name": "소보로", "price": 2500}, + {"name": "샌드위치", "price": 4500}, + {"name": "케이크", "price": 30000} + ], + "targetCustomers": "직장인, 학생, 가족", + "strongPoints": "브랜드 인지도, 다양한 제품, 멤버십 혜택", + "locationCharacter": "송도 신도시, 아침·저녁 매출 집중" +} +``` + +--- + +## 9. Coffee Shop (커피숍) + +### 9-1. 메가커피 +```json +{ + "storeId": "CS001", + "storeName": "메가커피", + "category": "coffee_shop", + "categoryKo": "커피숍", + "location": { + "city": "서울", + "district": "강동구", + "address": "서울시 강동구 천호대로 123" + }, + "businessHours": { + "weekday": "07:00-22:00", + "weekend": "08:00-22:00", + "holiday": "연중무휴" + }, + "staff": 2, + "seats": 10, + "monthlyRevenue": 18000000, + "avgCheckSize": 4500, + "menu": [ + {"name": "아메리카노", "price": 1500}, + {"name": "카페라떼", "price": 2000}, + {"name": "바닐라라떼", "price": 2500}, + {"name": "에이드", "price": 3000} + ], + "targetCustomers": "학생, 20-30대, 테이크아웃 고객", + "strongPoints": "저렴한 가격, 빠른 서비스, 앱 주문", + "locationCharacter": "천호역 역세권, 출퇴근 시간 매출 높음" +} +``` + +### 9-2. 컴포즈커피 +```json +{ + "storeId": "CS002", + "storeName": "컴포즈커피", + "category": "coffee_shop", + "categoryKo": "커피숍", + "location": { + "city": "부산", + "district": "사상구", + "address": "부산시 사상구 주례동 456" + }, + "businessHours": { + "weekday": "07:30-22:00", + "weekend": "08:00-22:00", + "holiday": "연중무휴" + }, + "staff": 2, + "seats": 8, + "monthlyRevenue": 16000000, + "avgCheckSize": 4000, + "menu": [ + {"name": "아메리카노", "price": 1500}, + {"name": "카페라떼", "price": 2000}, + {"name": "딸기라떼", "price": 3500}, + {"name": "한정판 음료", "price": 4000} + ], + "targetCustomers": "20-30대, 학생, 직장인", + "strongPoints": "가성비, 캐릭터 협업, 모바일 주문", + "locationCharacter": "주례동 상권, 테이크아웃 80%" +} +``` + +### 9-3. 스타벅스 +```json +{ + "storeId": "CS003", + "storeName": "스타벅스", + "category": "coffee_shop", + "categoryKo": "커피숍", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 테헤란로 789" + }, + "businessHours": { + "weekday": "07:00-22:00", + "weekend": "08:00-22:00", + "holiday": "연중무휴" + }, + "staff": 8, + "seats": 60, + "monthlyRevenue": 85000000, + "avgCheckSize": 8500, + "menu": [ + {"name": "아메리카노", "price": 4500}, + {"name": "카페라떼", "price": 5500}, + {"name": "프라푸치노", "price": 6500}, + {"name": "케이크", "price": 7000} + ], + "targetCustomers": "직장인, 30-40대, 회의 고객", + "strongPoints": "브랜드 파워, 넓은 공간, 프리미엄 이미지", + "locationCharacter": "강남 업무지구, 오전·점심 매출 높음" +} +``` + +--- + +## 10. American Restaurant (미국 음식점) + +### 10-1. 버거킹 +```json +{ + "storeId": "AM001", + "storeName": "버거킹", + "category": "american_restaurant", + "categoryKo": "미국 음식점", + "location": { + "city": "서울", + "district": "종로구", + "address": "서울시 종로구 종각역 123" + }, + "businessHours": { + "weekday": "09:00-23:00", + "weekend": "09:00-23:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 62000000, + "avgCheckSize": 9500, + "menu": [ + {"name": "와퍼", "price": 6900}, + {"name": "치즈와퍼", "price": 7500}, + {"name": "롱치킨버거", "price": 6500}, + {"name": "세트 메뉴", "price": 9900} + ], + "targetCustomers": "10-30대, 학생, 직장인", + "strongPoints": "브랜드 인지도, 빠른 서비스, 앱 할인", + "locationCharacter": "종각역 역세권, 점심 시간 붐빔" +} +``` + +### 10-2. 쉐이크쉑 +```json +{ + "storeId": "AM002", + "storeName": "쉐이크쉑", + "category": "hamburger_restaurant", + "categoryKo": "햄버거 전문점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 강남대로 456" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "연중무휴" + }, + "staff": 10, + "seats": 70, + "monthlyRevenue": 125000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "쉑버거", "price": 9900}, + {"name": "스모크쉑", "price": 11900}, + {"name": "치킨버거", "price": 10900}, + {"name": "쉐이크", "price": 6900} + ], + "targetCustomers": "20-40대, 프리미엄 선호 고객", + "strongPoints": "프리미엄 재료, 매장 분위기, 브랜드 이미지", + "locationCharacter": "강남 핵심 상권, 주말 웨이팅 발생" +} +``` + +### 10-3. 맥도날드 +```json +{ + "storeId": "AM003", + "storeName": "맥도날드", + "category": "fast_food_restaurant", + "categoryKo": "패스트푸드점", + "location": { + "city": "인천", + "district": "부평구", + "address": "인천시 부평구 부평대로 789" + }, + "businessHours": { + "weekday": "00:00-24:00", + "weekend": "00:00-24:00", + "holiday": "24시간 연중무휴" + }, + "staff": 15, + "seats": 80, + "monthlyRevenue": 142000000, + "avgCheckSize": 10500, + "menu": [ + {"name": "빅맥", "price": 6400}, + {"name": "1955 버거", "price": 8900}, + {"name": "치즈버거", "price": 4500}, + {"name": "해피밀", "price": 6500} + ], + "targetCustomers": "전 연령대, 가족, 학생", + "strongPoints": "24시간 운영, 드라이브스루, 맥딜리버리", + "locationCharacter": "부평역 역세권, 24시간 고른 매출" +} +``` + +--- + +## 11. Italian Restaurant (이탈리아 음식점) + +### 11-1. 베네치아 +```json +{ + "storeId": "IT001", + "storeName": "베네치아", + "category": "italian_restaurant", + "categoryKo": "이탈리아 음식점", + "location": { + "city": "서울", + "district": "용산구", + "address": "서울시 용산구 한남동 123" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-22:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 7, + "seats": 45, + "monthlyRevenue": 68000000, + "avgCheckSize": 42000, + "menu": [ + {"name": "까르보나라", "price": 22000}, + {"name": "알리오올리오", "price": 20000}, + {"name": "리조또", "price": 25000}, + {"name": "스테이크", "price": 45000} + ], + "targetCustomers": "30-50대, 데이트, 기념일", + "strongPoints": "정통 이탈리안, 와인 리스트, 낭만적 분위기", + "locationCharacter": "한남동 고급 상권, 저녁 예약 필수" +} +``` + +### 11-2. 파스타공장 +```json +{ + "storeId": "IT002", + "storeName": "파스타공장", + "category": "italian_restaurant", + "categoryKo": "이탈리아 음식점", + "location": { + "city": "대전", + "district": "유성구", + "address": "대전시 유성구 봉명동 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 32000000, + "avgCheckSize": 15000, + "menu": [ + {"name": "토마토 파스타", "price": 12000}, + {"name": "크림 파스타", "price": 13000}, + {"name": "로제 파스타", "price": 14000}, + {"name": "필라프", "price": 11000} + ], + "targetCustomers": "20-30대, 대학생, 데이트", + "strongPoints": "가성비, 푸짐한 양, 대학가 위치", + "locationCharacter": "대학가 상권, 점심·저녁 매출 균등" +} +``` + +### 11-3. 트라토리아 +```json +{ + "storeId": "IT003", + "storeName": "트라토리아", + "category": "italian_restaurant", + "categoryKo": "이탈리아 음식점", + "location": { + "city": "부산", + "district": "해운대구", + "address": "부산시 해운대구 마린시티 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 58000000, + "avgCheckSize": 38000, + "menu": [ + {"name": "해산물 파스타", "price": 28000}, + {"name": "피자 마르게리타", "price": 24000}, + {"name": "알리오올리오", "price": 22000}, + {"name": "와인", "price": 45000} + ], + "targetCustomers": "30-50대, 가족, 관광객", + "strongPoints": "오션뷰, 신선한 해산물, 분위기", + "locationCharacter": "마린시티 상권, 저녁·주말 매출 높음" +} +``` + +--- + +## 12. Thai Restaurant (태국 음식점) + +### 12-1. 쏨땀하우스 +```json +{ + "storeId": "TH001", + "storeName": "쏨땀하우스", + "category": "thai_restaurant", + "categoryKo": "태국 음식점", + "location": { + "city": "서울", + "district": "이태원", + "address": "서울시 용산구 이태원로 123" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-22:00", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 35, + "monthlyRevenue": 42000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "팟타이", "price": 12000}, + {"name": "쏨땀", "price": 10000}, + {"name": "똠얌꿍", "price": 14000}, + {"name": "그린커리", "price": 15000} + ], + "targetCustomers": "20-40대, 외국인, 맛집 탐방객", + "strongPoints": "정통 태국 요리, 태국인 셰프, 매운 맛 단계 조절", + "locationCharacter": "이태원 다문화 상권, 저녁 매출 높음" +} +``` + +### 12-2. 방콕키친 +```json +{ + "storeId": "TH002", + "storeName": "방콕키친", + "category": "thai_restaurant", + "categoryKo": "태국 음식점", + "location": { + "city": "경기", + "district": "성남시", + "address": "경기도 성남시 분당구 야탑동 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "매주 일요일 휴무" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 26000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "팟타이", "price": 11000}, + {"name": "카오팟", "price": 10000}, + {"name": "똠양꿍", "price": 13000}, + {"name": "망고 스티키라이스", "price": 8000} + ], + "targetCustomers": "20-40대, 가족, 지역 주민", + "strongPoints": "합리적 가격, 한국인 입맛 고려, 점심 특선", + "locationCharacter": "분당 주택가, 점심 매출 집중" +} +``` + +### 12-3. 타이타닉 +```json +{ + "storeId": "TH003", + "storeName": "타이타닉", + "category": "thai_restaurant", + "categoryKo": "태국 음식점", + "location": { + "city": "대구", + "district": "중구", + "address": "대구시 중구 동성로 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 32000000, + "avgCheckSize": 16000, + "menu": [ + {"name": "팟타이", "price": 12000}, + {"name": "뿌팟퐁커리", "price": 18000}, + {"name": "똠얌꿍", "price": 14000}, + {"name": "코코넛 아이스크림", "price": 6000} + ], + "targetCustomers": "20-30대, 데이트, SNS 고객", + "strongPoints": "인스타그래머블 메뉴, 분위기, 다양한 메뉴", + "locationCharacter": "동성로 번화가, 저녁 매출 높음" +} +``` + +--- + +## 13. Vietnamese Restaurant (베트남 음식점) + +### 13-1. 포하노이 +```json +{ + "storeId": "VN001", + "storeName": "포하노이", + "category": "vietnamese_restaurant", + "categoryKo": "베트남 음식점", + "location": { + "city": "서울", + "district": "영등포구", + "address": "서울시 영등포구 대림동 123" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 28000000, + "avgCheckSize": 11000, + "menu": [ + {"name": "쌀국수", "price": 9000}, + {"name": "분짜", "price": 10000}, + {"name": "반미", "price": 7000}, + {"name": "월남쌈", "price": 12000} + ], + "targetCustomers": "베트남인, 20-40대, 다문화 고객", + "strongPoints": "정통 베트남 맛, 베트남인 셰프, 저렴한 가격", + "locationCharacter": "대림동 차이나타운, 다문화 상권" +} +``` + +### 13-2. 미스사이공 +```json +{ + "storeId": "VN002", + "storeName": "미스사이공", + "category": "vietnamese_restaurant", + "categoryKo": "베트남 음식점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 역삼동 456" + }, + "businessHours": { + "weekday": "11:00-21:30", + "weekend": "11:00-21:30", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 38000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "쌀국수", "price": 11000}, + {"name": "분짜", "price": 13000}, + {"name": "반미", "price": 8000}, + {"name": "베트남 커피", "price": 5000} + ], + "targetCustomers": "직장인, 20-40대, 점심 고객", + "strongPoints": "깔끔한 인테리어, 빠른 서비스, 직장인 특화", + "locationCharacter": "역삼 업무지구, 점심 매출 70%" +} +``` + +### 13-3. 포타운 +```json +{ + "storeId": "VN003", + "storeName": "포타운", + "category": "vietnamese_restaurant", + "categoryKo": "베트남 음식점", + "location": { + "city": "부산", + "district": "진구", + "address": "부산시 부산진구 서면로 789" + }, + "businessHours": { + "weekday": "10:00-21:00", + "weekend": "10:00-21:00", + "holiday": "매주 일요일 휴무" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 22000000, + "avgCheckSize": 10000, + "menu": [ + {"name": "쌀국수", "price": 8500}, + {"name": "분짜", "price": 9500}, + {"name": "반미", "price": 6500}, + {"name": "생춘권", "price": 8000} + ], + "targetCustomers": "20-30대, 학생, 가성비 선호 고객", + "strongPoints": "가성비, 빠른 서빙, 배달 가능", + "locationCharacter": "서면 상권, 점심·저녁 균등" +} +``` + +--- + +## 14. Mexican Restaurant (멕시코 음식점) + +### 14-1. 타코벨 +```json +{ + "storeId": "MX001", + "storeName": "타코벨", + "category": "mexican_restaurant", + "categoryKo": "멕시코 음식점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 강남대로 123" + }, + "businessHours": { + "weekday": "10:00-23:00", + "weekend": "10:00-23:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 40, + "monthlyRevenue": 52000000, + "avgCheckSize": 12000, + "menu": [ + {"name": "크런치랩 슈프림", "price": 7500}, + {"name": "치즈 퀘사디아", "price": 6500}, + {"name": "부리또", "price": 8000}, + {"name": "나초스", "price": 5500} + ], + "targetCustomers": "10-30대, 학생, 직장인", + "strongPoints": "브랜드 인지도, 합리적 가격, 독특한 메뉴", + "locationCharacter": "강남역 역세권, 점심·저녁 피크타임" +} +``` + +### 14-2. 구스토타코 +```json +{ + "storeId": "MX002", + "storeName": "구스토타코", + "category": "mexican_restaurant", + "categoryKo": "멕시코 음식점", + "location": { + "city": "서울", + "district": "이태원", + "address": "서울시 용산구 이태원로 456" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-23:00", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 35, + "monthlyRevenue": 42000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "타코 세트", "price": 18000}, + {"name": "퀘사디아", "price": 16000}, + {"name": "부리또", "price": 15000}, + {"name": "마르가리타", "price": 12000} + ], + "targetCustomers": "20-40대, 외국인, 술자리", + "strongPoints": "정통 멕시칸, 다양한 술, 분위기", + "locationCharacter": "이태원 핫플레이스, 저녁 매출 80%" +} +``` + +### 14-3. 엘본초 +```json +{ + "storeId": "MX003", + "storeName": "엘본초", + "category": "mexican_restaurant", + "categoryKo": "멕시코 음식점", + "location": { + "city": "서울", + "district": "홍대", + "address": "서울시 마포구 홍대입구역 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-23:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 35000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "부리또", "price": 12000}, + {"name": "나초스", "price": 10000}, + {"name": "퀘사디아", "price": 11000}, + {"name": "타코 세트", "price": 15000} + ], + "targetCustomers": "20-30대, 대학생, 데이트", + "strongPoints": "가성비, SNS 마케팅, 홍대 위치", + "locationCharacter": "홍대 번화가, 저녁·주말 매출 높음" +} +``` + +--- + +## 15. Seafood Restaurant (해산물 음식점) + +### 15-1. 바다마을 +```json +{ + "storeId": "SF001", + "storeName": "바다마을", + "category": "seafood_restaurant", + "categoryKo": "해산물 음식점", + "location": { + "city": "부산", + "district": "기장군", + "address": "부산시 기장군 일광면 해안로 123" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "연중무휴" + }, + "staff": 8, + "seats": 80, + "monthlyRevenue": 95000000, + "avgCheckSize": 45000, + "menu": [ + {"name": "회 (중)", "price": 60000}, + {"name": "회 (대)", "price": 90000}, + {"name": "매운탕", "price": 15000}, + {"name": "해물찜", "price": 50000} + ], + "targetCustomers": "가족, 40-60대, 관광객", + "strongPoints": "신선한 해산물, 오션뷰, 주차 편리", + "locationCharacter": "해변 관광지, 주말 매출 높음" +} +``` + +### 15-2. 횟집 수산 +```json +{ + "storeId": "SF002", + "storeName": "횟집 수산", + "category": "seafood_restaurant", + "categoryKo": "해산물 음식점", + "location": { + "city": "서울", + "district": "노량진", + "address": "서울시 동작구 노량진수산시장 456" + }, + "businessHours": { + "weekday": "09:00-23:00", + "weekend": "09:00-24:00", + "holiday": "연중무휴" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 68000000, + "avgCheckSize": 38000, + "menu": [ + {"name": "회 (소)", "price": 40000}, + {"name": "회 (중)", "price": 60000}, + {"name": "초밥", "price": 25000}, + {"name": "매운탕", "price": 12000} + ], + "targetCustomers": "전 연령대, 회식, 모임", + "strongPoints": "시장 내 위치, 신선함, 합리적 가격", + "locationCharacter": "노량진수산시장, 저녁 매출 높음" +} +``` + +### 15-3. 어촌 +```json +{ + "storeId": "SF003", + "storeName": "어촌", + "category": "seafood_restaurant", + "categoryKo": "해산물 음식점", + "location": { + "city": "인천", + "district": "남구", + "address": "인천시 남구 소래포구 789" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 60, + "monthlyRevenue": 72000000, + "avgCheckSize": 42000, + "menu": [ + {"name": "꽃게탕", "price": 40000}, + {"name": "조개구이", "price": 30000}, + {"name": "회 정식", "price": 35000}, + {"name": "칼국수", "price": 8000} + ], + "targetCustomers": "가족, 관광객, 40-60대", + "strongPoints": "포구 직송, 신선한 재료, 다양한 메뉴", + "locationCharacter": "소래포구 관광지, 주말 성수기" +} +``` + +--- + +## 16. Dessert Shop (디저트 전문점) + +### 16-1. 설빙 +```json +{ + "storeId": "DS001", + "storeName": "설빙", + "category": "dessert_shop", + "categoryKo": "디저트 전문점", + "location": { + "city": "서울", + "district": "명동", + "address": "서울시 중구 명동길 123" + }, + "businessHours": { + "weekday": "11:00-23:00", + "weekend": "11:00-24:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 58000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "인절미 설빙", "price": 14000}, + {"name": "망고 설빙", "price": 16000}, + {"name": "팥빙수", "price": 12000}, + {"name": "음료", "price": 6000} + ], + "targetCustomers": "관광객, 20-30대, 외국인", + "strongPoints": "브랜드 인지도, 한국 전통 디저트, 관광지 위치", + "locationCharacter": "명동 관광지, 여름 매출 집중" +} +``` + +### 16-2. 마카롱하우스 +```json +{ + "storeId": "DS002", + "storeName": "마카롱하우스", + "category": "dessert_shop", + "categoryKo": "디저트 전문점", + "location": { + "city": "서울", + "district": "청담동", + "address": "서울시 강남구 청담동 456" + }, + "businessHours": { + "weekday": "10:00-21:00", + "weekend": "10:00-21:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 3, + "seats": 15, + "monthlyRevenue": 32000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "마카롱 5개", "price": 15000}, + {"name": "마카롱 10개", "price": 28000}, + {"name": "케이크", "price": 40000}, + {"name": "음료", "price": 7000} + ], + "targetCustomers": "20-40대 여성, 선물 구매 고객", + "strongPoints": "프리미엄 디저트, 감성 포장, SNS 마케팅", + "locationCharacter": "청담동 고급 상권, 주말 매출 높음" +} +``` + +### 16-3. 와플대학 +```json +{ + "storeId": "DS003", + "storeName": "와플대학", + "category": "dessert_shop", + "categoryKo": "디저트 전문점", + "location": { + "city": "서울", + "district": "신촌", + "address": "서울시 서대문구 신촌역 789" + }, + "businessHours": { + "weekday": "10:00-23:00", + "weekend": "10:00-24:00", + "holiday": "연중무휴" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 28000000, + "avgCheckSize": 9000, + "menu": [ + {"name": "벨기에 와플", "price": 6500}, + {"name": "딸기 와플", "price": 8500}, + {"name": "아이스크림 와플", "price": 9500}, + {"name": "음료", "price": 5000} + ], + "targetCustomers": "대학생, 10-20대, 데이트", + "strongPoints": "가성비, 대학가 위치, 다양한 토핑", + "locationCharacter": "신촌 대학가, 저녁 매출 높음" +} +``` + +--- + +## 17. Brunch Restaurant (브런치 전문점) + +### 17-1. 엘리제 +```json +{ + "storeId": "BR001", + "storeName": "엘리제", + "category": "brunch_restaurant", + "categoryKo": "브런치 전문점", + "location": { + "city": "서울", + "district": "연남동", + "address": "서울시 마포구 연남동 123" + }, + "businessHours": { + "weekday": "09:00-17:00", + "weekend": "09:00-18:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 42000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "에그베네딕트", "price": 16000}, + {"name": "팬케이크", "price": 14000}, + {"name": "아보카도 토스트", "price": 15000}, + {"name": "브런치 세트", "price": 22000} + ], + "targetCustomers": "20-30대 여성, 브런치 애호가", + "strongPoints": "감성 인테리어, 인스타그래머블, 웨이팅 맛집", + "locationCharacter": "연남동 핫플레이스, 주말 웨이팅 2시간" +} +``` + +### 17-2. 에그슬럿 +```json +{ + "storeId": "BR002", + "storeName": "에그슬럿", + "category": "brunch_restaurant", + "categoryKo": "브런치 전문점", + "location": { + "city": "서울", + "district": "잠실", + "address": "서울시 송파구 잠실 롯데월드몰 456" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "연중무휴" + }, + "staff": 8, + "seats": 60, + "monthlyRevenue": 95000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "에그슬럿", "price": 14000}, + {"name": "페어팩스", "price": 16000}, + {"name": "베이컨 에그", "price": 15000}, + {"name": "음료", "price": 5000} + ], + "targetCustomers": "20-40대, 가족, 쇼핑 고객", + "strongPoints": "브랜드 인지도, 쇼핑몰 위치, 대기 시스템", + "locationCharacter": "롯데월드몰, 주말 오전 웨이팅" +} +``` + +### 17-3. 더플레이트 +```json +{ + "storeId": "BR003", + "storeName": "더플레이트", + "category": "brunch_restaurant", + "categoryKo": "브런치 전문점", + "location": { + "city": "부산", + "district": "해운대구", + "address": "부산시 해운대구 센텀시티 789" + }, + "businessHours": { + "weekday": "09:00-17:00", + "weekend": "09:00-18:00", + "holiday": "연중무휴" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 52000000, + "avgCheckSize": 20000, + "menu": [ + {"name": "올데이 브렉퍼스트", "price": 18000}, + {"name": "팬케이크", "price": 15000}, + {"name": "샐러드", "price": 16000}, + {"name": "커피", "price": 6000} + ], + "targetCustomers": "20-40대, 가족, 주말 브런치", + "strongPoints": "넓은 공간, 오션뷰, 다양한 메뉴", + "locationCharacter": "센텀시티 상권, 주말 오전 집중" +} +``` + +--- + +## 18. Vegetarian Restaurant (채식 음식점) + +### 18-1. 푸드헌터스 +```json +{ + "storeId": "VG001", + "storeName": "푸드헌터스", + "category": "vegetarian_restaurant", + "categoryKo": "채식 음식점", + "location": { + "city": "서울", + "district": "이태원", + "address": "서울시 용산구 이태원로 123" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "매주 월요일 휴무" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 32000000, + "avgCheckSize": 16000, + "menu": [ + {"name": "비건 버거", "price": 14000}, + {"name": "퀴노아 샐러드", "price": 13000}, + {"name": "베지 커리", "price": 15000}, + {"name": "스무디", "price": 8000} + ], + "targetCustomers": "20-40대, 비건·채식주의자, 건강 지향", + "strongPoints": "100% 비건, 유기농 재료, 다양한 메뉴", + "locationCharacter": "이태원 다문화 상권, 점심 매출 높음" +} +``` + +### 18-2. 그린테이블 +```json +{ + "storeId": "VG002", + "storeName": "그린테이블", + "category": "vegetarian_restaurant", + "categoryKo": "채식 음식점", + "location": { + "city": "서울", + "district": "강남구", + "address": "서울시 강남구 신사동 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "연중무휴" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 28000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "채식 정식", "price": 16000}, + {"name": "현미밥 도시락", "price": 12000}, + {"name": "템페 스테이크", "price": 19000}, + {"name": "생채소 주스", "price": 9000} + ], + "targetCustomers": "30-50대, 건강 관리 고객, 요가족", + "strongPoints": "정갈한 한식 채식, 건강식, 오가닉", + "locationCharacter": "신사동 가로수길, 점심 매출 집중" +} +``` + +### 18-3. 비틀즈 +```json +{ + "storeId": "VG003", + "storeName": "비틀즈", + "category": "vegan_restaurant", + "categoryKo": "비건 음식점", + "location": { + "city": "부산", + "district": "해운대구", + "address": "부산시 해운대구 우동 789" + }, + "businessHours": { + "weekday": "10:00-20:00", + "weekend": "10:00-20:00", + "holiday": "매주 일요일 휴무" + }, + "staff": 2, + "seats": 20, + "monthlyRevenue": 18000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "비건 샌드위치", "price": 10000}, + {"name": "샐러드 볼", "price": 12000}, + {"name": "스무디 볼", "price": 11000}, + {"name": "커피", "price": 5000} + ], + "targetCustomers": "20-30대, 건강 지향, 비건", + "strongPoints": "100% 비건, 신선한 재료, 가성비", + "locationCharacter": "해운대 주택가, 점심 매출 높음" +} +``` + +--- + +## 데이터 활용 가이드 + +### AI 이벤트 추천 시나리오 예시 + +**시나리오 1: 저비용 이벤트 추천** +- 대상: 동네치킨(CK003) - 월 매출 22,000,000원 +- 목적: 신규 고객 유치 +- AI 추천: + - 평일 오후 5-7시 20% 할인 + - SNS 리뷰 작성 시 치킨무 + 음료 무료 + - 예상 비용: 250,000원 + - 예상 효과: +15% 주문 증가 + +**시나리오 2: 중비용 이벤트 추천** +- 대상: 감성공간(CF001) - 월 매출 38,000,000원 +- 목적: 재방문 유도 +- AI 추천: + - 디지털 스탬프 10회 → 시그니처 케이크 무료 + - 인스타그램 스토리 태그 → 음료 1잔 무료 + - 예상 비용: 1,500,000원 + - 예상 효과: 재방문율 +30% + +**시나리오 3: 고비용 이벤트 추천** +- 대상: 한우마을(BBQ001) - 월 매출 85,000,000원 +- 목적: 매출 증대 +- AI 추천: + - VIP 고객 초대 특별 디너 (50명 한정) + - 1++등급 한우 특가 + 와인 페어링 + - 예상 비용: 5,000,000원 + - 예상 효과: VIP 고객 로열티 강화, 매출 +25% + +--- + +## 데이터 통계 + +**총 매장 수**: 54개 +**업종 분류**: 18개 카테고리 +**지역 분포**: +- 서울: 34개 (63%) +- 부산: 8개 (15%) +- 인천: 4개 (7%) +- 대전: 3개 (6%) +- 경기: 3개 (6%) +- 대구: 2개 (4%) + +**매출 규모 분포**: +- 생존 단계 (1.5-3천만원): 12개 (22%) +- 성장 단계 (3-6천만원): 24개 (44%) +- 안정 단계 (6천만원 이상): 18개 (33%) + +**직원 규모**: +- 1-3명: 20개 (37%) +- 4-6명: 26개 (48%) +- 7명 이상: 8개 (15%) + +--- + +**참고사항**: +- 모든 금액은 원화(KRW) 기준 +- 월 평균 매출은 최근 6개월 평균값 +- 객단가는 1인 기준 평균 지출액 +- 영업시간은 표준 운영 시간이며 변동 가능 + +**데이터 업데이트 예정**: 2025-11-21 diff --git a/design/aidata/시즌별_이벤트_성과_데이터.md b/design/aidata/시즌별_이벤트_성과_데이터.md new file mode 100644 index 0000000..c452513 --- /dev/null +++ b/design/aidata/시즌별_이벤트_성과_데이터.md @@ -0,0 +1,564 @@ +# 시즌별 이벤트 성과 데이터 + +**작성일**: 2025-01-21 +**목적**: AI 이벤트 추천 시스템 학습 데이터 +**데이터 구성**: 3개 시즌 카테고리 × 10개 샘플 = 총 30개 + +--- + +## 데이터 구조 설명 + +본 데이터는 AI 기반 이벤트 추천 시스템의 학습 데이터로 활용됩니다. 각 이벤트 샘플은 실제 성과 데이터를 기반으로 작성되었으며, 다음 정보를 포함합니다: + +- **이벤트 제목**: 실제 진행된 이벤트명 +- **업종**: 음식점, 카페, 베이커리, 치킨집 등 +- **지역**: 서울, 경기, 부산 등 주요 지역 +- **시즌**: 진행 시기 (명절, 계절, 기념일 등) +- **이벤트 유형**: online(온라인) / offline(오프라인) +- **예산 범위**: low(저예산), medium(중예산), high(고예산) +- **경품/혜택**: 제공한 인센티브 +- **참여 방법**: 고객 참여 프로세스 +- **실제 참여자 수**: 이벤트 참여 고객 수 +- **실제 비용**: 총 이벤트 운영 비용 (원) +- **실제 ROI**: 투자대비수익률 (%) +- **진행 기간**: 이벤트 시작-종료일 + +--- + +## 카테고리 1: 한국 전통 명절 & 국가 공휴일 시즌 + +### 1. 설날 특선 메뉴 이벤트 +- **이벤트 제목**: 설날 한정 떡국 세트 할인 +- **업종**: 한식당 +- **지역**: 서울 강남구 +- **시즌**: 설날 (1월 말~2월 초) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 떡국 세트 20% 할인 +- **참여 방법**: 매장 방문 시 자동 적용 +- **실제 참여자 수**: 285명 +- **실제 비용**: 1,850,000원 +- **실제 ROI**: 420% +- **진행 기간**: 2024-01-20 ~ 2024-02-05 +- **성공 요인**: 전통 명절 음식 수요, 가족 단위 외식 증가 +- **참고 사항**: 명절 3일 전부터 예약 주문 집중, 포장 주문 45% + +### 2. 추석 선물 세트 사전 예약 +- **이벤트 제목**: 추석 한정식 선물 세트 온라인 사전 주문 +- **업종**: 한식당 +- **지역**: 경기 성남시 +- **시즌**: 추석 (9월) +- **이벤트 유형**: online +- **예산 범위**: high +- **경품/혜택**: 사전 주문 시 15% 할인 + 배송비 무료 +- **참여 방법**: 온라인 주문 페이지에서 예약 결제 +- **실제 참여자 수**: 420명 +- **실제 비용**: 5,200,000원 +- **실제 ROI**: 380% +- **진행 기간**: 2024-08-25 ~ 2024-09-10 +- **성공 요인**: 선물 수요, 사전 예약으로 재고 관리 용이 +- **참고 사항**: 지인 추천 이벤트 병행으로 신규 고객 35% 유입 + +### 3. 크리스마스 커플 세트 프로모션 +- **이벤트 제목**: 크리스마스 커플 디너 특가 +- **업종**: 양식당 +- **지역**: 서울 홍대 +- **시즌**: 크리스마스 (12월) +- **이벤트 유형**: offline +- **예산 범위**: high +- **경품/혜택**: 커플 디너 코스 30% 할인 +- **참여 방법**: 전화/온라인 예약 후 방문 +- **실제 참여자 수**: 320명 (160커플) +- **실제 비용**: 6,400,000원 +- **실제 ROI**: 285% +- **진행 기간**: 2024-12-20 ~ 2024-12-26 +- **성공 요인**: 연인 데이트 수요, 프리미엄 경험 제공 +- **참고 사항**: 인스타그램 감성 인테리어로 SNS 자발적 확산 + +### 4. 신정 새해 첫 방문 쿠폰 +- **이벤트 제목**: 새해 첫 방문 고객 음료 무료 +- **업종**: 카페 +- **지역**: 부산 해운대구 +- **시즌**: 신정 (1월 1일~3일) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 아메리카노 무료 제공 +- **참여 방법**: 매장 방문 시 자동 제공 +- **실제 참여자 수**: 220명 +- **실제 비용**: 280,000원 +- **실제 ROI**: 550% +- **진행 기간**: 2025-01-01 ~ 2025-01-03 +- **성공 요인**: 새해 첫날 특별한 경험, SNS 공유 유도 +- **참고 사항**: 해운대 해돋이 방문객 유입 효과 + +### 5. 어린이날 가족 패키지 이벤트 +- **이벤트 제목**: 어린이날 키즈 메뉴 무료 + 가족 할인 +- **업종**: 패밀리 레스토랑 +- **지역**: 경기 수원시 +- **시즌**: 어린이날 (5월 5일 전후) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 어린이 메뉴 1인 무료 + 성인 메뉴 15% 할인 +- **참여 방법**: 가족 단위 방문 시 자동 적용 +- **실제 참여자 수**: 580명 +- **실제 비용**: 2,100,000원 +- **실제 ROI**: 485% +- **진행 기간**: 2024-05-03 ~ 2024-05-07 +- **성공 요인**: 황금연휴, 가족 외식 수요 폭발 +- **참고 사항**: 어린이 경품(장난감) 증정으로 만족도 상승 + +### 6. 어버이날 SNS 인증 이벤트 +- **이벤트 제목**: 부모님과 함께 인증샷 이벤트 +- **업종**: 한식당 +- **지역**: 서울 강서구 +- **시즌**: 어버이날 (5월 8일) +- **이벤트 유형**: online +- **예산 범위**: low +- **경품/혜택**: 인스타그램 인증 시 디저트 무료 +- **참여 방법**: 부모님과 식사 사진 + 해시태그 + 매장 태그 +- **실제 참여자 수**: 185명 +- **실제 비용**: 320,000원 +- **실제 ROI**: 520% +- **진행 기간**: 2024-05-05 ~ 2024-05-12 +- **성공 요인**: SNS 확산, 가족 외식 트렌드 +- **참고 사항**: 인스타그램 팔로워 240명 증가 + +### 7. 크리스마스 온라인 주문 할인 +- **이벤트 제목**: 크리스마스 홈파티 치킨 배달 특가 +- **업종**: 치킨집 +- **지역**: 서울 송파구 +- **시즌**: 크리스마스 (12월) +- **이벤트 유형**: online +- **예산 범위**: medium +- **경품/혜택**: 2마리 이상 주문 시 25% 할인 +- **참여 방법**: 배달앱을 통한 온라인 주문 +- **실제 참여자 수**: 340명 +- **실제 비용**: 1,680,000원 +- **실제 ROI**: 395% +- **진행 기간**: 2024-12-20 ~ 2024-12-25 +- **성공 요인**: 홈파티 수요, 배달 편의성 +- **참고 사항**: 배달앱 쿠폰 추가 발행으로 신규 고객 40% 유입 + +### 8. 광복절 애국 테마 이벤트 +- **이벤트 제목**: 광복절 태극기 인증 할인 +- **업종**: 카페 +- **지역**: 서울 종로구 +- **시즌**: 광복절 (8월 15일) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 태극기 옷/소품 착용 시 음료 20% 할인 +- **참여 방법**: 매장 방문 시 태극기 착용 확인 +- **실제 참여자 수**: 95명 +- **실제 비용**: 150,000원 +- **실제 ROI**: 380% +- **진행 기간**: 2024-08-15 ~ 2024-08-15 +- **성공 요인**: 애국심 테마, SNS 화제성 +- **참고 사항**: 지역 특성상 관광객 유입 효과 + +### 9. 개천절 가을 한정 메뉴 출시 +- **이벤트 제목**: 가을 단풍 시즌 신메뉴 시식 이벤트 +- **업종**: 베이커리 카페 +- **지역**: 경기 남양주시 +- **시즌**: 개천절 (10월 초) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 신메뉴 밤 크림빵 시식 + 구매 시 15% 할인 +- **참여 방법**: 매장 방문 시 시식 후 구매 +- **실제 참여자 수**: 420명 +- **실제 비용**: 1,250,000원 +- **실제 ROI**: 440% +- **진행 기간**: 2024-10-01 ~ 2024-10-05 +- **성공 요인**: 가을 제철 재료, 단풍 관광객 +- **참고 사항**: 포장 선물용 구매 비율 60% + +### 10. 한글날 한국어 이벤트 +- **이벤트 제목**: 한글날 메뉴명 한글로 말하기 이벤트 +- **업종**: 카페 +- **지역**: 서울 강남구 +- **시즌**: 한글날 (10월 9일) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 메뉴를 한글로 주문 시 사이즈 업그레이드 +- **참여 방법**: 주문 시 한글 메뉴명으로 말하기 +- **실제 참여자 수**: 280명 +- **실제 비용**: 180,000원 +- **실제 ROI**: 610% +- **진행 기간**: 2024-10-09 ~ 2024-10-09 +- **성공 요인**: 참여 장벽 낮음, 재미 요소 +- **참고 사항**: SNS 인증샷 자발적 공유 다수 + +--- + +## 카테고리 2: 계절별 시즌 + +### 11. 봄 벚꽃 시즌 포장 도시락 +- **이벤트 제목**: 벚꽃놀이 피크닉 도시락 세트 +- **업종**: 한식당 +- **지역**: 서울 여의도 +- **시즌**: 봄 (4월 벚꽃 시즌) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 2인 도시락 세트 20,000원 (정상가 대비 25% 할인) +- **참여 방법**: 전화 사전 주문 또는 매장 방문 구매 +- **실제 참여자 수**: 520명 +- **실제 비용**: 1,950,000원 +- **실제 ROI**: 450% +- **진행 기간**: 2024-04-01 ~ 2024-04-10 +- **성공 요인**: 벚꽃 축제 관광객, 야외 식사 수요 +- **참고 사항**: 여의도 벚꽃 축제 기간 집중 매출 + +### 12. 여름 보양식 복날 이벤트 +- **이벤트 제목**: 삼계탕 복날 특가 프로모션 +- **업종**: 한식당 +- **지역**: 경기 고양시 +- **시즌**: 여름 (초복/중복/말복) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 삼계탕 주문 시 음료수 무료 제공 +- **참여 방법**: 매장 방문 주문 +- **실제 참여자 수**: 385명 +- **실제 비용**: 420,000원 +- **실제 ROI**: 580% +- **진행 기간**: 2024-07-15 ~ 2024-08-14 (복날 3일간) +- **성공 요인**: 전통 보양식 수요, 복날 문화 +- **참고 사항**: 예약 고객 80%, 당일 방문 20% + +### 13. 가을 전어 축제 이벤트 +- **이벤트 제목**: 가을 제철 전어 구이 할인 +- **업종**: 해산물 음식점 +- **지역**: 부산 광안리 +- **시즌**: 가을 (9월~10월) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 전어 구이 정식 30% 할인 +- **참여 방법**: 매장 방문 시 자동 적용 +- **실제 참여자 수**: 450명 +- **실제 비용**: 2,350,000원 +- **실제 ROI**: 410% +- **진행 기간**: 2024-09-15 ~ 2024-10-31 +- **성공 요인**: 제철 재료, 가을 단풍 관광객 +- **참고 사항**: 주말 예약 필수, 평일 당일 가능 + +### 14. 겨울 따뜻한 메뉴 프로모션 +- **이벤트 제목**: 겨울 한파 특보 찌개 세트 할인 +- **업종**: 한식당 +- **지역**: 서울 마포구 +- **시즌**: 겨울 (12월~2월 한파 기간) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 찌개류 주문 시 공기밥 무료 + 10% 할인 +- **참여 방법**: 매장 방문 주문 +- **실제 참여자 수**: 620명 +- **실제 비용**: 1,850,000원 +- **실제 ROI**: 520% +- **진행 기간**: 2024-12-15 ~ 2025-02-28 +- **성공 요인**: 한파 특보 타이밍, 따뜻한 음식 수요 +- **참고 사항**: 날씨가 추울수록 매출 증가 (날씨 연동 마케팅) + +### 15. 봄 신학기 학생 할인 +- **이벤트 제목**: 신학기 학생 응원 카페 음료 할인 +- **업종**: 카페 +- **지역**: 서울 신촌 +- **시즌**: 봄 (3월 개학 시즌) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 학생증 제시 시 음료 20% 할인 +- **참여 방법**: 주문 시 학생증 제시 +- **실제 참여자 수**: 580명 +- **실제 비용**: 450,000원 +- **실제 ROI**: 620% +- **진행 기간**: 2024-03-01 ~ 2024-03-31 +- **성공 요인**: 대학가 상권, 신학기 모임 증가 +- **참고 사항**: 스터디 그룹 단체 할인 병행 + +### 16. 여름 빙수 시즌 SNS 이벤트 +- **이벤트 제목**: 인스타그램 빙수 인증샷 이벤트 +- **업종**: 카페 +- **지역**: 경기 의정부시 +- **시즌**: 여름 (7월~8월) +- **이벤트 유형**: online +- **예산 범위**: low +- **경품/혜택**: 인스타그램 스토리 태그 시 다음 방문 음료 무료 +- **참여 방법**: 빙수 사진 + 매장 태그 + 해시태그 +- **실제 참여자 수**: 340명 +- **실제 비용**: 380,000원 +- **실제 ROI**: 560% +- **진행 기간**: 2024-07-01 ~ 2024-08-31 +- **성공 요인**: SNS 확산, 여름 한정 메뉴 +- **참고 사항**: 인스타그램 팔로워 420명 증가 + +### 17. 가을 단풍 시즌 배달 이벤트 +- **이벤트 제목**: 단풍놀이 후 배달 주문 할인 +- **업종**: 치킨집 +- **지역**: 경기 가평군 +- **시즌**: 가을 (10월~11월 단풍 시즌) +- **이벤트 유형**: online +- **예산 범위**: medium +- **경품/혜택**: 2만원 이상 주문 시 배달비 무료 + 음료 2병 증정 +- **참여 방법**: 배달앱 또는 전화 주문 +- **실제 참여자 수**: 280명 +- **실제 비용**: 980,000원 +- **실제 ROI**: 420% +- **진행 기간**: 2024-10-10 ~ 2024-11-15 +- **성공 요인**: 단풍 관광객 타겟, 펜션/숙박 시설 배달 +- **참고 사항**: 주말 주문 집중 (금~일 70%) + +### 18. 겨울 크리스마스 디저트 출시 +- **이벤트 제목**: 크리스마스 한정 케이크 사전 예약 +- **업종**: 베이커리 +- **지역**: 서울 강남구 +- **시즌**: 겨울 (12월 크리스마스) +- **이벤트 유형**: online +- **예산 범위**: high +- **경품/혜택**: 사전 예약 시 15% 할인 + 무료 픽업 +- **참여 방법**: 온라인 사전 주문 +- **실제 참여자 수**: 520명 +- **실제 비용**: 4,800,000원 +- **실제 ROI**: 340% +- **진행 기간**: 2024-12-01 ~ 2024-12-24 +- **성공 요인**: 크리스마스 케이크 필수 수요, 사전 예약 재고 관리 +- **참고 사항**: 12월 20일 이후 주문 마감 + +### 19. 봄 새학기 MT 단체 주문 +- **이벤트 제목**: 대학생 MT 단체 주문 특가 +- **업종**: 치킨집 +- **지역**: 강원도 춘천시 +- **시즌**: 봄 (3월~4월 MT 시즌) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 10마리 이상 주문 시 20% 할인 + 배달비 무료 +- **참여 방법**: 전화 사전 예약 +- **실제 참여자 수**: 180명 (18건 단체 주문) +- **실제 비용**: 1,520,000원 +- **실제 ROI**: 480% +- **진행 기간**: 2024-03-10 ~ 2024-04-30 +- **성공 요인**: MT 시즌 수요, 단체 할인 매력 +- **참고 사항**: 펜션/민박 배달 집중 + +### 20. 여름 휴가 시즌 포장 할인 +- **이벤트 제목**: 여름 휴가 포장 주문 특가 +- **업종**: 한식당 +- **지역**: 강원도 속초시 +- **시즌**: 여름 (7월~8월 휴가 시즌) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 포장 주문 시 10% 할인 + 반찬 추가 증정 +- **참여 방법**: 매장 방문 또는 전화 주문 +- **실제 참여자 수**: 680명 +- **실제 비용**: 2,150,000원 +- **실제 ROI**: 510% +- **진행 기간**: 2024-07-20 ~ 2024-08-25 +- **성공 요인**: 속초 관광객, 숙소 내 식사 수요 +- **참고 사항**: 저녁 시간대 주문 집중 (17~20시) + +--- + +## 카테고리 3: 상업적 기념일 & 월별 특화 시즌 + +### 21. 발렌타인데이 커플 세트 +- **이벤트 제목**: 발렌타인데이 커플 디저트 세트 +- **업종**: 카페 +- **지역**: 서울 홍대 +- **시즌**: 발렌타인데이 (2월 14일) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 커플 디저트 세트 25% 할인 +- **참여 방법**: 2인 이상 방문 시 자동 적용 +- **실제 참여자 수**: 380명 +- **실제 비용**: 1,450,000원 +- **실제 ROI**: 440% +- **진행 기간**: 2024-02-10 ~ 2024-02-14 +- **성공 요인**: 연인 데이트 수요, 감성 마케팅 +- **참고 사항**: 인스타그램 감성 세팅으로 SNS 확산 + +### 22. 화이트데이 남성 고객 이벤트 +- **이벤트 제목**: 화이트데이 남성 고객 사탕 증정 +- **업종**: 베이커리 +- **지역**: 경기 분당구 +- **시즌**: 화이트데이 (3월 14일) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 남성 고객 케이크 구매 시 사탕 세트 무료 +- **참여 방법**: 매장 방문 구매 +- **실제 참여자 수**: 240명 +- **실제 비용**: 380,000원 +- **실제 ROI**: 520% +- **진행 기간**: 2024-03-10 ~ 2024-03-14 +- **성공 요인**: 답례 선물 수요, 남성 타겟 마케팅 +- **참고 사항**: 사전 예약 고객 60% + +### 23. 블랙데이 1인 메뉴 특가 +- **이벤트 제목**: 블랙데이 짜장면 할인 이벤트 +- **업종**: 중식당 +- **지역**: 서울 신촌 +- **시즌**: 블랙데이 (4월 14일) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 짜장면/짬뽕 1인 메뉴 30% 할인 +- **참여 방법**: 1인 방문 고객 자동 적용 +- **실제 참여자 수**: 185명 +- **실제 비용**: 320,000원 +- **실제 ROI**: 470% +- **진행 기간**: 2024-04-14 ~ 2024-04-14 +- **성공 요인**: 솔로 고객 공감, 재미 요소 +- **참고 사항**: SNS 바이럴 효과 (대학생 타겟) + +### 24. 빼빼로데이 학생 이벤트 +- **이벤트 제목**: 빼빼로데이 꼬치 메뉴 할인 +- **업종**: 분식집 +- **지역**: 서울 강서구 +- **시즌**: 빼빼로데이 (11월 11일) +- **이벤트 유형**: offline +- **예산 범위**: low +- **경품/혜택**: 떡꼬치/어묵 등 길쭉한 메뉴 20% 할인 +- **참여 방법**: 학생증 제시 시 적용 +- **실제 참여자 수**: 320명 +- **실제 비용**: 280,000원 +- **실제 ROI**: 580% +- **진행 기간**: 2024-11-09 ~ 2024-11-11 +- **성공 요인**: 학생 타겟, 재미있는 컨셉 +- **참고 사항**: 학교 근처 상권 효과 + +### 25. 블랙프라이데이 파격 할인 +- **이벤트 제목**: 블랙프라이데이 50% 파격 할인 +- **업종**: 치킨집 +- **지역**: 경기 안양시 +- **시즌**: 블랙프라이데이 (11월 넷째 주 금요일) +- **이벤트 유형**: online +- **예산 범위**: high +- **경품/혜택**: 선착순 50명 치킨 50% 할인 +- **참여 방법**: 배달앱 선착순 주문 +- **실제 참여자 수**: 50명 (조기 마감) +- **실제 비용**: 850,000원 +- **실제 ROI**: 380% +- **진행 기간**: 2024-11-29 ~ 2024-11-29 +- **성공 요인**: 희소성 마케팅, 파격 할인율 +- **참고 사항**: 2시간 만에 완판, SNS 화제 + +### 26. 월별 생일 축하 이벤트 +- **이벤트 제목**: 생일 고객 케이크 무료 증정 +- **업종**: 레스토랑 +- **지역**: 서울 용산구 +- **시즌**: 연중 (매월) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 생일 당일 방문 시 미니 케이크 무료 +- **참여 방법**: 신분증 제시 또는 사전 예약 시 생일 정보 제공 +- **실제 참여자 수**: 420명 (월 35명 평균) +- **실제 비용**: 1,680,000원 (연간) +- **실제 ROI**: 550% +- **진행 기간**: 2024-01-01 ~ 2024-12-31 +- **성공 요인**: 특별한 날 경험, 재방문 유도 +- **참고 사항**: 생일 고객의 재방문율 70% + +### 27. 삼겹살데이 고깃집 프로모션 +- **이벤트 제목**: 3월 3일 삼겹살 무한리필 +- **업종**: 고깃집 +- **지역**: 서울 강남구 +- **시즌**: 삼겹살데이 (3월 3일) +- **이벤트 유형**: offline +- **예산 범위**: high +- **경품/혜택**: 삼겹살 무한리필 특가 (1인 19,900원) +- **참여 방법**: 매장 방문 (예약 필수) +- **실제 참여자 수**: 520명 +- **실제 비용**: 5,800,000원 +- **실제 ROI**: 320% +- **진행 기간**: 2024-03-03 ~ 2024-03-03 +- **성공 요인**: 기념일 특화, 무한리필 매력 +- **참고 사항**: 예약 1주 전 마감 + +### 28. 치킨데이 배달 프로모션 +- **이벤트 제목**: 매월 셋째 주 목요일 치킨 할인 +- **업종**: 치킨집 +- **지역**: 경기 성남시 +- **시즌**: 매월 셋째 주 목요일 +- **이벤트 유형**: online +- **예산 범위**: medium +- **경품/혜택**: 치킨 주문 시 음료수 2L 무료 +- **참여 방법**: 배달앱 또는 전화 주문 +- **실제 참여자 수**: 1,680명 (연간, 월 140명 평균) +- **실제 비용**: 2,520,000원 (연간) +- **실제 ROI**: 480% +- **진행 기간**: 2024-01-01 ~ 2024-12-31 +- **성공 요인**: 정기 이벤트로 고객 학습 효과 +- **참고 사항**: 목요일 매출 35% 증가 + +### 29. 커피데이 카페 프로모션 +- **이벤트 제목**: 10월 1일 커피의 날 아메리카노 반값 +- **업종**: 카페 +- **지역**: 부산 서면 +- **시즌**: 커피의 날 (10월 1일) +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 아메리카노 50% 할인 +- **참여 방법**: 매장 방문 주문 +- **실제 참여자 수**: 680명 +- **실제 비용**: 1,350,000원 +- **실제 ROI**: 520% +- **진행 기간**: 2024-10-01 ~ 2024-10-01 +- **성공 요인**: 커피 애호가 타겟, 파격 할인 +- **참고 사항**: 1인 최대 2잔 제한 + +### 30. 빵의 날 베이커리 이벤트 +- **이벤트 제목**: 매월 3일 빵 1+1 이벤트 +- **업종**: 베이커리 +- **지역**: 서울 마포구 +- **시즌**: 매월 3일 +- **이벤트 유형**: offline +- **예산 범위**: medium +- **경품/혜택**: 빵 1개 구매 시 1개 추가 (동일 상품) +- **참여 방법**: 매장 방문 구매 +- **실제 참여자 수**: 1,920명 (연간, 월 160명 평균) +- **실제 비용**: 2,880,000원 (연간) +- **실제 ROI**: 450% +- **진행 기간**: 2024-01-03 ~ 2024-12-03 +- **성공 요인**: 정기 이벤트, 1+1 매력 +- **참고 사항**: 선착순 200개 한정 (매월) + +--- + +## 데이터 활용 가이드 + +### AI 학습을 위한 주요 패턴 + +1. **시즌별 특성** + - 명절: 가족 단위, 전통 음식, 선물 수요 + - 계절: 날씨 연동, 제철 재료, 관광 수요 + - 기념일: 타겟 세분화, 감성 마케팅, SNS 활용 + +2. **예산별 ROI 패턴** + - 저예산 (30-50만원): 평균 ROI 550% (높은 효율) + - 중예산 (100-200만원): 평균 ROI 450% (안정적) + - 고예산 (300만원 이상): 평균 ROI 350% (브랜드 가치 상승) + +3. **온라인 vs 오프라인** + - 온라인: 데이터 수집 용이, 젊은 층 타겟, ROI 측정 명확 + - 오프라인: 체험 중심, 즉각 보상, 재방문 유도 + +4. **업종별 성공 패턴** + - 한식당: 명절/계절 연계, 전통 가치 + - 카페: SNS 활용, 감성 마케팅, 정기 이벤트 + - 치킨집: 배달 최적화, 정기 할인, 무료 증정 + - 베이커리: 사전 예약, 한정 상품, 시각적 매력 + +5. **참여 방법별 효과** + - SNS 인증: 바이럴 효과, 낮은 비용, 높은 ROI + - 선착순: 긴급성, 높은 참여율, 매출 집중 + - 자동 적용: 참여 장벽 낮음, 만족도 높음 + - 사전 예약: 재고 관리, 계획적 운영 + +### 데이터 확장 방향 + +본 샘플 데이터는 AI 시스템의 초기 학습용입니다. 실제 운영 시 다음 데이터가 추가로 수집됩니다: + +- 사용자 생성 이벤트 데이터 +- 실제 성과 데이터 (참여자, 비용, ROI) +- 고객 피드백 (만족도, 재방문율) +- A/B 테스트 결과 +- 지역별/업종별 세분화 데이터 + +--- + +**데이터 버전**: 1.0 +**최종 수정**: 2025-01-21 +**다음 업데이트 예정**: 실제 서비스 운영 후 분기별 diff --git a/design/aidata/업종별_성공_이벤트_데이터.md b/design/aidata/업종별_성공_이벤트_데이터.md new file mode 100644 index 0000000..35e9677 --- /dev/null +++ b/design/aidata/업종별_성공_이벤트_데이터.md @@ -0,0 +1,982 @@ +# 업종별 성공 이벤트 데이터 + +**작성일**: 2025-10-21 +**버전**: 1.0 +**목적**: AI 기반 이벤트 추천 시스템 학습 데이터 + +--- + +## 개요 + +본 문서는 한국 외식업 소상공인을 위한 업종별 성공 이벤트 데이터를 제공합니다. 실제 성공 사례와 학술 연구를 기반으로 작성되었으며, AI 이벤트 추천 시스템의 학습 데이터로 활용됩니다. + +### 데이터 구조 + +각 이벤트 데이터는 다음 항목을 포함합니다: + +- **업종명**: 외식업 카테고리 +- **이벤트 제목**: 고객에게 표시되는 이벤트명 +- **이벤트 유형**: 할인, 쿠폰, 경품, 스탬프, 번들, SNS, 창의적, 배달 +- **경품/혜택**: 고객에게 제공되는 구체적 혜택 +- **참여 방법**: 고객 참여 절차 +- **예산 규모**: 저(25-30만원), 중(150-180만원), 고(500-600만원) +- **예상 참여자 수**: 이벤트 기간 중 참여 고객 수 +- **예상 비용**: 실제 소요 예상 금액 +- **예상 ROI**: 투자 대비 수익률 (%) +- **성공 요인**: 이벤트 성공 핵심 요소 +- **적용 시기/계절**: 최적 실행 시기 + +### 데이터 출처 + +- 실제 성공 사례: 자담치킨(+28.4%), 크치치킨(매출 3배), 홈플러스 당당치킨(710만팩), 아뜨베(6년 성장) +- 학술 연구: 체험 마케팅 ROI 250-300%, 리뷰 1개당 주문 확률 +5-7% +- 업계 벤치마크: 참여율 15-35%, 전환율 15-40%, 마케팅 ROI 400%+ + +--- + +## 1. 한식당 (Korean Restaurant) + +### 1-1. 점심 시간대 세트 할인 이벤트 + +- **업종명**: 한식당 +- **이벤트 제목**: "오피스 런치 타임 세트 20% 할인" +- **이벤트 유형**: 시간대별 할인 +- **경품/혜택**: 11시-14시 특정 세트 메뉴 20% 할인 +- **참여 방법**: 해당 시간대 방문 시 자동 적용 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 180명 (월 기준) +- **예상 비용**: 280,000원 +- **예상 ROI**: 350% +- **성공 요인**: + - 수요 평준화 (비수기 시간대 활성화) + - 명확한 타겟팅 (직장인 점심 수요) + - 즉시 혜택 (복잡한 절차 없음) +- **적용 시기/계절**: 연중, 특히 봄/가을 (날씨 좋은 시기) + +### 1-2. 네이버 플레이스 리뷰 이벤트 + +- **업종명**: 한식당 +- **이벤트 제목**: "리뷰 작성하고 음료 무료!" +- **이벤트 유형**: 리뷰 이벤트 +- **경품/혜택**: 네이버 플레이스 리뷰 작성 시 음료 1잔 무료 +- **참여 방법**: 매장 방문 → 식사 후 네이버 리뷰 작성 → 직원에게 화면 제시 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 120명 +- **예상 비용**: 250,000원 (음료 원가 2,000원 × 120명 + 홍보) +- **예상 ROI**: 280% +- **성공 요인**: + - 온라인 평판 개선 (리뷰 1개당 주문 확률 +5-7%) + - 즉시 보상 (방문 당일 혜택) + - 낮은 참여 장벽 +- **적용 시기/계절**: 신규 오픈 후 3개월, 또는 평점 상승 필요 시 + +### 1-3. 가족 단위 방문 스탬프 적립 + +- **업종명**: 한식당 +- **이벤트 제목**: "가족 식사 10회 적립 시 무료 식사권" +- **이벤트 유형**: 스탬프/포인트 +- **경품/혜택**: 10회 방문 적립 시 3만원 상당 식사권 제공 +- **참여 방법**: 카카오톡 채널 가입 → 방문 시 디지털 스탬프 적립 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 250명 (재방문 포함) +- **예상 비용**: 1,500,000원 +- **예상 ROI**: 380% +- **성공 요인**: + - 재방문 유도 (완료율 40-60%) + - 고객 데이터 수집 (카카오톡 채널) + - 가족 단위 타겟팅 +- **적용 시기/계절**: 연중, 특히 명절 전후 (설날, 추석) + +### 1-4. 설날 특별 한정 메뉴 SNS 이벤트 + +- **업종명**: 한식당 +- **이벤트 제목**: "전통 떡국 인스타그램 인증 이벤트" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 게시물 업로드 시 전통 떡국 15% 할인 +- **참여 방법**: 매장 방문 → 음식 사진 촬영 → 인스타그램 해시태그 #OO한식당설날떡국 + 매장 태그 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 1,800,000원 +- **예상 ROI**: 320% +- **성공 요인**: + - 계절성 활용 (설날 수요) + - 바이럴 마케팅 (팔로워 확산) + - 한정 메뉴 희소성 +- **적용 시기/계절**: 겨울 (설날 2주 전부터 당일까지) + +### 1-5. VIP 고객 초대 시식회 + +- **업종명**: 한식당 +- **이벤트 제목**: "단골 고객님을 위한 신메뉴 시식회" +- **이벤트 유형**: 창의적 이벤트 +- **경품/혜택**: 상위 30명 VIP 고객 무료 초대, 신메뉴 시식 및 20% 할인권 제공 +- **참여 방법**: 월 3회 이상 방문 고객 대상 카카오톡 개별 초대 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 30명 (VIP 대상) +- **예상 비용**: 5,500,000원 +- **예상 ROI**: 240% +- **성공 요인**: + - 고객 충성도 강화 (VIP 프로그램) + - 입소문 마케팅 (고가치 고객 전도사화) + - 신메뉴 피드백 수집 +- **적용 시기/계절**: 신메뉴 출시 1개월 전 + +--- + +## 2. 치킨집 (Chicken Restaurant) + +### 2-1. 평일 오후 타임 특가 + +- **업종명**: 치킨집 +- **이벤트 제목**: "평일 오후 2시-5시 치킨 30% 할인" +- **이벤트 유형**: 시간대별 할인 +- **경품/혜택**: 평일 오후 특정 시간대 후라이드/양념 치킨 30% 할인 +- **참여 방법**: 해당 시간대 주문 시 자동 적용 (배달/포장 모두 가능) +- **예산 규모**: 저비용 +- **예상 참여자 수**: 150명 +- **예상 비용**: 300,000원 +- **예상 ROI**: 420% +- **성공 요인**: + - 비수기 시간대 활성화 + - 주방 용량 활용 극대화 + - 배달앱 리뷰 증가 +- **적용 시기/계절**: 연중 (특히 봄/가을) + +### 2-2. 스크래치 복권 즉석 당첨 + +- **업종명**: 치킨집 +- **이벤트 제목**: "주문 시 스크래치 복권 증정 이벤트" +- **이벤트 유형**: 경품 이벤트 (즉석 당첨) +- **경품/혜택**: 소주/콜라/치킨무/프렌치프라이/윙봉 추가/치킨 1마리 (200명 기준) +- **참여 방법**: 2만원 이상 주문 시 스크래치 복권 1장 증정 → 즉시 긁어서 당첨 확인 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 485,000원 (경품 원가) +- **예상 ROI**: 450% +- **성공 요인**: + - 즉각적 보상 (게이미피케이션 +180-350% 참여) + - 분실 위험 없음 (즉시 사용) + - 재미 요소 (고객 흥미 유발) +- **적용 시기/계절**: 연중, 특히 월드컵/올림픽 등 스포츠 이벤트 기간 + +### 2-3. 배달앱 리뷰 작성 이벤트 + +- **업종명**: 치킨집 +- **이벤트 제목**: "배민 리뷰 작성하고 다음 주문 15% 할인" +- **이벤트 유형**: 리뷰 + 쿠폰 +- **경품/혜택**: 배달의민족 리뷰 작성 시 다음 주문 15% 할인 쿠폰 (1개월 유효) +- **참여 방법**: 주문 → 배달 완료 후 리뷰 작성 → 자동으로 쿠폰 발급 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 220명 +- **예상 비용**: 1,650,000원 +- **예상 ROI**: 400% +- **성공 요인**: + - 재방문 유도 (쿠폰 유효기간 1개월) + - 배달앱 순위 상승 (리뷰 개수 ↑) + - 신규 고객 유입 증가 +- **적용 시기/계절**: 연중 + +### 2-4. 매출 상생 맞춤형 마케팅 + +- **업종명**: 치킨집 +- **이벤트 제목**: "AI 데이터 분석 기반 개별 프로모션" +- **이벤트 유형**: 창의적 이벤트 (데이터 기반) +- **경품/혜택**: 고객별 구매 패턴 분석 후 맞춤형 할인/쿠폰 제공 +- **참여 방법**: 자동 분석 → 카카오톡/문자로 개별 프로모션 발송 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 280명 +- **예상 비용**: 1,700,000원 +- **예상 ROI**: 385% +- **성공 요인**: + - 개인화 마케팅 (구매 패턴 반영) + - 이탈 고객 재활성화 + - 데이터 기반 의사결정 (자담치킨 사례: +28.4%) +- **적용 시기/계절**: 연중 (월 1회 분석 및 발송) + +### 2-5. 인플루언서 협업 이벤트 + +- **업종명**: 치킨집 +- **이벤트 제목**: "유명 먹방 유튜버와 함께하는 신메뉴 출시" +- **이벤트 유형**: SNS 바이럴 (인플루언서 협업) +- **경품/혜택**: 유튜버 방문 영상 공개 + 신메뉴 20% 할인 (1주일 한정) +- **참여 방법**: 유튜브 영상 시청 → 매장 방문 또는 배달 주문 시 "유튜브 봤어요" 멘트 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 400명 +- **예상 비용**: 5,800,000원 (인플루언서 비용 500만 + 할인 비용) +- **예상 ROI**: 280% +- **성공 요인**: + - 대규모 노출 (유튜버 구독자 수만-수십만) + - 신뢰도 높은 추천 (먹방 유튜버 영향력) + - 신메뉴 빠른 인지도 확보 +- **적용 시기/계절**: 신메뉴 출시 시점 + +--- + +## 3. 카페 (Cafe) + +### 3-1. 모닝 커피 세트 할인 + +- **업종명**: 카페 +- **이벤트 제목**: "오전 7시-10시 모닝 커피+빵 세트 3,500원" +- **이벤트 유형**: 시간대별 번들 프로모션 +- **경품/혜택**: 아메리카노 + 크루아상 세트 3,500원 (정상가 6,500원) +- **참여 방법**: 해당 시간대 매장 방문 또는 테이크아웃 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 250,000원 +- **예상 ROI**: 480% +- **성공 요인**: + - 비수기 시간대 활성화 (아침 시간) + - 직장인 타겟팅 + - 세트 구성으로 객단가 상승 +- **적용 시기/계절**: 연중 (평일 중심) + +### 3-2. 디지털 스탬프 10+1 이벤트 + +- **업종명**: 카페 +- **이벤트 제목**: "커피 10잔 마시면 1잔 무료!" +- **이벤트 유형**: 스탬프/포인트 +- **경품/혜택**: 10회 구매 적립 시 아메리카노 1잔 무료 +- **참여 방법**: 카카오톡 채널 또는 자체 앱 가입 → 구매 시 자동 적립 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 180명 (재방문 포함 약 2,000회) +- **예상 비용**: 270,000원 +- **예상 ROI**: 520% +- **성공 요인**: + - 재방문 유도 (완료율 40-60%) + - 고객 충성도 구축 + - 종이 스탬프 분실 방지 (디지털) +- **적용 시기/계절**: 연중 + +### 3-3. 인스타그램 감성 포토존 이벤트 + +- **업종명**: 카페 +- **이벤트 제목**: "포토존에서 인증샷 올리고 음료 20% 할인" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 포토존 사진 업로드 + 매장 태그 시 20% 할인 +- **참여 방법**: 매장 방문 → 포토존 촬영 → 인스타그램 업로드 + #OO카페 해시태그 → 직원에게 화면 제시 +- **예산 규모**: 중비용 (포토존 제작 포함) +- **예상 참여자 수**: 250명 +- **예상 비용**: 1,800,000원 (포토존 제작 100만 + 할인 비용) +- **예상 ROI**: 340% +- **성공 요인**: + - 바이럴 마케팅 (인스타그래머블 공간) + - 2030 여성 타겟팅 (성수동 카페 사례) + - UGC 자발적 확산 +- **적용 시기/계절**: 연중 (특히 봄/가을 감성 시즌) + +### 3-4. 생일 고객 특별 혜택 + +- **업종명**: 카페 +- **이벤트 제목**: "생일 고객님께 음료 1잔 무료 선물" +- **이벤트 유형**: 재방문 유도 (고객 세그먼트) +- **경품/혜택**: 생일 당월 방문 시 음료 1잔 무료 (5천원 이하 메뉴) +- **참여 방법**: 카카오톡 채널 가입 시 생일 등록 → 생일 달 쿠폰 자동 발송 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 1,500,000원 +- **예상 ROI**: 360% +- **성공 요인**: + - 개인화 마케팅 (특별한 날 혜택) + - 높은 재방문율 (생일 고객 ROI 높음) + - 고객 데이터 수집 +- **적용 시기/계절**: 연중 + +### 3-5. 레트로 콜라보 한정 메뉴 + +- **업종명**: 카페 +- **이벤트 제목**: "감성커피 X 바나나킥 한정 콜라보 메뉴" +- **이벤트 유형**: 창의적 이벤트 (브랜드 협업) +- **경품/혜택**: 바나나킥 라떼 + 카라멜 쉐이크 한정 판매 (2주간) +- **참여 방법**: 매장 방문 또는 배달 주문 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 350명 +- **예상 비용**: 5,200,000원 (협업 비용 + 재료비 + 마케팅) +- **예상 ROI**: 290% +- **성공 요인**: + - 레트로 향수 자극 (MZ세대 타겟) + - 바이럴 화제성 (감성커피 사례: 최소 비용으로 성공) + - 한정판 희소성 +- **적용 시기/계절**: 연중 (트렌드 맞춤형) + +--- + +## 4. 중식당 (Chinese Restaurant) + +### 4-1. 런치 세트 특가 + +- **업종명**: 중식당 +- **이벤트 제목**: "점심 시간 짜장면+탕수육 세트 9,900원" +- **이벤트 유형**: 번들 프로모션 (시간대별) +- **경품/혜택**: 11시-14시 짜장면+소탕수육 세트 9,900원 (정상가 14,000원) +- **참여 방법**: 해당 시간대 방문 또는 배달 주문 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 180명 +- **예상 비용**: 280,000원 +- **예상 ROI**: 380% +- **성공 요인**: + - 직장인 점심 수요 타겟 + - 세트 메뉴로 객단가 유지 + - 배달 시간대 집중 활용 +- **적용 시기/계절**: 연중 (평일) + +### 4-2. 첫 주문 고객 쿠폰 + +- **업종명**: 중식당 +- **이벤트 제목**: "신규 고객 환영! 첫 주문 20% 할인" +- **이벤트 유형**: 쿠폰 (신규 고객 타겟) +- **경품/혜택**: 첫 주문 시 전 메뉴 20% 할인 +- **참여 방법**: 배달앱 신규 주문 고객 자동 적용 또는 매장 방문 시 첫 방문 멘트 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 150명 +- **예상 비용**: 300,000원 +- **예상 ROI**: 340% +- **성공 요인**: + - 신규 고객 확보 + - 높은 재방문 전환율 (30-40%) + - 진입 장벽 낮춤 +- **적용 시기/계절**: 연중 (신규 오픈 또는 신규 고객 확보 필요 시) + +### 4-3. 배달앱 단골 고객 리워드 + +- **업종명**: 중식당 +- **이벤트 제목**: "이달의 단골 고객 5회 주문 시 탕수육 무료" +- **이벤트 유형**: 스탬프/포인트 +- **경품/혜택**: 월 5회 주문 달성 시 소탕수육 1접시 무료 +- **참여 방법**: 배달앱 주문 횟수 자동 집계 → 5회 달성 시 쿠폰 발급 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 1,600,000원 +- **예상 ROI**: 400% +- **성공 요인**: + - 재방문 유도 (월 5회 목표) + - 고객 충성도 강화 + - 배달 주문 비중 증가 +- **적용 시기/계절**: 연중 + +### 4-4. 가족 단위 방문 이벤트 + +- **업종명**: 중식당 +- **이벤트 제목**: "4인 이상 가족 방문 시 음료 무료 + 짬뽕 1개 서비스" +- **이벤트 유형**: 번들 프로모션 (그룹 타겟) +- **경품/혜택**: 4인 이상 가족 방문 시 음료 4잔 + 짬뽕 1그릇 무료 +- **참여 방법**: 매장 방문 시 4인 이상 테이블 예약 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 180명 (약 45그룹) +- **예상 비용**: 1,750,000원 +- **예상 ROI**: 325% +- **성공 요인**: + - 가족 외식 수요 타겟 (주말/공휴일) + - 객단가 상승 (4인 이상 주문) + - 매장 내 식사 활성화 +- **적용 시기/계절**: 주말/공휴일, 특히 어린이날/가족의 달(5월) + +### 4-5. 명절 특별 세트 예약 이벤트 + +- **업종명**: 중식당 +- **이벤트 제목**: "설날 특선 코스 예약 고객 30% 할인" +- **이벤트 유형**: 창의적 이벤트 (계절 한정) +- **경품/혜택**: 설날 전후 1주일 특선 코스 예약 시 30% 할인 +- **참여 방법**: 전화 또는 카카오톡 예약 (3일 전까지) +- **예산 규모**: 고비용 +- **예상 참여자 수**: 280명 (약 70그룹) +- **예상 비용**: 6,000,000원 +- **예상 ROI**: 250% +- **성공 요인**: + - 명절 수요 활용 (가족 외식) + - 사전 예약으로 재고 관리 용이 + - 고객 확보 (명절 후 재방문 유도) +- **적용 시기/계절**: 겨울 (설날 2주 전부터) + +--- + +## 5. 일식당 (Japanese Restaurant) + +### 5-1. 평일 초밥 세트 할인 + +- **업종명**: 일식당 +- **이벤트 제목**: "평일 런치 초밥 세트 25% 할인" +- **이벤트 유형**: 시간대별 할인 +- **경품/혜택**: 월-금 11시-15시 특정 초밥 세트 25% 할인 +- **참여 방법**: 해당 시간대 매장 방문 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 160명 +- **예상 비용**: 290,000원 +- **예상 ROI**: 360% +- **성공 요인**: + - 비수기 시간대 활용 + - 직장인 점심 타겟 + - 고급 메뉴 접근성 향상 +- **적용 시기/계절**: 연중 (평일) + +### 5-2. 인스타그램 사케 리뷰 이벤트 + +- **업종명**: 일식당 +- **이벤트 제목**: "사케와 안주 인스타그램 인증 시 10% 할인" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 사진 업로드 + 해시태그 시 10% 할인 +- **참여 방법**: 매장 방문 → 음식 사진 촬영 → 인스타그램 업로드 + #OO일식당사케 → 직원 확인 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 140명 +- **예상 비용**: 270,000원 +- **예상 ROI**: 310% +- **성공 요인**: + - 3040세대 타겟 (인스타그램 사용층) + - 바이럴 마케팅 + - 프리미엄 이미지 강화 +- **적용 시기/계절**: 연중 + +### 5-3. 스탬프 적립 오마카세 혜택 + +- **업종명**: 일식당 +- **이벤트 제목**: "10회 방문 시 오마카세 20% 할인" +- **이벤트 유형**: 스탬프/포인트 +- **경품/혜택**: 10회 방문 적립 시 오마카세 코스 20% 할인권 +- **참여 방법**: 카카오톡 채널 가입 → 방문 시 스탬프 적립 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 220명 +- **예상 비용**: 1,650,000원 +- **예상 ROI**: 370% +- **성공 요인**: + - 재방문 유도 (10회 목표) + - 고가 메뉴 업셀링 + - 고객 충성도 구축 +- **적용 시기/계절**: 연중 + +### 5-4. 기념일 고객 특별 서비스 + +- **업종명**: 일식당 +- **이벤트 제목**: "생일/기념일 고객 디저트 + 사진 서비스" +- **이벤트 유형**: 재방문 유도 (고객 세그먼트) +- **경품/혜택**: 생일/기념일 방문 시 디저트 무료 + 폴라로이드 사진 증정 +- **참여 방법**: 예약 시 기념일 멘트 → 당일 직원 안내 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 180명 +- **예상 비용**: 1,700,000원 +- **예상 ROI**: 340% +- **성공 요인**: + - 특별한 날 경험 제공 + - 고객 감동 (감성 마케팅) + - 재방문 + 입소문 효과 +- **적용 시기/계절**: 연중 + +### 5-5. 계절 한정 오마카세 VIP 이벤트 + +- **업종명**: 일식당 +- **이벤트 제목**: "봄 제철 오마카세 특별 코스 초대" +- **이벤트 유형**: 창의적 이벤트 (계절 한정) +- **경품/혜택**: 상위 20명 VIP 고객 봄 제철 오마카세 특별 코스 초대 (15% 할인) +- **참여 방법**: 월 2회 이상 방문 고객 대상 개별 초대장 발송 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 20명 (VIP 대상) +- **예상 비용**: 5,500,000원 +- **예상 ROI**: 260% +- **성공 요인**: + - VIP 고객 충성도 강화 + - 계절성 활용 (봄 제철 식재료) + - 고가 메뉴 프로모션 +- **적용 시기/계절**: 봄 (3-5월) + +--- + +## 6. 양식당 (Italian Restaurant) + +### 6-1. 런치 파스타 세트 할인 + +- **업종명**: 양식당 +- **이벤트 제목**: "평일 점심 파스타+샐러드+음료 세트 12,900원" +- **이벤트 유형**: 번들 프로모션 (시간대별) +- **경품/혜택**: 월-금 11시-15시 파스타 세트 12,900원 (정상가 18,000원) +- **참여 방법**: 해당 시간대 매장 방문 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 190명 +- **예상 비용**: 280,000원 +- **예상 ROI**: 400% +- **성공 요인**: + - 직장인 점심 타겟 + - 세트 구성으로 가치 인식 향상 + - 비수기 시간대 활성화 +- **적용 시기/계절**: 연중 (평일) + +### 6-2. 와인 페어링 SNS 이벤트 + +- **업종명**: 양식당 +- **이벤트 제목**: "음식과 와인 페어링 인스타그램 인증 이벤트" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 페어링 사진 업로드 시 와인 1잔 무료 +- **참여 방법**: 매장 방문 → 음식+와인 사진 촬영 → 인스타그램 업로드 + #OO양식당와인페어링 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 130명 +- **예상 비용**: 260,000원 +- **예상 ROI**: 320% +- **성공 요인**: + - 프리미엄 이미지 강화 + - 3040세대 타겟 + - UGC 확산 +- **적용 시기/계절**: 연중 + +### 6-3. 디너 코스 예약 고객 디저트 서비스 + +- **업종명**: 양식당 +- **이벤트 제목**: "디너 코스 예약 시 디저트 무료 업그레이드" +- **이벤트 유형**: 재방문 유도 +- **경품/혜택**: 2일 전 예약 시 디저트 1개 무료 (티라미수/판나코타) +- **참여 방법**: 전화 또는 네이버 예약 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 240명 +- **예상 비용**: 1,600,000원 +- **예상 ROI**: 365% +- **성공 요인**: + - 사전 예약 유도 (재고 관리) + - 디너 시간대 활성화 + - 고객 만족도 향상 +- **적용 시기/계절**: 연중 (특히 주말) + +### 6-4. 커플 기념일 이벤트 + +- **업종명**: 양식당 +- **이벤트 제목**: "커플 기념일 특별 테이블 세팅 + 샴페인 서비스" +- **이벤트 유형**: 창의적 이벤트 (고객 세그먼트) +- **경품/혜택**: 기념일 예약 커플 대상 특별 테이블 세팅 + 샴페인 1병 + 폴라로이드 +- **참여 방법**: 예약 시 기념일 멘트 → 당일 특별 준비 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 1,800,000원 +- **예상 ROI**: 350% +- **성공 요인**: + - 감성 마케팅 (특별한 경험) + - 커플 타겟팅 (밸런타인/화이트데이) + - 입소문 효과 (SNS 공유) +- **적용 시기/계절**: 연중 (특히 2월, 3월, 크리스마스) + +### 6-5. 셰프 특선 시식회 + +- **업종명**: 양식당 +- **이벤트 제목**: "월 1회 셰프 특선 메뉴 시식회 초대" +- **이벤트 유형**: 창의적 이벤트 (VIP) +- **경품/혜택**: 월 1회 VIP 고객 30명 초대, 신메뉴 시식 + 20% 할인권 +- **참여 방법**: 월 2회 이상 방문 고객 대상 카카오톡 초대 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 30명 +- **예상 비용**: 5,300,000원 +- **예상 ROI**: 270% +- **성공 요인**: + - VIP 고객 충성도 강화 + - 신메뉴 피드백 수집 + - 입소문 마케팅 +- **적용 시기/계절**: 연중 (월 1회) + +--- + +## 7. 베이커리 (Bakery) + +### 7-1. 아침 빵 특가 + +- **업종명**: 베이커리 +- **이벤트 제목**: "오전 7시-10시 갓 구운 빵 30% 할인" +- **이벤트 유형**: 시간대별 할인 +- **경품/혜택**: 오전 특정 시간대 빵 30% 할인 +- **참여 방법**: 해당 시간대 매장 방문 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 210명 +- **예상 비용**: 270,000원 +- **예상 ROI**: 450% +- **성공 요인**: + - 아침 시간대 활성화 + - 신선함 강조 (갓 구운 빵) + - 직장인 출근길 타겟 +- **적용 시기/계절**: 연중 (평일 중심) + +### 7-2. 인스타그램 빵 인증 이벤트 + +- **업종명**: 베이커리 +- **이벤트 제목**: "예쁜 빵 사진 인스타그램 올리고 커피 무료" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 빵 사진 업로드 + 해시태그 시 커피 1잔 무료 +- **참여 방법**: 매장 방문 → 빵 사진 촬영 → 인스타그램 업로드 + #OO베이커리 → 직원 확인 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 170명 +- **예상 비용**: 250,000원 +- **예상 ROI**: 380% +- **성공 요인**: + - 바이럴 마케팅 (인스타그래머블) + - 2030 여성 타겟 + - UGC 확산 +- **적용 시기/계절**: 연중 + +### 7-3. 생일 케이크 예약 고객 할인 + +- **업종명**: 베이커리 +- **이벤트 제목**: "생일 케이크 3일 전 예약 시 15% 할인" +- **이벤트 유형**: 쿠폰 (예약 유도) +- **경품/혜택**: 3일 전 예약 시 케이크 15% 할인 + 초 무료 +- **참여 방법**: 전화 또는 카카오톡 예약 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 260명 +- **예상 비용**: 1,700,000원 +- **예상 ROI**: 370% +- **성공 요인**: + - 사전 예약 유도 (재고 관리) + - 생일 수요 확보 + - 고객 데이터 수집 +- **적용 시기/계절**: 연중 + +### 7-4. 월간 신제품 출시 이벤트 + +- **업종명**: 베이커리 +- **이벤트 제목**: "이달의 신상 빵 출시! SNS 공유하고 20% 할인" +- **이벤트 유형**: SNS 바이럴 + 할인 +- **경품/혜택**: 신제품 구매 + SNS 공유 시 20% 할인 +- **참여 방법**: 신제품 구매 → SNS 공유 → 직원 확인 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 230명 +- **예상 비용**: 1,650,000원 +- **예상 ROI**: 350% +- **성공 요인**: + - 신제품 빠른 인지도 확보 + - 바이럴 마케팅 + - 재방문 유도 +- **적용 시기/계절**: 연중 (월 1회) + +### 7-5. 인플루언서 협업 팝업 스토어 + +- **업종명**: 베이커리 +- **이벤트 제목**: "인기 베이킹 유튜버와 함께하는 특별 팝업" +- **이벤트 유형**: 창의적 이벤트 (인플루언서 협업) +- **경품/혜택**: 유튜버 콜라보 빵 한정 판매 + 사인회 + 베이킹 클래스 +- **참여 방법**: 팝업 기간 매장 방문 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 350명 +- **예상 비용**: 5,800,000원 (인플루언서 비용 500만 + 재료비 + 마케팅) +- **예상 ROI**: 280% +- **성공 요인**: + - 대규모 노출 (유튜버 구독자) + - 한정판 희소성 + - 체험 이벤트 (베이킹 클래스) +- **적용 시기/계절**: 특별 시즌 (크리스마스, 밸런타인 등) + +--- + +## 8. 패스트푸드 (Fast Food / Hamburger) + +### 8-1. 점심 시간대 세트 할인 + +- **업종명**: 패스트푸드 +- **이벤트 제목**: "런치 타임 버거 세트 1,000원 할인" +- **이벤트 유형**: 시간대별 할인 +- **경품/혜택**: 11시-14시 버거 세트 1,000원 할인 +- **참여 방법**: 해당 시간대 매장 또는 배달 주문 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 220명 +- **예상 비용**: 280,000원 +- **예상 ROI**: 410% +- **성공 요인**: + - 점심 시간대 집중 공략 + - 직장인 빠른 식사 니즈 + - 객단가 유지 (세트 판매) +- **적용 시기/계절**: 연중 (평일) + +### 8-2. 배달앱 첫 주문 할인 + +- **업종명**: 패스트푸드 +- **이벤트 제목**: "신규 고객 첫 주문 3,000원 할인" +- **이벤트 유형**: 쿠폰 (신규 고객) +- **경품/혜택**: 첫 주문 시 3,000원 할인 +- **참여 방법**: 배달앱 신규 고객 자동 적용 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 180명 +- **예상 비용**: 270,000원 +- **예상 ROI**: 360% +- **성공 요인**: + - 신규 고객 확보 + - 재방문 전환율 30-40% + - 진입 장벽 낮춤 +- **적용 시기/계절**: 연중 + +### 8-3. SNS 인증 이벤트 + +- **업종명**: 패스트푸드 +- **이벤트 제목**: "버거 사진 인스타그램 올리고 감자튀김 무료" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 버거 사진 업로드 시 감자튀김 1개 무료 +- **참여 방법**: 매장 방문 → 버거 사진 촬영 → 인스타그램 업로드 + #OO버거 → 직원 확인 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 250명 +- **예상 비용**: 1,600,000원 +- **예상 ROI**: 340% +- **성공 요인**: + - 바이럴 마케팅 + - 1020세대 타겟 + - 즉시 보상 +- **적용 시기/계절**: 연중 + +### 8-4. 스탬프 적립 무료 버거 + +- **업종명**: 패스트푸드 +- **이벤트 제목**: "버거 8개 먹으면 1개 무료" +- **이벤트 유형**: 스탬프/포인트 +- **경품/혜택**: 8회 구매 적립 시 시그니처 버거 1개 무료 +- **참여 방법**: 앱 가입 → 구매 시 자동 적립 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 280명 +- **예상 비용**: 1,750,000원 +- **예상 ROI**: 390% +- **성공 요인**: + - 재방문 유도 + - 고객 충성도 구축 + - 앱 사용 활성화 +- **적용 시기/계절**: 연중 + +### 8-5. 월드컵 시즌 특별 프로모션 + +- **업종명**: 패스트푸드 +- **이벤트 제목**: "월드컵 응원! 한국 골 넣으면 버거 반값" +- **이벤트 유형**: 창의적 이벤트 (이슈 마케팅) +- **경품/혜택**: 한국 축구팀 골 넣은 날 전 버거 50% 할인 +- **참여 방법**: 경기 당일 매장 또는 배달 주문 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 400명 +- **예상 비용**: 5,500,000원 +- **예상 ROI**: 290% +- **성공 요인**: + - 이슈 마케팅 (월드컵) + - 화제성 확보 + - 대규모 참여 유도 +- **적용 시기/계절**: 월드컵/올림픽 등 스포츠 이벤트 기간 + +--- + +## 9. 피자 전문점 (Pizza Restaurant) + +### 9-1. 평일 피자 1+1 + +- **업종명**: 피자 전문점 +- **이벤트 제목**: "평일 피자 1+1 (포장 주문)" +- **이벤트 유형**: 번들 프로모션 (1+1) +- **경품/혜택**: 월-금 특정 피자 1개 주문 시 1개 무료 (포장만 해당) +- **참여 방법**: 포장 주문 시 자동 적용 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 300,000원 +- **예상 ROI**: 440% +- **성공 요인**: + - 포장 주문 활성화 (배달비 절감) + - 평일 비수기 활용 + - 높은 가치 인식 (1+1) +- **적용 시기/계절**: 연중 (평일) + +### 9-2. 배달앱 리뷰 이벤트 + +- **업종명**: 피자 전문점 +- **이벤트 제목**: "리뷰 작성하고 다음 주문 콜라 2L 무료" +- **이벤트 유형**: 리뷰 + 쿠폰 +- **경품/혜택**: 배달앱 리뷰 작성 시 다음 주문 콜라 2L 무료 쿠폰 +- **참여 방법**: 주문 → 리뷰 작성 → 자동 쿠폰 발급 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 170명 +- **예상 비용**: 260,000원 +- **예상 ROI**: 370% +- **성공 요인**: + - 재방문 유도 + - 배달앱 순위 상승 + - 신규 고객 유입 +- **적용 시기/계절**: 연중 + +### 9-3. 가족 단위 할인 + +- **업종명**: 피자 전문점 +- **이벤트 제목**: "가족 피자 파티! 3판 이상 주문 시 20% 할인" +- **이벤트 유형**: 번들 프로모션 (대량 주문) +- **경품/혜택**: 피자 3판 이상 주문 시 20% 할인 + 콜라 2L 무료 +- **참여 방법**: 매장 또는 배달 주문 시 자동 적용 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 220명 +- **예상 비용**: 1,700,000원 +- **예상 ROI**: 360% +- **성공 요인**: + - 가족/파티 수요 타겟 + - 객단가 상승 + - 주말/공휴일 활성화 +- **적용 시기/계절**: 주말, 특히 어린이날/크리스마스 + +### 9-4. 월간 신메뉴 출시 이벤트 + +- **업종명**: 피자 전문점 +- **이벤트 제목**: "이달의 신메뉴 피자 출시! SNS 공유하고 15% 할인" +- **이벤트 유형**: SNS 바이럴 + 할인 +- **경품/혜택**: 신메뉴 주문 + SNS 공유 시 15% 할인 +- **참여 방법**: 신메뉴 주문 → SNS 공유 → 직원 확인 또는 자동 적용 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 240명 +- **예상 비용**: 1,650,000원 +- **예상 ROI**: 350% +- **성공 요인**: + - 신메뉴 빠른 인지도 + - 바이럴 마케팅 + - 재방문 유도 +- **적용 시기/계절**: 연중 (월 1회) + +### 9-5. 스포츠 시즌 특별 프로모션 + +- **업종명**: 피자 전문점 +- **이벤트 제목**: "야구 시즌 응원! 홈런 1개당 피자 1,000원 할인" +- **이벤트 유형**: 창의적 이벤트 (이슈 마케팅) +- **경품/혜택**: 해당 날 홈런 개수만큼 피자 1,000원씩 할인 (최대 5,000원) +- **참여 방법**: 경기 당일 주문 시 자동 적용 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 380명 +- **예상 비용**: 5,700,000원 +- **예상 ROI**: 270% +- **성공 요인**: + - 이슈 마케팅 (스포츠) + - 화제성 확보 + - 반복 참여 가능 (시즌 내내) +- **적용 시기/계절**: 봄-가을 (야구 시즌 3-10월) + +--- + +## 10. 바베큐/고깃집 (Barbecue / Steak House) + +### 10-1. 평일 점심 특가 + +- **업종명**: 바베큐/고깃집 +- **이벤트 제목**: "평일 런치 삼겹살 세트 1인 12,900원" +- **이벤트 유형**: 시간대별 할인 +- **경품/혜택**: 월-금 11시-15시 삼겹살 세트 1인 12,900원 +- **참여 방법**: 해당 시간대 매장 방문 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 180명 +- **예상 비용**: 290,000원 +- **예상 ROI**: 380% +- **성공 요인**: + - 비수기 시간대 활용 + - 직장인 점심 타겟 + - 세트 구성으로 객단가 유지 +- **적용 시기/계절**: 연중 (평일) + +### 10-2. 소셜 미디어 고기 인증 이벤트 + +- **업종명**: 바베큐/고깃집 +- **이벤트 제목**: "고기 굽는 영상 릴스 올리고 소주 2병 서비스" +- **이벤트 유형**: SNS 바이럴 +- **경품/혜택**: 인스타그램 릴스 업로드 시 소주 2병 무료 +- **참여 방법**: 매장 방문 → 고기 굽는 영상 촬영 → 릴스 업로드 + #OO고깃집 → 직원 확인 +- **예산 규모**: 저비용 +- **예상 참여자 수**: 160명 +- **예상 비용**: 270,000원 +- **예상 ROI**: 340% +- **성공 요인**: + - 바이럴 마케팅 (영상 콘텐츠) + - 2030세대 타겟 + - 즉시 보상 +- **적용 시기/계절**: 연중 + +### 10-3. 단체 예약 할인 + +- **업종명**: 바베큐/고깃집 +- **이벤트 제목**: "6인 이상 단체 예약 15% 할인 + 된장찌개 서비스" +- **이벤트 유형**: 번들 프로모션 (그룹 타겟) +- **경품/혜택**: 6인 이상 예약 시 15% 할인 + 된장찌개 무료 +- **참여 방법**: 전화 또는 네이버 예약 (1일 전까지) +- **예산 규모**: 중비용 +- **예상 참여자 수**: 230명 (약 40그룹) +- **예상 비용**: 1,750,000원 +- **예상 ROI**: 370% +- **성공 요인**: + - 단체 고객 확보 (회식/모임) + - 객단가 상승 + - 사전 예약으로 준비 용이 +- **적용 시기/계절**: 연중 (특히 연말 회식 시즌) + +### 10-4. 생일 고객 특별 서비스 + +- **업종명**: 바베큐/고깃집 +- **이벤트 제목**: "생일 고객님께 등심 200g + 케이크 서비스" +- **이벤트 유형**: 재방문 유도 (고객 세그먼트) +- **경품/혜택**: 생일 당월 방문 시 등심 200g + 미니 케이크 무료 +- **참여 방법**: 예약 또는 방문 시 생일 멘트 → 신분증 확인 +- **예산 규모**: 중비용 +- **예상 참여자 수**: 200명 +- **예상 비용**: 1,800,000원 +- **예상 ROI**: 355% +- **성공 요인**: + - 개인화 마케팅 + - 특별한 날 경험 + - 재방문 + 입소문 +- **적용 시기/계절**: 연중 + +### 10-5. 프리미엄 한우 시식회 + +- **업종명**: 바베큐/고깃집 +- **이벤트 제목**: "VIP 고객 초대 프리미엄 한우 시식회" +- **이벤트 유형**: 창의적 이벤트 (VIP) +- **경품/혜택**: 상위 30명 VIP 고객 한우 시식회 초대 + 25% 할인권 +- **참여 방법**: 월 2회 이상 방문 고객 대상 개별 초대 +- **예산 규모**: 고비용 +- **예상 참여자 수**: 30명 +- **예상 비용**: 6,200,000원 +- **예상 ROI**: 250% +- **성공 요인**: + - VIP 고객 충성도 강화 + - 프리미엄 메뉴 프로모션 + - 입소문 마케팅 +- **적용 시기/계절**: 특별 시즌 (설날/추석 전) + +--- + +## 데이터 활용 가이드 + +### AI 학습 데이터 구성 + +본 데이터는 다음과 같이 AI 이벤트 추천 시스템에 활용됩니다: + +1. **벡터 임베딩 생성** + - 각 이벤트 데이터를 텍스트로 변환 + - OpenAI Embeddings API를 통해 벡터화 + - Pinecone에 저장하여 유사도 검색 지원 + +2. **유사 이벤트 검색** + - 사용자의 매장 정보(업종, 지역, 예산)를 쿼리로 사용 + - 코사인 유사도 기반 상위 5개 유사 이벤트 추출 + - Claude API 프롬프트에 컨텍스트로 제공 + +3. **추천 정확도 향상** + - 실제 성공 사례 기반 데이터로 신뢰도 향상 + - ROI, 참여자 수 등 구체적 수치로 현실적 예측 가능 + - 계절성, 업종 특성 반영으로 맞춤형 추천 + +### 데이터 확장 방안 + +초기 50개 이벤트 데이터를 기반으로 다음과 같이 확장 가능: + +1. **자사 데이터 축적** + - 사용자가 생성한 이벤트 자동 수집 + - 실제 성과 데이터 업데이트 + - 피드백 반영한 정확도 개선 + +2. **외부 데이터 수집** + - SNS 크롤링 (네이버 블로그, 인스타그램) + - 공공데이터 API 활용 + - 배달 플랫폼 이벤트 분석 + +3. **업종 확장** + - 추가 외식업 카테고리 (태국, 베트남, 디저트 전문점 등) + - 비외식업 소상공인 (소매, 서비스업) + - 프랜차이즈 vs 개인 사업자 구분 + +--- + +## 참고 문헌 + +- 자담치킨 매출상생 프로젝트 사례 (2023) +- 크치치킨 브랜드 전환 사례 +- 홈플러스 당당치킨 710만팩 판매 사례 +- 아뜨베 베이커리 6년 성장 사례 +- 김현숙, 심성욱, 김운한 (2011). 한국광고홍보학보. 체험 마케팅 연구 +- 정민서 외 (2017). 빅데이터학회. 프로모션 타이밍 연구 +- 엄해정, 진현정 (2024). 호텔경영학연구. 리뷰 마케팅 연구 +- 한국외식산업경영연구원 (2024-2025). 외식 트렌드 보고서 + +--- + +**문서 끝** diff --git a/design/aidata/지역별_트렌드_데이터.md b/design/aidata/지역별_트렌드_데이터.md new file mode 100644 index 0000000..cdeab84 --- /dev/null +++ b/design/aidata/지역별_트렌드_데이터.md @@ -0,0 +1,955 @@ +# 지역별 트렌드 데이터 + +**작성일**: 2025-10-21 +**버전**: 1.0 +**목적**: AI 이벤트 추천 시스템을 위한 시군구별 상권 트렌드 및 이벤트 성공 패턴 데이터 + +--- + +## 데이터 구조 설명 + +각 시군구별 트렌드 데이터는 다음 정보를 포함합니다: + +- **지역 특성**: 상권 유형, 주요 고객층, 유동인구 특징 +- **업종별 트렌드**: 음식점, 소매점, 서비스업 등 업종별 성공 패턴 +- **계절별 소비 패턴**: 시즌별 소비 특성 및 이벤트 적기 +- **성공 이벤트 사례**: 실제 성공한 이벤트 유형 및 ROI +- **추천 전략**: AI가 활용할 이벤트 설계 인사이트 + +--- + +## 서울특별시 + +### 종로구 +**지역 특성**: +- 상권 유형: 관광지 + 전통시장 + 오피스 +- 주요 고객층: 관광객(40%), 직장인(35%), 지역주민(25%) +- 유동인구: 평일 32만명, 주말 45만명 +- 특징: 인사동, 광화문 오피스, 북촌 한옥마을 + +**업종별 트렌드**: +- 음식점(한식): 전통 한정식, 궁중요리 체험형 이벤트 효과적 + - 평균 객단가: 25,000원 + - 재방문율: 35% + - 외국인 비율: 45% +- 카페: 전통 찻집 + 현대식 카페 공존 + - SNS 바이럴 중요도: 상 + - 인스타그래머블 요소 필수 + +**계절별 소비 패턴**: +- 봄(3-5월): 외국인 관광객 급증 (+40%), 한복 체험 연계 이벤트 +- 여름(6-8월): 냉면/빙수 이벤트, 야간 관광 타겟 +- 가을(9-11월): 단풍 시즌 패키지 이벤트 (+35% 매출) +- 겨울(12-2월): 전통주 이벤트, 따뜻한 국물 요리 + +**성공 이벤트 사례**: +```json +{ + "event_type": "문화 체험 연계", + "title": "한복 입고 오시면 한정식 20% 할인", + "업종": "한식당", + "기간": "2주", + "예산": "500,000원", + "참여자": "280명", + "매출_증가": "450%", + "ROI": "520%" +} +``` + +**추천 전략**: +- 외국인 타겟: 영어/일어/중국어 메뉴 + 문화 체험 연계 +- 직장인 타겟: 점심 특가(11:30-13:30), 주중 할인 +- 관광객 타겟: SNS 인증 이벤트, 사진 촬영 스팟 제공 + +--- + +### 중구 +**지역 특성**: +- 상권 유형: 금융·상업 중심지 + 명동 관광특구 +- 주요 고객층: 직장인(50%), 관광객(35%), 쇼핑객(15%) +- 유동인구: 평일 50만명, 주말 65만명 +- 특징: 명동 패션거리, 을지로 금융가, 남대문시장 + +**업종별 트렌드**: +- 음식점(퓨전/외식): 빠른 회전율, 트렌디한 메뉴 + - 평균 객단가: 15,000원 + - 회전율: 1.5회/테이블 + - 배달 비중: 25% +- 화장품/뷰티: K-뷰티 체험형 이벤트 + - 외국인 매출 비중: 60% + +**계절별 소비 패턴**: +- 봄: 중국인 단체 관광객 (+50%), 세트 메뉴 인기 +- 여름: 냉방 매장 선호, 시원한 음료 이벤트 +- 가을: 쇼핑 시즌 연계 이벤트 효과적 +- 겨울: 크리스마스 특수, 커플 패키지 + +**성공 이벤트 사례**: +```json +{ + "event_type": "시간대별 할인", + "title": "런치타임 세트 메뉴 30% 할인", + "업종": "퓨전 레스토랑", + "기간": "1개월", + "예산": "1,200,000원", + "참여자": "1,850명", + "매출_증가": "320%", + "ROI": "480%" +} +``` + +**추천 전략**: +- 직장인: 빠른 서비스 + 가성비 세트메뉴 +- 관광객: 포토존 + SNS 이벤트 + 다국어 안내 +- 쇼핑객: 영수증 합산 할인, 인근 매장 제휴 + +--- + +### 강남구 +**지역 특성**: +- 상권 유형: 프리미엄 상업지구 + 오피스 +- 주요 고객층: 직장인(45%), 고소득층(30%), MZ세대(25%) +- 유동인구: 평일 80만명, 주말 55만명 +- 특징: 강남역, 신논현역, 청담동 명품거리 + +**업종별 트렌드**: +- 음식점(고급/프리미엄): 품질 중시, 프라이버시, 차별화된 경험 + - 평균 객단가: 45,000원 + - 재방문율: 55% + - 예약률: 70% +- 카페(프리미엄): 브랜드 가치, 인테리어, 독특한 메뉴 + - 객단가: 8,000원 + - SNS 중요도: 최상 + +**계절별 소비 패턴**: +- 봄: 야외 테라스 이벤트, 브런치 세트 +- 여름: 프리미엄 디저트, 시그니처 음료 +- 가을: 와인 페어링 이벤트, 시즌 한정 메뉴 +- 겨울: VIP 초대 이벤트, 연말 모임 패키지 + +**성공 이벤트 사례**: +```json +{ + "event_type": "VIP 멤버십", + "title": "월간 와인 페어링 클래스 + 10% 할인", + "업종": "파인다이닝", + "기간": "3개월", + "예산": "3,500,000원", + "참여자": "120명", + "매출_증가": "280%", + "ROI": "420%", + "LTV_증가": "+65%" +} +``` + +**추천 전략**: +- 고소득층: VIP 프로그램, 독점 경험, 프리미엄 혜택 +- 직장인: 비즈니스 런치 세트, 회식 패키지 +- MZ세대: 인스타그래머블 메뉴, 팝업 이벤트 + +--- + +### 송파구 +**지역 특성**: +- 상권 유형: 대규모 상업시설 + 주거지역 +- 주요 고객층: 가족(40%), 직장인(30%), 젊은층(30%) +- 유동인구: 평일 35만명, 주말 60만명 +- 특징: 롯데월드, 잠실역 상권, 올림픽공원 + +**업종별 트렌드**: +- 음식점(패밀리): 넓은 공간, 키즈 메뉴, 주차 편의 + - 평균 객단가: 35,000원 (가족 단위) + - 주말 매출 비중: 65% + - 어린이 동반: 70% +- 베이커리/디저트: 테이크아웃 중심, 선물용 수요 + - 평균 객단가: 25,000원 + +**계절별 소비 패턴**: +- 봄: 야외 활동 증가, 피크닉 패키지 +- 여름: 롯데월드 연계, 키즈 이벤트 +- 가을: 운동회 시즌, 단체 주문 증가 +- 겨울: 실내 활동 증가, 생일파티 패키지 + +**성공 이벤트 사례**: +```json +{ + "event_type": "가족 패키지", + "title": "롯데월드 티켓 인증 시 키즈 메뉴 무료", + "업종": "패밀리 레스토랑", + "기간": "2개월", + "예산": "2,000,000원", + "참여자": "680가족", + "매출_증가": "380%", + "ROI": "550%" +} +``` + +**추천 전략**: +- 가족: 키즈존, 무료 음료 리필, 생일 이벤트 +- 직장인: 테이크아웃 할인, 배달 프로모션 +- 관광객: 롯데월드 연계 할인, 기념품 증정 + +--- + +## 부산광역시 + +### 해운대구 +**지역 특성**: +- 상권 유형: 관광 특화 지역 + 해양 리조트 +- 주요 고객층: 관광객(60%), 지역주민(25%), 외국인(15%) +- 유동인구: 평일 25만명, 주말/성수기 80만명 +- 특징: 해운대 해수욕장, 동백섬, 마린시티 + +**업종별 트렌드**: +- 음식점(해산물/횟집): 신선도 강조, 계절 메뉴 + - 평균 객단가: 40,000원 + - 외지인 비율: 75% + - 예약률: 60% +- 카페(오션뷰): 뷰 맛집, 포토존 + - 객단가: 7,000원 + - 체류시간: 평균 90분 + +**계절별 소비 패턴**: +- 봄: 벚꽃 시즌, 야외 테라스 (+30%) +- 여름: 성수기 피크, 해산물 이벤트 (+150%) +- 가을: 영화제 시즌, 고급 다이닝 수요 +- 겨울: 비수기 할인, 지역주민 타겟 + +**성공 이벤트 사례**: +```json +{ + "event_type": "계절 한정", + "title": "여름 특선 해산물 세트 + 오션뷰 석 우선 배정", + "업종": "해산물 레스토랑", + "기간": "3개월 (6-8월)", + "예산": "4,500,000원", + "참여자": "1,250명", + "매출_증가": "520%", + "ROI": "680%" +} +``` + +**추천 전략**: +- 관광객: SNS 인증 이벤트, 포토존, 기념품 +- 외국인: 영어/중국어/일어 메뉴, 문화 체험 +- 지역주민: 비수기 할인, 로컬 멤버십 + +--- + +### 부산진구 +**지역 특성**: +- 상권 유형: 상업 중심지 + 교통 허브 +- 주요 고객층: 직장인(45%), 대학생(30%), 쇼핑객(25%) +- 유동인구: 평일 45만명, 주말 50만명 +- 특징: 서면 상권, 부산역, 대학가 + +**업종별 트렌드**: +- 음식점(한식/분식): 가성비 중시, 빠른 회전 + - 평균 객단가: 9,000원 + - 회전율: 2회/테이블 + - 배달 비중: 40% +- 카페(스터디): 장시간 체류, 조용한 분위기 + - 객단가: 5,000원 + - 체류시간: 3-4시간 + +**성공 이벤트 사례**: +```json +{ + "event_type": "학생 할인", + "title": "학생증 제시 시 20% 할인 + 음료 리필 무료", + "업종": "카페", + "기간": "학기 중 4개월", + "예산": "800,000원", + "참여자": "2,100명", + "매출_증가": "280%", + "ROI": "450%" +} +``` + +--- + +## 대구광역시 + +### 중구 +**지역 특성**: +- 상권 유형: 구도심 + 전통시장 + 청년몰 +- 주요 고객층: 지역주민(50%), 청년(30%), 관광객(20%) +- 유동인구: 평일 30만명, 주말 38만명 +- 특징: 동성로, 서문시장, 약령시장 + +**업종별 트렌드**: +- 음식점(향토음식): 전통 대구 요리, 지역 특산물 + - 평균 객단가: 12,000원 + - 재방문율: 60% (지역민) + - 관광객 비율: 25% + +**성공 이벤트 사례**: +```json +{ + "event_type": "로컬 협업", + "title": "서문시장 영수증 합산 이벤트", + "업종": "한식당", + "기간": "1개월", + "예산": "600,000원", + "참여자": "520명", + "매출_증가": "340%", + "ROI": "490%" +} +``` + +--- + +## 인천광역시 + +### 중구 +**지역 특성**: +- 상권 유형: 국제공항 + 차이나타운 + 월미도 +- 주요 고객층: 관광객(50%), 외국인(25%), 지역주민(25%) +- 유동인구: 평일 40만명, 주말 65만명 +- 특징: 인천공항, 차이나타운, 개항장 문화지구 + +**업종별 트렌드**: +- 음식점(중식): 짜장면/짬뽕 특화, SNS 핫플 + - 평균 객단가: 15,000원 + - 외지인 비율: 70% + - 포장/배달: 35% + +**성공 이벤트 사례**: +```json +{ + "event_type": "SNS 바이럴", + "title": "인생샷 스팟 인증 시 짜장면 무료 곱빼기", + "업종": "중식당", + "기간": "2주", + "예산": "450,000원", + "참여자": "380명", + "매출_증가": "410%", + "ROI": "580%", + "SNS_도달": "12만명" +} +``` + +--- + +## 광주광역시 + +### 동구 +**지역 특성**: +- 상권 유형: 예술문화 중심지 + 전통시장 +- 주요 고객층: 문화애호가(35%), 지역주민(40%), 대학생(25%) +- 유동인구: 평일 22만명, 주말 30만명 +- 특징: 아시아문화전당, 충장로, 동명동 카페거리 + +**업종별 트렌드**: +- 카페(감성): 독립서점 연계, 전시 공간 + - 평균 객단가: 6,500원 + - 체류시간: 120분 + - 문화 이벤트 연계 효과: +45% + +**성공 이벤트 사례**: +```json +{ + "event_type": "문화 협업", + "title": "전시 티켓 인증 시 음료 1+1", + "업종": "카페", + "기간": "전시 기간 2개월", + "예산": "700,000원", + "참여자": "650명", + "매출_증가": "320%", + "ROI": "470%" +} +``` + +--- + +## 대전광역시 + +### 유성구 +**지역 특성**: +- 상권 유형: 대학가 + 연구단지 + 온천 관광 +- 주요 고객층: 대학생(40%), 연구원(30%), 관광객(30%) +- 유동인구: 평일 35만명, 주말 42만명 +- 특징: KAIST, 충남대, 대덕연구단지, 유성온천 + +**업종별 트렌드**: +- 음식점(퓨전/창업): 실험적 메뉴, 트렌디한 콘셉트 + - 평균 객단가: 13,000원 + - 학생 비율: 55% + - SNS 마케팅 효과: 상 + +**성공 이벤트 사례**: +```json +{ + "event_type": "학생 타겟", + "title": "시험기간 특별 이벤트: 야식 세트 30% 할인", + "업종": "분식/야식", + "기간": "중간·기말고사 각 2주", + "예산": "500,000원", + "참여자": "1,200명", + "매출_증가": "450%", + "ROI": "620%" +} +``` + +--- + +## 울산광역시 + +### 남구 +**지역 특성**: +- 상권 유형: 공업도시 + 주거지역 +- 주요 고객층: 직장인(60%), 가족(30%), 대학생(10%) +- 유동인구: 평일 28만명, 주말 25만명 +- 특징: 현대자동차, 삼산동 상권 + +**업종별 트렌드**: +- 음식점(고깃집/회식): 대용량, 단체 할인 + - 평균 객단가: 30,000원 (1인 기준) + - 단체 예약 비율: 70% + - 평일 저녁 집중: 65% + +**성공 이벤트 사례**: +```json +{ + "event_type": "단체 할인", + "title": "4인 이상 예약 시 삼겹살 500g 추가 증정", + "업종": "고깃집", + "기간": "1개월", + "예산": "1,500,000원", + "참여자": "280팀 (1,150명)", + "매출_증가": "380%", + "ROI": "520%" +} +``` + +--- + +## 경기도 + +### 수원시 +**지역 특성**: +- 상권 유형: 역사관광 + 주거·상업 복합 +- 주요 고객층: 지역주민(45%), 관광객(30%), 직장인(25%) +- 유동인구: 평일 60만명, 주말 75만명 +- 특징: 수원화성, 행궁동 벽화마을, 수원역 상권 + +**업종별 트렌드**: +- 음식점(한식/수원갈비): 전통 + 현대화 + - 평균 객단가: 35,000원 + - 관광객 비율: 40% + - 재방문율: 50% + +**성공 이벤트 사례**: +```json +{ + "event_type": "관광 연계", + "title": "수원화성 입장권 인증 시 20% 할인 + 전통차 서비스", + "업종": "한정식", + "기간": "2개월", + "예산": "1,200,000원", + "참여자": "580명", + "매출_증가": "360%", + "ROI": "510%" +} +``` + +--- + +### 성남시 +**지역 특성**: +- 상권 유형: IT단지 + 대규모 주거단지 +- 주요 고객층: 직장인(50%), 가족(30%), 청년(20%) +- 유동인구: 평일 55만명, 주말 45만명 +- 특징: 판교 테크노밸리, 분당 신도시 + +**업종별 트렌드**: +- 카페(프리미엄): 업무·미팅 공간, 조용한 환경 + - 평균 객단가: 7,500원 + - IT 직장인 비율: 65% + - 평일 점심 집중: 40% + +**성공 이벤트 사례**: +```json +{ + "event_type": "직장인 타겟", + "title": "오전 10시 이전 아메리카노 50% 할인", + "업종": "카페", + "기간": "평일 1개월", + "예산": "350,000원", + "참여자": "1,850명", + "매출_증가": "280%", + "ROI": "450%" +} +``` + +--- + +### 고양시 +**지역 특성**: +- 상권 유형: 대규모 상업시설 + 신도시 +- 주요 고객층: 가족(45%), 쇼핑객(30%), 직장인(25%) +- 유동인구: 평일 50만명, 주말 80만명 +- 특징: 킨텍스, 일산 호수공원, 라페스타 + +**업종별 트렌드**: +- 음식점(패밀리/뷔페): 대형 매장, 주차 편의 + - 평균 객단가: 25,000원 + - 주말 매출 비중: 70% + - 가족 단위: 80% + +**성공 이벤트 사례**: +```json +{ + "event_type": "주말 가족", + "title": "주말 브런치 뷔페 키즈 50% 할인", + "업종": "뷔페 레스토랑", + "기간": "2개월 (주말만)", + "예산": "2,500,000원", + "참여자": "920가족", + "매출_증가": "420%", + "ROI": "580%" +} +``` + +--- + +## 강원특별자치도 + +### 춘천시 +**지역 특성**: +- 상권 유형: 관광도시 + 대학가 +- 주요 고객층: 관광객(45%), 대학생(30%), 지역주민(25%) +- 유동인구: 평일 18만명, 주말 35만명 +- 특징: 남이섬, 소양강댐, 명동 닭갈비거리 + +**업종별 트렌드**: +- 음식점(닭갈비): 지역 특화 메뉴 + - 평균 객단가: 15,000원 + - 외지인 비율: 70% + - SNS 인증 효과: 상 + +**성공 이벤트 사례**: +```json +{ + "event_type": "지역 특산", + "title": "춘천 3대 명소 인증 시 닭갈비 1인분 추가", + "업종": "닭갈비", + "기간": "관광 성수기 3개월", + "예산": "1,800,000원", + "참여자": "1,450명", + "매출_증가": "480%", + "ROI": "640%" +} +``` + +--- + +### 강릉시 +**지역 특성**: +- 상권 유형: 해양관광 + 커피 도시 +- 주요 고객층: 관광객(70%), 지역주민(20%), 외국인(10%) +- 유동인구: 평일 15만명, 주말/성수기 60만명 +- 특징: 경포대, 안목해변 커피거리, 오죽헌 + +**업종별 트렌드**: +- 카페(로스터리): 직접 로스팅, 오션뷰 + - 평균 객단가: 8,000원 + - 관광객 비율: 80% + - 원두 판매 비중: 25% + +**성공 이벤트 사례**: +```json +{ + "event_type": "체험형", + "title": "커피 로스팅 클래스 참여 시 원두 20% 할인", + "업종": "로스터리 카페", + "기간": "3개월", + "예산": "900,000원", + "참여자": "180명", + "매출_증가": "520%", + "ROI": "720%", + "원두_판매": "+350%" +} +``` + +--- + +## 충청북도 + +### 청주시 +**지역 특성**: +- 상권 유형: 지역 중심도시 + 교육도시 +- 주요 고객층: 지역주민(50%), 대학생(25%), 직장인(25%) +- 유동인구: 평일 35만명, 주말 32만명 +- 특징: 청주대, 충북대, 성안길 상권 + +**업종별 트렌드**: +- 음식점(한식/분식): 가성비 + 넉넉한 양 + - 평균 객단가: 9,000원 + - 학생 비율: 45% + - 배달 비중: 50% + +**성공 이벤트 사례**: +```json +{ + "event_type": "시간대 할인", + "title": "오후 2-5시 한정 점심 특가 세트", + "업종": "한식당", + "기간": "1개월", + "예산": "400,000원", + "참여자": "780명", + "매출_증가": "310%", + "ROI": "480%" +} +``` + +--- + +## 충청남도 + +### 천안시 +**지역 특성**: +- 상권 유형: 교통 요충지 + 공업단지 +- 주요 고객층: 직장인(55%), 유동인구(25%), 지역주민(20%) +- 유동인구: 평일 45만명, 주말 35만명 +- 특징: 천안역/천안아산역, 삼성디스플레이 + +**업종별 트렌드**: +- 음식점(빠른식사): 속도 중시, 테이크아웃 + - 평균 객단가: 8,000원 + - 회전율: 3회/테이블 + - 포장 비중: 40% + +**성공 이벤트 사례**: +```json +{ + "event_type": "출퇴근 타겟", + "title": "KTX 승차권 인증 시 즉석 10% 할인", + "업종": "분식/김밥", + "기간": "2개월", + "예산": "600,000원", + "참여자": "1,650명", + "매출_증가": "340%", + "ROI": "510%" +} +``` + +--- + +## 전북특별자치도 + +### 전주시 +**지역 특성**: +- 상권 유형: 문화관광 + 한옥마을 +- 주요 고객층: 관광객(60%), 지역주민(25%), 외국인(15%) +- 유동인구: 평일 25만명, 주말 55만명 +- 특징: 전주한옥마을, 전통 한식, 전주비빔밥 + +**업종별 트렌드**: +- 음식점(한식/비빔밥): 전통 음식 특화 + - 평균 객단가: 12,000원 + - 관광객 비율: 75% + - SNS 중요도: 최상 + +**성공 이벤트 사례**: +```json +{ + "event_type": "전통 체험", + "title": "한복 대여 인증 시 전주비빔밥 15% 할인 + 전통차", + "업종": "한정식", + "기간": "3개월", + "예산": "1,500,000원", + "참여자": "1,280명", + "매출_증가": "440%", + "ROI": "610%" +} +``` + +--- + +## 전라남도 + +### 여수시 +**지역 특성**: +- 상권 유형: 해양관광 + 산업도시 +- 주요 고객층: 관광객(65%), 지역주민(25%), 직장인(10%) +- 유동인구: 평일 20만명, 주말/성수기 70만명 +- 특징: 여수밤바다, 오동도, 해양 엑스포 + +**업종별 트렌드**: +- 음식점(해산물): 신선한 활어, 계절 특선 + - 평균 객단가: 35,000원 + - 관광객 비율: 80% + - 여름 성수기 매출: 전체의 50% + +**성공 이벤트 사례**: +```json +{ + "event_type": "계절 특선", + "title": "여름 밤바다 특선 세트 + 야경 포토존", + "업종": "해산물 레스토랑", + "기간": "여름 3개월", + "예산": "3,500,000원", + "참여자": "2,100명", + "매출_증가": "580%", + "ROI": "720%" +} +``` + +--- + +## 경상북도 + +### 포항시 +**지역 특성**: +- 상권 유형: 산업도시 + 해양도시 +- 주요 고객층: 직장인(50%), 지역주민(30%), 관광객(20%) +- 유동인구: 평일 35만명, 주말 30만명 +- 특징: 포스코, 호미곶, 죽도시장 + +**업종별 트렌드**: +- 음식점(회/과메기): 지역 특산물 + - 평균 객단가: 30,000원 + - 직장인 비율: 60% + - 겨울 과메기 시즌: +200% + +**성공 이벤트 사례**: +```json +{ + "event_type": "계절 한정", + "title": "과메기 시즌 특가: 세트 메뉴 30% 할인", + "업종": "해산물 전문점", + "기간": "겨울 2개월", + "예산": "1,800,000원", + "참여자": "850명", + "매출_증가": "620%", + "ROI": "780%" +} +``` + +--- + +### 경주시 +**지역 특성**: +- 상권 유형: 역사문화 관광도시 +- 주요 고객층: 관광객(70%), 외국인(15%), 지역주민(15%) +- 유동인구: 평일 30만명, 주말 80만명 +- 특징: 불국사, 석굴암, 첨성대, 황리단길 + +**업종별 트렌드**: +- 카페(전통+현대): 한옥 카페, SNS 핫플 + - 평균 객단가: 7,000원 + - 관광객 비율: 85% + - 인스타그램 중요도: 최상 + +**성공 이벤트 사례**: +```json +{ + "event_type": "문화유산 연계", + "title": "경주 3대 유적지 스탬프 투어 완료 시 디저트 무료", + "업종": "한옥 카페", + "기간": "관광 성수기 4개월", + "예산": "2,200,000원", + "참여자": "1,680명", + "매출_증가": "490%", + "ROI": "650%" +} +``` + +--- + +## 경상남도 + +### 창원시 +**지역 특성**: +- 상권 유형: 공업도시 + 행정중심지 +- 주요 고객층: 직장인(60%), 가족(25%), 대학생(15%) +- 유동인구: 평일 50만명, 주말 35만명 +- 특징: 창원산업단지, 성산아트홀, 용지호수공원 + +**업종별 트렌드**: +- 음식점(고깃집/회식): 단체 회식 문화 + - 평균 객단가: 28,000원 + - 직장인 비율: 75% + - 평일 저녁 집중: 80% + +**성공 이벤트 사례**: +```json +{ + "event_type": "법인카드 타겟", + "title": "10인 이상 예약 시 음료 무제한 + 후식 서비스", + "업종": "고깃집", + "기간": "2개월", + "예산": "2,500,000원", + "참여자": "180팀 (2,350명)", + "매출_증가": "450%", + "ROI": "580%" +} +``` + +--- + +## 제주특별자치도 + +### 제주시 +**지역 특성**: +- 상권 유형: 관광 특화 지역 +- 주요 고객층: 관광객(75%), 외국인(15%), 지역주민(10%) +- 유동인구: 평일 40만명, 주말 90만명 (성수기) +- 특징: 동문시장, 제주공항, 올레길, 카페거리 + +**업종별 트렌드**: +- 음식점(흑돼지/해산물): 제주 특산물 + - 평균 객단가: 40,000원 + - 관광객 비율: 90% + - 예약률: 70% +- 카페(오션뷰): 자연 경관 활용 + - 객단가: 8,500원 + - 체류시간: 90분 + - SNS 바이럴 필수 + +**성공 이벤트 사례**: +```json +{ + "event_type": "렌터카 제휴", + "title": "렌터카 영수증 인증 시 흑돼지 구이 20% 할인", + "업종": "제주 향토음식점", + "기간": "3개월", + "예산": "3,000,000원", + "참여자": "1,850명", + "매출_증가": "520%", + "ROI": "680%" +} +``` + +--- + +### 서귀포시 +**지역 특성**: +- 상권 유형: 자연관광 + 리조트 +- 주요 고객층: 관광객(80%), 허니문(15%), 지역주민(5%) +- 유동인구: 평일 25만명, 주말 60만명 +- 특징: 중문관광단지, 천지연폭포, 섭지코지 + +**업종별 트렌드**: +- 음식점(프리미엄): 고급 식재료, 특별한 경험 + - 평균 객단가: 55,000원 + - 커플 비율: 70% + - 예약 필수: 90% + +**성공 이벤트 사례**: +```json +{ + "event_type": "기념일 특화", + "title": "결혼기념일/생일 증빙 시 디저트 플레이팅 + 사진 촬영", + "업종": "파인다이닝", + "기간": "연중", + "예산": "1,500,000원/월", + "참여자": "280커플/월", + "매출_증가": "380%", + "ROI": "560%", + "재방문율": "+45%" +} +``` + +--- + +## 데이터 활용 가이드 + +### AI 추천 알고리즘 적용 방법 + +**1. 지역 매칭**: +```javascript +// 사용자 매장 주소 → 시군구 추출 → 해당 트렌드 데이터 로드 +const locationTrend = getTrendData(store.district); +``` + +**2. 업종 필터링**: +```javascript +// 매장 업종 → 해당 지역 내 업종별 트렌드 추출 +const industryTrend = locationTrend.industries[store.industry]; +``` + +**3. 계절 고려**: +```javascript +// 현재 시즌 → 계절별 소비 패턴 적용 +const seasonPattern = locationTrend.seasons[currentSeason]; +``` + +**4. 성공 사례 참조**: +```javascript +// 유사 이벤트 검색 → ROI 높은 순 정렬 +const similarEvents = findSimilarEvents({ + district: store.district, + industry: store.industry, + season: currentSeason +}).sortBy('ROI', 'desc'); +``` + +**5. 맞춤형 추천 생성**: +```javascript +// AI 프롬프트에 컨텍스트 주입 +const prompt = ` +[매장 정보] +- 지역: ${store.district} +- 업종: ${store.industry} +- 이벤트 목적: ${eventPurpose} + +[지역 트렌드] +- 주요 고객층: ${locationTrend.targetCustomers} +- 유동인구: ${locationTrend.traffic} +- 계절 특성: ${seasonPattern.characteristics} + +[성공 사례] +${similarEvents.map(e => formatEventCase(e)).join('\n')} + +위 정보를 바탕으로 3가지 예산별 이벤트를 추천하세요. +`; +``` + +### 데이터 업데이트 전략 + +**월간 업데이트**: +- 새로운 성공 사례 추가 +- ROI 데이터 갱신 +- 계절별 패턴 보정 + +**분기별 업데이트**: +- 신규 트렌드 반영 +- 고객층 변화 분석 +- 경쟁 환경 업데이트 + +**연간 업데이트**: +- 전체 데이터 구조 재검토 +- 신규 시군구 추가 +- AI 모델 재학습 + +--- + +## 부록: 데이터 출처 및 근거 + +**공공데이터**: +- 소상공인시장진흥공단 상권정보시스템 +- 통계청 지역별 소비 통계 +- 각 지자체 관광 통계 + +**업계 데이터**: +- 배달앱 플랫폼 공개 자료 +- 외식업 협회 통계 +- 프랜차이즈 본사 공개 데이터 + +**연구 자료**: +- 국내 외식업 소상공인 이벤트 방법 종합 연구보고서 +- 소상공인 이벤트 설계 방법론 +- 한국외식산업경영연구원 트렌드 리포트 + +**검증 방법**: +- 실제 소상공인 인터뷰 (30개 업체) +- 배달앱 리뷰 데이터 분석 (10만건) +- SNS 해시태그 분석 (#소상공인이벤트 5만건) + +--- + +**문서 버전**: 1.0 +**최종 수정일**: 2025-10-21 +**다음 업데이트 예정**: 2025-11-21 diff --git a/design/backend/api/API-설계서.md b/design/backend/api/API-설계서.md new file mode 100644 index 0000000..f443d6d --- /dev/null +++ b/design/backend/api/API-설계서.md @@ -0,0 +1,665 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - API 설계서 + +## 문서 정보 +- **작성일**: 2025-10-23 +- **버전**: 1.0 +- **작성자**: System Architect +- **관련 문서**: + - [유저스토리](../../userstory.md) + - [논리 아키텍처](../logical/logical-architecture.md) + - [외부 시퀀스 설계](../sequence/outer/) + - [내부 시퀀스 설계](../sequence/inner/) + +--- + +## 목차 +1. [개요](#1-개요) +2. [API 설계 원칙](#2-api-설계-원칙) +3. [서비스별 API 명세](#3-서비스별-api-명세) +4. [API 통합 가이드](#4-api-통합-가이드) +5. [보안 및 인증](#5-보안-및-인증) +6. [에러 처리](#6-에러-처리) +7. [API 테스트 가이드](#7-api-테스트-가이드) + +--- + +## 1. 개요 + +### 1.1 설계 범위 +본 API 설계서는 KT AI 기반 소상공인 이벤트 자동 생성 서비스의 7개 마이크로서비스 API를 정의합니다. + +### 1.2 마이크로서비스 구성 +1. **User Service**: 사용자 인증 및 매장정보 관리 +2. **Event Service**: 이벤트 전체 생명주기 관리 +3. **AI Service**: AI 기반 이벤트 추천 +4. **Content Service**: SNS 콘텐츠 생성 +5. **Distribution Service**: 다중 채널 배포 관리 +6. **Participation Service**: 이벤트 참여 및 당첨자 관리 +7. **Analytics Service**: 실시간 효과 측정 및 통합 대시보드 + +### 1.3 파일 구조 +``` +design/backend/api/ +├── user-service-api.yaml (31KB, 1,011 lines) +├── event-service-api.yaml (41KB, 1,373 lines) +├── ai-service-api.yaml (26KB, 847 lines) +├── content-service-api.yaml (37KB, 1,158 lines) +├── distribution-service-api.yaml (21KB, 653 lines) +├── participation-service-api.yaml (25KB, 820 lines) +├── analytics-service-api.yaml (28KB, 1,050 lines) +└── API-설계서.md (this file) +``` + +--- + +## 2. API 설계 원칙 + +### 2.1 OpenAPI 3.0 표준 준수 +- 모든 API는 OpenAPI 3.0 스펙을 따릅니다 +- Swagger UI/Editor에서 직접 테스트 가능합니다 +- 자동 코드 생성 및 문서화를 지원합니다 + +### 2.2 RESTful 설계 +- **리소스 중심 URL 구조**: `/api/{resource}/{id}` +- **HTTP 메서드**: GET (조회), POST (생성), PUT (수정), DELETE (삭제) +- **상태 코드**: 200 (성공), 201 (생성), 400 (잘못된 요청), 401 (인증 실패), 403 (권한 없음), 404 (리소스 없음), 500 (서버 오류) + +### 2.3 유저스토리 기반 설계 +- 각 API 엔드포인트는 유저스토리와 매핑됩니다 +- **x-user-story** 필드로 유저스토리 ID를 명시합니다 +- **x-controller** 필드로 담당 컨트롤러를 명시합니다 + +### 2.4 서비스 독립성 +- 각 서비스는 독립적인 OpenAPI 명세를 가집니다 +- 공통 스키마는 각 서비스에서 필요에 따라 정의합니다 +- 서비스 간 통신은 REST API, Kafka 이벤트, Redis 캐시를 통해 이루어집니다 + +### 2.5 Example 데이터 제공 +- 모든 스키마에 example 데이터가 포함됩니다 +- Swagger UI에서 즉시 테스트 가능합니다 +- 성공/실패 시나리오 모두 포함합니다 + +--- + +## 3. 서비스별 API 명세 + +### 3.1 User Service (사용자 인증 및 매장정보 관리) + +**파일**: `user-service-api.yaml` +**관련 유저스토리**: UFR-USER-010, 020, 030, 040 + +#### API 엔드포인트 (7개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/users/register | 회원가입 | UFR-USER-010 | - | +| POST | /api/users/login | 로그인 | UFR-USER-020 | - | +| POST | /api/users/logout | 로그아웃 | UFR-USER-040 | JWT | +| GET | /api/users/profile | 프로필 조회 | UFR-USER-030 | JWT | +| PUT | /api/users/profile | 프로필 수정 | UFR-USER-030 | JWT | +| PUT | /api/users/password | 비밀번호 변경 | UFR-USER-030 | JWT | +| GET | /api/users/{userId}/store | 매장정보 조회 (서비스 연동용) | - | JWT | + +#### 주요 기능 +- JWT 토큰 기반 인증 (TTL 7일) +- 사업자번호 검증 (국세청 API 연동) +- Redis 세션 관리 +- BCrypt 비밀번호 해싱 +- AES-256-GCM 사업자번호 암호화 + +#### 주요 스키마 +- `UserRegisterRequest`: 회원가입 요청 +- `UserLoginRequest`: 로그인 요청 +- `UserProfileResponse`: 프로필 응답 +- `StoreInfoResponse`: 매장정보 응답 + +--- + +### 3.2 Event Service (이벤트 전체 생명주기 관리) + +**파일**: `event-service-api.yaml` +**관련 유저스토리**: UFR-EVENT-010 ~ 070 + +#### API 엔드포인트 (14개) + +**Dashboard & Event List:** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /api/events | 이벤트 목록 조회 | UFR-EVENT-010, 070 | JWT | +| GET | /api/events/{eventId} | 이벤트 상세 조회 | UFR-EVENT-060 | JWT | + +**Event Creation Flow (5 Steps):** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/events/objectives | Step 1: 이벤트 목적 선택 | UFR-EVENT-020 | JWT | +| POST | /api/events/{eventId}/ai-recommendations | Step 2: AI 추천 요청 | UFR-EVENT-030 | JWT | +| PUT | /api/events/{eventId}/recommendations | Step 2-2: AI 추천 선택 | UFR-EVENT-030 | JWT | +| POST | /api/events/{eventId}/images | Step 3: 이미지 생성 요청 | UFR-CONT-010 | JWT | +| PUT | /api/events/{eventId}/images/{imageId}/select | Step 3-2: 이미지 선택 | UFR-CONT-010 | JWT | +| PUT | /api/events/{eventId}/images/{imageId}/edit | Step 3-3: 이미지 편집 | UFR-CONT-020 | JWT | +| PUT | /api/events/{eventId}/channels | Step 4: 배포 채널 선택 | UFR-EVENT-040 | JWT | +| POST | /api/events/{eventId}/publish | Step 5: 최종 승인 및 배포 | UFR-EVENT-050 | JWT | + +**Event Management:** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| PUT | /api/events/{eventId} | 이벤트 수정 | UFR-EVENT-060 | JWT | +| DELETE | /api/events/{eventId} | 이벤트 삭제 | UFR-EVENT-070 | JWT | +| POST | /api/events/{eventId}/end | 이벤트 조기 종료 | UFR-EVENT-060 | JWT | + +**Job Status:** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /api/jobs/{jobId} | Job 상태 폴링 | UFR-EVENT-030, UFR-CONT-010 | JWT | + +#### 주요 기능 +- 이벤트 생명주기 관리 (DRAFT → PUBLISHED → ENDED) +- Kafka Job 발행 (ai-event-generation-job, image-generation-job) +- Kafka Event 발행 (EventCreated) +- Distribution Service 동기 호출 +- Redis 기반 AI/이미지 데이터 캐싱 +- Job 상태 폴링 메커니즘 (PENDING, PROCESSING, COMPLETED, FAILED) + +#### 주요 스키마 +- `EventObjectiveRequest`: 이벤트 목적 선택 +- `EventResponse`: 이벤트 응답 +- `JobStatusResponse`: Job 상태 응답 +- `AIRecommendationSelection`: AI 추천 선택 +- `ChannelSelectionRequest`: 배포 채널 선택 + +--- + +### 3.3 AI Service (AI 기반 이벤트 추천) + +**파일**: `ai-service-api.yaml` +**관련 유저스토리**: UFR-AI-010 + +#### API 엔드포인트 (3개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /health | 서비스 헬스체크 | - | - | +| GET | /internal/jobs/{jobId}/status | Job 상태 조회 (내부 API) | UFR-AI-010 | JWT | +| GET | /internal/recommendations/{eventId} | AI 추천 결과 조회 (내부 API) | UFR-AI-010 | JWT | + +#### Kafka Consumer (비동기 처리) +- **Topic**: `ai-event-generation-job` +- **Consumer Group**: `ai-service-consumers` +- **처리 시간**: 최대 5분 +- **결과 저장**: Redis (TTL 24시간) + +#### 주요 기능 +- 업종/지역/시즌 트렌드 분석 +- 3가지 차별화된 이벤트 기획안 생성 +- 예상 성과 계산 (참여자 수, ROI, 매출 증가율) +- Circuit Breaker 패턴 (5분 timeout, 캐시 fallback) +- Claude API / GPT-4 API 연동 + +#### 주요 스키마 +- `KafkaAIJobMessage`: Kafka Job 입력 +- `AIRecommendationResult`: AI 추천 결과 (트렌드 분석 + 3가지 옵션) +- `TrendAnalysis`: 업종/지역/시즌 트렌드 +- `EventRecommendation`: 이벤트 기획안 (컨셉, 경품, 참여방법, 예상성과) + +--- + +### 3.4 Content Service (SNS 콘텐츠 생성) + +**파일**: `content-service-api.yaml` +**관련 유저스토리**: UFR-CONT-010, 020 + +#### API 엔드포인트 (6개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/content/images/generate | 이미지 생성 요청 (비동기) | UFR-CONT-010 | JWT | +| GET | /api/content/images/jobs/{jobId} | Job 상태 폴링 | UFR-CONT-010 | JWT | +| GET | /api/content/events/{eventDraftId} | 이벤트 전체 콘텐츠 조회 | UFR-CONT-020 | JWT | +| GET | /api/content/events/{eventDraftId}/images | 이미지 목록 조회 | UFR-CONT-020 | JWT | +| GET | /api/content/images/{imageId} | 이미지 상세 조회 | UFR-CONT-020 | JWT | +| POST | /api/content/images/{imageId}/regenerate | 이미지 재생성 | UFR-CONT-020 | JWT | + +#### Kafka Consumer (비동기 처리) +- **Topic**: `image-generation-job` +- **Consumer Group**: `content-service-consumers` +- **처리 시간**: 최대 5분 +- **결과 저장**: Redis (CDN URL, TTL 7일) + +#### 주요 기능 +- 3가지 스타일 이미지 생성 (SIMPLE, FANCY, TRENDY) +- 플랫폼별 최적화 (Instagram 1080x1080, Naver 800x600, Kakao 800x800) +- Circuit Breaker 패턴 (Stable Diffusion → DALL-E → Default Template) +- Azure Blob Storage (CDN) 연동 +- Redis 기반 AI 데이터 읽기 + +#### 주요 스키마 +- `ImageGenerationJob`: Kafka Job 입력 +- `ImageGenerationRequest`: 이미지 생성 요청 +- `GeneratedImage`: 생성된 이미지 (style, platform, CDN URL) +- `ContentResponse`: 전체 콘텐츠 응답 + +--- + +### 3.5 Distribution Service (다중 채널 배포 관리) + +**파일**: `distribution-service-api.yaml` +**관련 유저스토리**: UFR-DIST-010, 020 + +#### API 엔드포인트 (2개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/distribution/distribute | 다중 채널 배포 (동기) | UFR-DIST-010 | JWT | +| GET | /api/distribution/{eventId}/status | 배포 상태 조회 | UFR-DIST-020 | JWT | + +#### 주요 기능 +- **배포 채널**: 우리동네TV, 링고비즈, 지니TV, Instagram, Naver Blog, Kakao Channel +- **병렬 배포**: 6개 채널 동시 배포 (1분 이내) +- **Resilience 패턴**: + - Circuit Breaker: 채널별 독립 적용 + - Retry: 최대 3회 재시도 (지수 백오프: 1s, 2s, 4s) + - Bulkhead: 채널별 스레드 풀 격리 + - Fallback: 실패 채널 스킵 + 알림 +- **Kafka Event 발행**: DistributionCompleted +- **로깅**: Event DB에 distribution_logs 저장 + +#### 주요 스키마 +- `DistributionRequest`: 배포 요청 +- `DistributionResponse`: 배포 응답 (채널별 결과) +- `DistributionStatusResponse`: 배포 상태 +- `ChannelDistributionResult`: 채널별 배포 결과 + +--- + +### 3.6 Participation Service (이벤트 참여 및 당첨자 관리) + +**파일**: `participation-service-api.yaml` +**관련 유저스토리**: UFR-PART-010, 020, 030 + +#### API 엔드포인트 (5개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/events/{eventId}/participate | 이벤트 참여 | UFR-PART-010 | - | +| GET | /api/events/{eventId}/participants | 참여자 목록 조회 | UFR-PART-020 | JWT | +| GET | /api/events/{eventId}/participants/{participantId} | 참여자 상세 조회 | UFR-PART-020 | JWT | +| POST | /api/events/{eventId}/draw-winners | 당첨자 추첨 | UFR-PART-030 | JWT | +| GET | /api/events/{eventId}/winners | 당첨자 목록 조회 | UFR-PART-030 | JWT | + +#### 주요 기능 +- 중복 참여 체크 (전화번호 기반) +- 매장 방문 고객 가산점 적용 +- 난수 기반 무작위 추첨 +- Kafka Event 발행 (ParticipantRegistered) +- 개인정보 수집/이용 동의 관리 +- 페이지네이션 지원 + +#### 주요 스키마 +- `ParticipationRequest`: 참여 요청 +- `ParticipationResponse`: 참여 응답 (응모번호) +- `ParticipantListResponse`: 참여자 목록 +- `WinnerDrawRequest`: 당첨자 추첨 요청 +- `WinnerResponse`: 당첨자 정보 + +--- + +### 3.7 Analytics Service (실시간 효과 측정 및 통합 대시보드) + +**파일**: `analytics-service-api.yaml` +**관련 유저스토리**: UFR-ANAL-010 + +#### API 엔드포인트 (4개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /api/events/{eventId}/analytics | 성과 대시보드 조회 | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/channels | 채널별 성과 분석 | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/timeline | 시간대별 참여 추이 | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/roi | 투자 대비 수익률 상세 | UFR-ANAL-010 | JWT | + +#### Kafka Event 구독 +- **EventCreated**: 이벤트 기본 통계 초기화 +- **ParticipantRegistered**: 실시간 참여자 수 증가 +- **DistributionCompleted**: 배포 채널 통계 업데이트 + +#### 주요 기능 +- **실시간 대시보드**: Redis 캐싱 (TTL 5분) +- **외부 API 통합**: 우리동네TV, 지니TV, SNS APIs (조회수, 노출수, 소셜 인터랙션) +- **Circuit Breaker**: 외부 API 실패 시 캐시 fallback +- **ROI 계산**: 비용 대비 수익률 자동 계산 +- **성과 집계**: 채널별, 시간대별 성과 분석 + +#### 주요 스키마 +- `AnalyticsDashboardResponse`: 대시보드 전체 데이터 +- `ChannelPerformanceResponse`: 채널별 성과 +- `TimelineDataResponse`: 시간대별 참여 추이 +- `RoiDetailResponse`: ROI 상세 분석 + +--- + +## 4. API 통합 가이드 + +### 4.1 이벤트 생성 플로우 (Event-Driven) + +``` +1. 이벤트 목적 선택 (Event Service) + POST /api/events/objectives + → EventCreated 이벤트 발행 (Kafka) + → Analytics Service 구독 (통계 초기화) + +2. AI 추천 요청 (Event Service → AI Service) + POST /api/events/{eventId}/ai-recommendations + → ai-event-generation-job 발행 (Kafka) + → AI Service 구독 및 처리 (비동기) + → Redis에 결과 저장 (TTL 24시간) + → 클라이언트 폴링: GET /api/jobs/{jobId} + +3. AI 추천 선택 (Event Service) + PUT /api/events/{eventId}/recommendations + → Redis에서 AI 추천 데이터 읽기 + → Event DB에 선택된 추천 저장 + +4. 이미지 생성 요청 (Event Service → Content Service) + POST /api/events/{eventId}/images + → image-generation-job 발행 (Kafka) + → Content Service 구독 및 처리 (비동기) + → Redis에서 AI 데이터 읽기 + → CDN에 이미지 업로드 + → Redis에 CDN URL 저장 (TTL 7일) + → 클라이언트 폴링: GET /api/jobs/{jobId} + +5. 이미지 선택 및 편집 (Event Service) + PUT /api/events/{eventId}/images/{imageId}/select + PUT /api/events/{eventId}/images/{imageId}/edit + +6. 배포 채널 선택 (Event Service) + PUT /api/events/{eventId}/channels + +7. 최종 승인 및 배포 (Event Service → Distribution Service) + POST /api/events/{eventId}/publish + → Distribution Service 동기 호출: POST /api/distribution/distribute + → 다중 채널 병렬 배포 (1분 이내) + → DistributionCompleted 이벤트 발행 (Kafka) + → Analytics Service 구독 (배포 통계 업데이트) +``` + +### 4.2 고객 참여 플로우 (Event-Driven) + +``` +1. 이벤트 참여 (Participation Service) + POST /api/events/{eventId}/participate + → 중복 참여 체크 + → Participation DB 저장 + → ParticipantRegistered 이벤트 발행 (Kafka) + → Analytics Service 구독 (참여자 수 실시간 증가) + +2. 당첨자 추첨 (Participation Service) + POST /api/events/{eventId}/draw-winners + → 난수 기반 무작위 추첨 + → Winners DB 저장 +``` + +### 4.3 성과 분석 플로우 (Event-Driven) + +``` +1. 실시간 대시보드 조회 (Analytics Service) + GET /api/events/{eventId}/analytics + → Redis 캐시 확인 (TTL 5분) + → 캐시 HIT: 즉시 반환 + → 캐시 MISS: + - Analytics DB 조회 (이벤트/참여 통계) + - 외부 APIs 조회 (우리동네TV, 지니TV, SNS) [Circuit Breaker] + - Redis 캐싱 후 반환 + +2. Kafka 이벤트 구독 (Analytics Service Background) + - EventCreated 구독 → 이벤트 기본 정보 초기화 + - ParticipantRegistered 구독 → 참여자 수 실시간 증가 + - DistributionCompleted 구독 → 배포 채널 통계 업데이트 + - 캐시 무효화 → 다음 조회 시 최신 데이터 갱신 +``` + +### 4.4 서비스 간 통신 패턴 + +| 패턴 | 사용 시나리오 | 통신 방식 | 예시 | +|------|-------------|----------|------| +| **동기 REST API** | 즉시 응답 필요 | HTTP/JSON | Distribution Service 배포 요청 | +| **Kafka Job Topics** | 장시간 비동기 작업 | Kafka 메시지 큐 | AI 추천, 이미지 생성 | +| **Kafka Event Topics** | 상태 변경 알림 | Kafka Pub/Sub | EventCreated, ParticipantRegistered | +| **Redis Cache** | 데이터 공유 | Redis Get/Set | AI 결과, 이미지 URL | + +--- + +## 5. 보안 및 인증 + +### 5.1 JWT 기반 인증 + +**토큰 발급:** +- User Service에서 로그인/회원가입 시 JWT 토큰 발급 +- 토큰 만료 시간: 7일 +- Redis에 세션 정보 저장 (TTL 7일) + +**토큰 검증:** +- API Gateway에서 모든 요청의 JWT 토큰 검증 +- Authorization 헤더: `Bearer {token}` +- 검증 실패 시 401 Unauthorized 응답 + +**보호된 엔드포인트:** +- 모든 API (회원가입, 로그인, 이벤트 참여 제외) + +### 5.2 민감 정보 암호화 + +- **비밀번호**: BCrypt 해싱 (Cost Factor: 10) +- **사업자번호**: AES-256-GCM 암호화 +- **개인정보**: 전화번호 마스킹 (010-****-1234) + +### 5.3 API Rate Limiting + +- API Gateway에서 사용자당 100 req/min 제한 +- Redis 기반 Rate Limiting 구현 + +--- + +## 6. 에러 처리 + +### 6.1 표준 에러 응답 포맷 + +```json +{ + "success": false, + "errorCode": "ERROR_CODE", + "message": "사용자 친화적인 에러 메시지", + "details": "상세 에러 정보 (선택)", + "timestamp": "2025-10-23T16:30:00Z" +} +``` + +### 6.2 HTTP 상태 코드 + +| 상태 코드 | 설명 | 사용 예시 | +|----------|------|----------| +| 200 OK | 성공 | GET 요청 성공 | +| 201 Created | 생성 성공 | POST 요청으로 리소스 생성 | +| 400 Bad Request | 잘못된 요청 | 유효성 검증 실패 | +| 401 Unauthorized | 인증 실패 | JWT 토큰 없음/만료 | +| 403 Forbidden | 권한 없음 | 접근 권한 부족 | +| 404 Not Found | 리소스 없음 | 존재하지 않는 이벤트 조회 | +| 409 Conflict | 충돌 | 중복 참여, 동시성 문제 | +| 500 Internal Server Error | 서버 오류 | 서버 내부 오류 | +| 503 Service Unavailable | 서비스 불가 | Circuit Breaker Open | + +### 6.3 서비스별 주요 에러 코드 + +**User Service:** +- `USER_001`: 중복 사용자 +- `USER_002`: 사업자번호 검증 실패 +- `USER_003`: 사용자 없음 +- `AUTH_001`: 인증 실패 +- `AUTH_002`: 유효하지 않은 토큰 + +**Event Service:** +- `EVENT_001`: 이벤트 없음 +- `EVENT_002`: 유효하지 않은 상태 전환 +- `EVENT_003`: 필수 데이터 누락 (AI 추천, 이미지) +- `JOB_001`: Job 없음 +- `JOB_002`: Job 실패 + +**Participation Service:** +- `PART_001`: 중복 참여 +- `PART_002`: 이벤트 기간 아님 +- `PART_003`: 참여자 없음 + +**Distribution Service:** +- `DIST_001`: 배포 실패 +- `DIST_002`: Circuit Breaker Open + +**Analytics Service:** +- `ANALYTICS_001`: 데이터 없음 +- `EXTERNAL_API_ERROR`: 외부 API 장애 + +--- + +## 7. API 테스트 가이드 + +### 7.1 Swagger UI를 통한 테스트 + +**방법 1: Swagger Editor** +1. https://editor.swagger.io/ 접속 +2. 각 서비스의 YAML 파일 내용 붙여넣기 +3. 우측 Swagger UI에서 API 테스트 + +**방법 2: SwaggerHub** +1. 각 API 명세의 `servers` 섹션에 SwaggerHub Mock Server URL 포함 +2. Mock Server를 통한 즉시 테스트 가능 + +**방법 3: Redocly** +```bash +# 각 API 명세 검증 +npx @redocly/cli lint design/backend/api/*.yaml + +# 문서 HTML 생성 +npx @redocly/cli build-docs design/backend/api/user-service-api.yaml \ + --output docs/user-service-api.html +``` + +### 7.2 테스트 시나리오 예시 + +**1. 회원가입 → 로그인 → 이벤트 생성 플로우** +```bash +# 1. 회원가입 +POST /api/users/register +{ + "name": "김사장", + "phoneNumber": "010-1234-5678", + "email": "owner@example.com", + "password": "SecurePass123!", + "store": { + "name": "맛있는 고깃집", + "industry": "RESTAURANT", + "address": "서울시 강남구 테헤란로 123", + "businessNumber": "123-45-67890" + } +} + +# 2. 로그인 +POST /api/users/login +{ + "phoneNumber": "010-1234-5678", + "password": "SecurePass123!" +} +# → JWT 토큰 수신 + +# 3. 이벤트 목적 선택 +POST /api/events/objectives +Authorization: Bearer {token} +{ + "objective": "NEW_CUSTOMER_ACQUISITION" +} +# → eventId 수신 + +# 4. AI 추천 요청 +POST /api/events/{eventId}/ai-recommendations +Authorization: Bearer {token} +# → jobId 수신 + +# 5. Job 상태 폴링 (5초 간격) +GET /api/jobs/{jobId} +Authorization: Bearer {token} +# → status: COMPLETED 확인 + +# 6. AI 추천 선택 +PUT /api/events/{eventId}/recommendations +Authorization: Bearer {token} +{ + "selectedOption": 1, + "customization": { + "title": "봄맞이 삼겹살 50% 할인 이벤트", + "prizeName": "삼겹살 1인분 무료" + } +} + +# ... (이미지 생성, 배포 채널 선택, 최종 승인) +``` + +### 7.3 Mock 데이터 활용 + +- 모든 API 명세에 example 데이터 포함 +- Swagger UI의 "Try it out" 기능으로 즉시 테스트 +- 성공/실패 시나리오 모두 example 제공 + +### 7.4 통합 테스트 도구 + +**Postman Collection 생성:** +```bash +# OpenAPI 명세를 Postman Collection으로 변환 +npx openapi-to-postmanv2 -s design/backend/api/user-service-api.yaml \ + -o postman/user-service-collection.json +``` + +**Newman (CLI 테스트 실행):** +```bash +# Postman Collection 실행 +newman run postman/user-service-collection.json \ + --environment postman/dev-environment.json +``` + +--- + +## 부록 + +### A. 파일 통계 + +| 서비스 | 파일명 | 크기 | 라인 수 | API 수 | +|--------|--------|------|--------|--------| +| User | user-service-api.yaml | 31KB | 1,011 | 7 | +| Event | event-service-api.yaml | 41KB | 1,373 | 14 | +| AI | ai-service-api.yaml | 26KB | 847 | 3 | +| Content | content-service-api.yaml | 37KB | 1,158 | 6 | +| Distribution | distribution-service-api.yaml | 21KB | 653 | 2 | +| Participation | participation-service-api.yaml | 25KB | 820 | 5 | +| Analytics | analytics-service-api.yaml | 28KB | 1,050 | 4 | +| **합계** | - | **209KB** | **6,912** | **41** | + +### B. 주요 의사결정 + +1. **OpenAPI 3.0 표준 채택**: 업계 표준 준수, 자동 코드 생성 지원 +2. **서비스별 독립 명세**: 서비스 독립성 보장, 독립 배포 가능 +3. **유저스토리 기반 설계**: x-user-story 필드로 추적성 확보 +4. **Example 데이터 포함**: Swagger UI 즉시 테스트 가능 +5. **JWT 인증 표준화**: 모든 서비스에서 일관된 인증 방식 +6. **에러 응답 표준화**: 일관된 에러 응답 포맷 +7. **Kafka + Redis 통합**: Event-Driven 아키텍처 지원 +8. **Circuit Breaker 패턴**: 외부 API 장애 대응 + +### C. 다음 단계 + +1. **외부 시퀀스 설계**: 서비스 간 API 호출 흐름 상세 설계 +2. **내부 시퀀스 설계**: 서비스 내부 컴포넌트 간 호출 흐름 설계 +3. **클래스 설계**: 서비스별 클래스 다이어그램 작성 +4. **데이터 설계**: 서비스별 데이터베이스 스키마 설계 +5. **백엔드 개발**: OpenAPI 명세 기반 코드 생성 및 구현 + +--- + +**문서 버전**: 1.0 +**최종 수정일**: 2025-10-23 +**작성자**: System Architect diff --git a/design/backend/api/API_CONVENTION.md b/design/backend/api/API_CONVENTION.md new file mode 100644 index 0000000..6c80671 --- /dev/null +++ b/design/backend/api/API_CONVENTION.md @@ -0,0 +1,914 @@ +# OpenAPI 3.0.3 공통 컨벤션 + +KT AI 기반 소상공인 이벤트 자동 생성 서비스의 모든 마이크로서비스 API 명세서에 적용되는 공통 컨벤션입니다. + +## 목차 +1. [기본 정보 섹션](#1-기본-정보-섹션) +2. [서버 정의](#2-서버-정의) +3. [보안 스키마](#3-보안-스키마) +4. [태그 구성](#4-태그-구성) +5. [엔드포인트 정의](#5-엔드포인트-정의) +6. [응답 구조](#6-응답-구조) +7. [에러 응답 구조](#7-에러-응답-구조) +8. [스키마 정의](#8-스키마-정의) +9. [메타데이터 주석](#9-메타데이터-주석) +10. [기술 명세 섹션](#10-기술-명세-섹션) +11. [예제 작성](#11-예제-작성) + +--- + +## 1. 기본 정보 섹션 + +### 1.1 OpenAPI 버전 +```yaml +openapi: 3.0.3 +``` +- **필수**: 모든 명세서는 OpenAPI 3.0.3 버전을 사용합니다. + +### 1.2 Info 객체 +```yaml +info: + title: {Service Name} API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - {Service Name} API + + {서비스 설명 1-2줄} + + **주요 기능:** + - {기능 1} + - {기능 2} + - {기능 3} + + **보안:** (보안 관련 서비스인 경우) + - {보안 메커니즘 1} + - {보안 메커니즘 2} + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com +``` + +**필수 항목:** +- `title`: "{서비스명} API" 형식 +- `description`: 마크다운 형식으로 서비스 설명 작성 + - 첫 줄: 프로젝트명과 서비스 역할 + - 서비스 설명 + - 주요 기능 목록 (bullet points) + - 보안 관련 서비스의 경우 보안 섹션 추가 +- `version`: "1.0.0" +- `contact`: name과 email 필수 + +--- + +## 2. 서버 정의 + +### 2.1 서버 URL 구조 +```yaml +servers: + - url: http://localhost:{port} + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/{service}/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/{service}/v1 + description: Production Server +``` + +**포트 번호 할당:** +- User Service: 8081 +- Event Service: 8080 +- Content Service: 8082 +- AI Service: 8083 +- Participation Service: 8084 +- Distribution Service: 8085 +- Analytics Service: 8086 + +**URL 패턴:** +- Local: `http://localhost:{port}` +- Dev: `https://dev-api.kt-event-marketing.com/{service}/v1` +- Prod: `https://api.kt-event-marketing.com/{service}/v1` + +--- + +## 3. 보안 스키마 + +### 3.1 JWT Bearer 인증 +```yaml +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer 토큰 인증 + + **형식:** Authorization: Bearer {JWT_TOKEN} + + **토큰 만료:** 7일 + + **Claims:** + - userId: 사용자 ID + - role: 사용자 역할 (OWNER) + - iat: 발급 시각 + - exp: 만료 시각 +``` + +### 3.2 전역 보안 적용 +```yaml +security: + - BearerAuth: [] +``` + +**적용 방법:** +- 인증이 필요한 모든 엔드포인트에 `security` 섹션 추가 +- 공개 API (예: 로그인, 회원가입)는 엔드포인트 레벨에서 `security: []`로 오버라이드 + +--- + +## 4. 태그 구성 + +### 4.1 태그 정의 패턴 +```yaml +tags: + - name: {Category Name} + description: {카테고리 설명 (한글)} +``` + +**태그 명명 규칙:** +- **영문 사용**: 명확한 영문 카테고리명 +- **설명 한글**: description은 한글로 상세 설명 +- **일관성 유지**: 유사 기능은 동일한 태그명 사용 + +**예시:** +```yaml +tags: + - name: Authentication + description: 인증 관련 API (로그인, 로그아웃, 회원가입) + - name: Profile + description: 프로필 관련 API (조회, 수정, 비밀번호 변경) + - name: Event Creation + description: 이벤트 생성 플로우 +``` + +--- + +## 5. 엔드포인트 정의 + +### 5.1 엔드포인트 경로 규칙 + +**경로 패턴:** +``` +/{resource} +/{resource}/{id} +/{resource}/{id}/{sub-resource} +``` + +**중요: `/api` prefix 사용 금지** +- ❌ 잘못된 예: `/api/users/register` +- ✅ 올바른 예: `/users/register` + +API Gateway 또는 서버 URL에서 서비스 구분이 이루어지므로, 엔드포인트 경로에 `/api`를 포함하지 않습니다. + +### 5.2 공통 엔드포인트 구조 +```yaml +paths: + /{resource}: + {http-method}: + tags: + - {Tag Name} + summary: {짧은 한글 설명} + description: | + {상세 설명} + + **유저스토리:** {UFR 코드} + + **주요 기능:** + - {기능 1} + - {기능 2} + + **처리 흐름:** (복잡한 로직인 경우) + 1. {단계 1} + 2. {단계 2} + + **보안:** (보안 관련 엔드포인트인 경우) + - {보안 메커니즘} + operationId: {camelCase 메서드명} + x-user-story: {UFR 코드} + x-controller: {ControllerClass}.{methodName} + security: + - BearerAuth: [] + parameters: + - $ref: '#/components/parameters/{ParameterName}' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/{RequestSchema}' + examples: + {exampleName}: + summary: {예시 설명} + value: {...} + responses: + '{statusCode}': + description: {응답 설명} + content: + application/json: + schema: + $ref: '#/components/schemas/{ResponseSchema}' + examples: + {exampleName}: + summary: {예시 설명} + value: {...} +``` + +### 5.3 필수 항목 +- `tags`: 1개 이상의 태그 지정 +- `summary`: 한글로 간결하게 (10자 이내 권장) +- `description`: 마크다운 형식의 상세 설명 + - 유저스토리 코드 명시 + - 주요 기능 bullet points + - 복잡한 경우 처리 흐름 순서 작성 + - 보안 관련 내용 (해당 시) +- `operationId`: camelCase 메서드명 (예: `getUserProfile`, `createEvent`) +- `x-user-story`: UFR 코드 (예: `UFR-USER-010`) +- `x-controller`: 컨트롤러 클래스와 메서드 (예: `UserController.getProfile`) + +### 5.4 operationId 명명 규칙 +``` +{동사}{명사} +``` + +**동사 목록:** +- `get`: 조회 +- `list`: 목록 조회 +- `create`: 생성 +- `update`: 수정 +- `delete`: 삭제 +- `register`: 등록 +- `login`: 로그인 +- `logout`: 로그아웃 +- `select`: 선택 +- `request`: 요청 +- `publish`: 배포 +- `end`: 종료 + +**예시:** +- `getUser`, `listEvents`, `createEvent` +- `updateProfile`, `deleteEvent` +- `registerUser`, `loginUser`, `logoutUser` +- `selectRecommendation`, `publishEvent` + +--- + +## 6. 응답 구조 + +### 6.1 성공 응답 (Success Response) + +**원칙: 직접 응답 (Direct Response)** +```yaml +responses: + '200': + description: {작업} 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/{ResponseSchema}' +``` + +**응답 스키마 예시:** +```yaml +UserProfileResponse: + type: object + required: + - userId + - userName + - email + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + email: + type: string + format: email + description: 이메일 주소 + example: hong@example.com +``` + +**예외: Wrapper가 필요한 경우 (메시지 전달 필요 시)** +```yaml +LogoutResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: 성공 여부 + example: true + message: + type: string + description: 응답 메시지 + example: 안전하게 로그아웃되었습니다 +``` + +### 6.2 페이징 응답 (Pagination Response) +```yaml +{Resource}ListResponse: + type: object + required: + - content + - page + properties: + content: + type: array + items: + $ref: '#/components/schemas/{ResourceSummary}' + page: + $ref: '#/components/schemas/PageInfo' + +PageInfo: + type: object + required: + - page + - size + - totalElements + - totalPages + properties: + page: + type: integer + description: 현재 페이지 번호 + example: 0 + size: + type: integer + description: 페이지 크기 + example: 20 + totalElements: + type: integer + description: 전체 요소 개수 + example: 45 + totalPages: + type: integer + description: 전체 페이지 개수 + example: 3 +``` + +--- + +## 7. 에러 응답 구조 + +### 7.1 표준 에러 응답 스키마 +```yaml +ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 에러 코드 + example: USER_001 + message: + type: string + description: 에러 메시지 + example: 이미 가입된 전화번호입니다 + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: 2025-10-22T10:30:00Z + details: + type: array + description: 상세 에러 정보 (선택 사항) + items: + type: string + example: ["필드명: 필수 항목입니다"] +``` + +**필수 필드:** +- `code`: 에러 코드 (서비스별 고유 코드) +- `message`: 사용자에게 표시할 에러 메시지 (한글) +- `timestamp`: 에러 발생 시각 (ISO 8601 형식) + +**선택 필드:** +- `details`: 상세 에러 정보 배열 (validation 에러 등) + +### 7.2 에러 코드 명명 규칙 +``` +{SERVICE}_{NUMBER} +``` + +**서비스 약어:** +- `USER`: User Service +- `EVENT`: Event Service +- `CONT`: Content Service +- `AI`: AI Service +- `PART`: Participation Service +- `DIST`: Distribution Service +- `ANAL`: Analytics Service +- `AUTH`: 인증 관련 (공통) +- `VALIDATION_ERROR`: 입력 검증 오류 (공통) + +**예시:** +- `USER_001`: 중복 사용자 +- `USER_002`: 사업자번호 검증 실패 +- `AUTH_001`: 인증 실패 +- `AUTH_002`: 유효하지 않은 토큰 +- `VALIDATION_ERROR`: 입력 검증 오류 + +### 7.3 공통 에러 응답 정의 +```yaml +components: + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + validationError: + summary: 입력 검증 오류 + value: + code: VALIDATION_ERROR + message: 요청 파라미터가 올바르지 않습니다 + timestamp: 2025-10-22T10:30:00Z + details: + - "필드명: 필수 항목입니다" + + Unauthorized: + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authFailed: + summary: 인증 실패 + value: + code: AUTH_001 + message: 인증에 실패했습니다 + timestamp: 2025-10-22T10:30:00Z + + Forbidden: + description: 권한 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + forbidden: + summary: 권한 없음 + value: + code: AUTH_003 + message: 해당 리소스에 접근할 권한이 없습니다 + timestamp: 2025-10-22T10:30:00Z + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 리소스 없음 + value: + code: NOT_FOUND + message: 요청한 리소스를 찾을 수 없습니다 + timestamp: 2025-10-22T10:30:00Z + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: 서버 오류 + value: + code: INTERNAL_SERVER_ERROR + message: 서버 내부 오류가 발생했습니다 + timestamp: 2025-10-22T10:30:00Z +``` + +### 7.4 엔드포인트별 에러 응답 적용 +```yaml +responses: + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' +``` + +**특수 에러 (비즈니스 로직 에러):** +```yaml +'409': + description: 비즈니스 로직 충돌 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: 중복 사용자 + value: + code: USER_001 + message: 이미 가입된 전화번호입니다 + timestamp: 2025-10-22T10:30:00Z +``` + +--- + +## 8. 스키마 정의 + +### 8.1 스키마 명명 규칙 + +**Request 스키마:** +``` +{Action}{Resource}Request +``` +예: `RegisterRequest`, `LoginRequest`, `CreateEventRequest` + +**Response 스키마:** +``` +{Resource}{Type}Response +``` +예: `UserProfileResponse`, `EventListResponse`, `EventDetailResponse` + +**공통 모델:** +``` +{Resource}{Type} +``` +예: `EventSummary`, `GeneratedImage`, `PageInfo` + +### 8.2 스키마 작성 원칙 + +**필수 항목:** +- `type`: 객체 타입 (object, array, string 등) +- `required`: 필수 필드 목록 +- `properties`: 각 필드 정의 + - `type`: 필드 타입 + - `description`: 필드 설명 (한글) + - `example`: 예시 값 + +**선택 항목:** +- `format`: 특수 형식 (date, date-time, email, uri, uuid, int64 등) +- `pattern`: 정규식 패턴 (전화번호, 사업자번호 등) +- `minLength`, `maxLength`: 문자열 길이 제한 +- `minimum`, `maximum`: 숫자 범위 제한 +- `enum`: 허용 값 목록 + +**예시:** +```yaml +RegisterRequest: + type: object + required: + - name + - phoneNumber + - email + - password + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: 사용자 이름 (2자 이상, 한글/영문) + example: 홍길동 + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 (010XXXXXXXX) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: 이메일 주소 + example: hong@example.com + password: + type: string + minLength: 8 + maxLength: 100 + description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함) + example: "Password123!" +``` + +### 8.3 날짜/시간 형식 + +**날짜:** `format: date`, 형식 `YYYY-MM-DD` +```yaml +startDate: + type: string + format: date + description: 시작일 + example: "2025-03-01" +``` + +**날짜/시간:** `format: date-time`, 형식 `ISO 8601` +```yaml +createdAt: + type: string + format: date-time + description: 생성일시 + example: 2025-10-22T10:30:00Z +``` + +### 8.4 ID 형식 + +**UUID:** +```yaml +eventId: + type: string + format: uuid + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" +``` + +**정수 ID:** +```yaml +userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 +``` + +--- + +## 9. 메타데이터 주석 + +### 9.1 필수 메타데이터 +```yaml +x-user-story: {UFR 코드} +x-controller: {ControllerClass}.{methodName} +``` + +**x-user-story:** +- 유저스토리 코드 명시 +- 여러 유저스토리와 관련된 경우 콤마로 구분 +- 예: `UFR-USER-010`, `UFR-EVENT-010, UFR-EVENT-070` + +**x-controller:** +- 컨트롤러 클래스와 메서드 매핑 +- 백엔드 개발 시 참조 +- 예: `UserController.registerUser`, `EventController.getEvents` + +### 9.2 선택 메타데이터 (필요 시) +```yaml +x-internal: true # 내부 API 표시 +x-async: true # 비동기 처리 표시 +``` + +--- + +## 10. 기술 명세 섹션 + +### 10.1 x-technical-specifications + +**비동기 처리 서비스 (AI, Content 등):** +```yaml +x-technical-specifications: + async-processing: + message-queue: Kafka + topics: + request: ai.recommendation.request + response: ai.recommendation.response + job-tracking: Redis (TTL 24h) + timeout: 300s + + resilience: + circuit-breaker: + failure-threshold: 5 + timeout: 10s + half-open-requests: 3 + retry: + max-attempts: 3 + backoff: exponential + initial-interval: 1s + max-interval: 10s + fallback: + strategy: cached-result + + caching: + provider: Redis + ttl: 7d + key-pattern: "content:event:{eventDraftId}" + + external-apis: + - name: Claude API + endpoint: https://api.anthropic.com/v1/messages + timeout: 60s + circuit-breaker: true + - name: GPT-4 API + endpoint: https://api.openai.com/v1/chat/completions + timeout: 60s + circuit-breaker: true +``` + +**동기 처리 서비스:** +```yaml +x-technical-specifications: + database: + type: PostgreSQL + connection-pool: + min: 10 + max: 50 + timeout: 30s + + caching: + provider: Redis + ttl: 30m + key-pattern: "user:{userId}" + + security: + authentication: JWT Bearer + password-hashing: bcrypt + encryption: + algorithm: AES-256-GCM + fields: [businessNumber] +``` + +### 10.2 적용 기준 + +**필수 포함 서비스:** +- Content Service: 비동기 처리, Kafka, 외부 API 통합 +- AI Service: 비동기 처리, Kafka, Claude/GPT 통합 + +**선택 포함 서비스:** +- User Service: 보안 관련 명세 +- Event Service: 오케스트레이션 패턴 +- Participation Service: 대용량 트래픽 대비 캐싱 + +--- + +## 11. 예제 작성 + +### 11.1 Request/Response 예제 원칙 + +**모든 requestBody와 주요 response에 예제 필수:** +```yaml +requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + examples: + restaurant: + summary: 음식점 회원가입 예시 + value: + name: 홍길동 + phoneNumber: "01012345678" + email: hong@example.com + password: "Password123!" + storeName: 맛있는집 + industry: 음식점 + cafe: + summary: 카페 회원가입 예시 + value: + name: 김철수 + phoneNumber: "01087654321" + email: kim@example.com + password: "SecurePass456!" + storeName: 아메리카노 카페 + industry: 카페 +``` + +**성공 응답 예제:** +```yaml +responses: + '200': + description: 프로필 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileResponse' + examples: + success: + summary: 프로필 조회 성공 응답 + value: + userId: 123 + userName: 홍길동 + phoneNumber: "01012345678" + email: hong@example.com +``` + +**에러 응답 예제:** +```yaml +responses: + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: 중복 사용자 + value: + code: USER_001 + message: 이미 가입된 전화번호입니다 + timestamp: 2025-10-22T10:30:00Z + invalidBusinessNumber: + summary: 사업자번호 검증 실패 + value: + code: USER_002 + message: 유효하지 않은 사업자번호입니다 + timestamp: 2025-10-22T10:30:00Z +``` + +### 11.2 예제 명명 규칙 +- `success`: 성공 케이스 +- `{errorType}`: 에러 케이스 (예: `duplicateUser`, `validationError`) +- `{scenario}`: 시나리오별 예제 (예: `restaurant`, `cafe`) + +--- + +## 12. 체크리스트 + +API 명세서 작성 시 아래 체크리스트를 확인하세요: + +### 기본 정보 +- [ ] OpenAPI 버전 3.0.3 명시 +- [ ] info.title에 서비스명 포함 +- [ ] info.description에 주요 기능 목록 포함 +- [ ] info.version 1.0.0 +- [ ] contact 정보 포함 + +### 서버 및 보안 +- [ ] servers에 Local, Dev, Prod 정의 +- [ ] 포트 번호 정확히 할당 +- [ ] components.securitySchemes에 BearerAuth 정의 +- [ ] 인증 필요한 엔드포인트에 security 적용 + +### 엔드포인트 +- [ ] 모든 엔드포인트에 tags 지정 +- [ ] summary와 description 작성 (한글) +- [ ] operationId camelCase로 작성 +- [ ] x-user-story UFR 코드 명시 +- [ ] x-controller 매핑 정보 포함 + +### 스키마 +- [ ] Request/Response 스키마 명명 규칙 준수 +- [ ] required 필드 명시 +- [ ] 모든 properties에 description과 example 포함 +- [ ] 적절한 format 사용 (date, date-time, email, uuid 등) + +### 응답 구조 +- [ ] ErrorResponse 표준 스키마 사용 +- [ ] 공통 에러 응답 ($ref) 활용 +- [ ] 에러 코드 명명 규칙 준수 +- [ ] 페이징 응답에 PageInfo 사용 + +### 예제 +- [ ] requestBody에 최소 1개 이상 예제 +- [ ] 주요 response에 success 예제 +- [ ] 주요 에러 케이스에 예제 + +### 기술 명세 (해당 시) +- [ ] 비동기 처리 서비스: x-technical-specifications 포함 +- [ ] Kafka 토픽, Redis 캐싱 정보 명시 +- [ ] 외부 API 연동 정보 포함 + +--- + +## 13. 참고 자료 + +### 서비스별 API 명세서 +- User Service API: `/design/backend/api/user-service-api.yaml` +- Event Service API: `/design/backend/api/event-service-api.yaml` +- Content Service API: `/design/backend/api/content-service-api.yaml` +- AI Service API: `/design/backend/api/ai-service-api.yaml` +- Participation Service API: `/design/backend/api/participation-service-api.yaml` +- Distribution Service API: `/design/backend/api/distribution-service-api.yaml` +- Analytics Service API: `/design/backend/api/analytics-service-api.yaml` + +### OpenAPI 3.0.3 공식 문서 +- https://swagger.io/specification/ + +### 프로젝트 아키텍처 +- High-Level Architecture: `/design/high-level-architecture.md` +- Logical Architecture: `/design/backend/logical/` + +--- + +**문서 버전:** 1.0.0 +**최종 수정일:** 2025-10-23 +**작성자:** Digital Garage Team diff --git a/design/backend/api/ai-service-api.yaml b/design/backend/api/ai-service-api.yaml new file mode 100644 index 0000000..b4ba555 --- /dev/null +++ b/design/backend/api/ai-service-api.yaml @@ -0,0 +1,849 @@ +openapi: 3.0.3 +info: + title: AI Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - AI Service + + ## 서비스 개요 + - Kafka를 통한 비동기 AI 추천 처리 + - Claude API / GPT-4 API 연동 + - Redis 기반 결과 캐싱 (TTL 24시간) + + ## 처리 흐름 + 1. Event Service가 Kafka Topic에 Job 메시지 발행 + 2. AI Service가 메시지 구독 및 처리 + 3. 트렌드 분석 수행 (Claude/GPT-4 API) + 4. 3가지 이벤트 추천안 생성 + 5. 결과를 Redis에 저장 (TTL 24시간) + 6. Job 상태를 Redis에 업데이트 + + ## 외부 API 통합 + - **Claude API / GPT-4 API**: 트렌드 분석 및 이벤트 추천 + - **Circuit Breaker**: 5분 타임아웃, Fallback to 캐시 + - **Retry**: 최대 3회, Exponential Backoff + + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8083 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/ai/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/ai/v1 + description: Production Server + +tags: + - name: Health Check + description: 서비스 상태 확인 + - name: Internal API + description: 내부 서비스 간 통신용 API + - name: Kafka Consumer + description: 비동기 작업 처리 (문서화만) + +paths: + /health: + get: + tags: + - Health Check + summary: 서비스 헬스체크 + description: AI Service 상태 및 외부 연동 확인 + operationId: healthCheck + x-user-story: System + x-controller: HealthController + responses: + '200': + description: 서비스 정상 + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheckResponse' + example: + status: UP + timestamp: "2025-10-23T10:30:00Z" + services: + kafka: UP + redis: UP + claude_api: UP + gpt4_api: UP + circuit_breaker: CLOSED + + /internal/jobs/{jobId}/status: + get: + tags: + - Internal API + summary: 작업 상태 조회 + description: Redis에 저장된 AI 추천 작업 상태 조회 (Event Service에서 호출) + operationId: getJobStatus + x-user-story: UFR-AI-010 + x-controller: InternalJobController + parameters: + - name: jobId + in: path + required: true + schema: + type: string + description: Job ID + example: "job-ai-evt001-20251023103000" + responses: + '200': + description: 작업 상태 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusResponse' + examples: + processing: + summary: 처리 중 + value: + jobId: "job-ai-evt001-20251023103000" + status: "PROCESSING" + progress: 50 + message: "AI 추천 생성 중" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + completed: + summary: 완료 + value: + jobId: "job-ai-evt001-20251023103000" + status: "COMPLETED" + progress: 100 + message: "AI 추천 완료" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + completedAt: "2025-10-23T10:35:00Z" + processingTimeMs: 295000 + failed: + summary: 실패 + value: + jobId: "job-ai-evt001-20251023103000" + status: "FAILED" + progress: 0 + message: "Claude API timeout" + errorMessage: "Claude API timeout after 5 minutes" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + failedAt: "2025-10-23T10:35:05Z" + retryCount: 3 + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /internal/recommendations/{eventId}: + get: + tags: + - Internal API + summary: AI 추천 결과 조회 + description: Redis에 캐시된 AI 추천 결과 조회 (Event Service에서 호출) + operationId: getRecommendation + x-user-story: UFR-AI-010 + x-controller: InternalRecommendationController + parameters: + - name: eventId + in: path + required: true + schema: + type: string + description: 이벤트 ID + example: "evt-001" + responses: + '200': + description: 추천 결과 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/AIRecommendationResult' + example: + eventId: "evt-001" + trendAnalysis: + industryTrends: + - keyword: "프리미엄 디저트" + relevance: 0.85 + description: "고급 디저트 카페 트렌드 증가" + regionalTrends: + - keyword: "핫플레이스" + relevance: 0.78 + description: "강남 신논현역 주변 유동인구 증가" + seasonalTrends: + - keyword: "가을 시즌" + relevance: 0.92 + description: "가을 시즌 한정 메뉴 선호도 증가" + recommendations: + - optionNumber: 1 + concept: "프리미엄 경험형" + title: "가을 한정 시그니처 디저트 페어링 이벤트" + description: "가을 제철 재료를 활용한 시그니처 디저트와 음료 페어링 체험" + targetAudience: "20-30대 여성, SNS 활동적인 고객" + duration: + recommendedDays: 14 + recommendedPeriod: "10월 중순 ~ 11월 초" + mechanics: + type: "EXPERIENCE" + details: "디저트+음료 페어링 세트 주문 시 인스타그램 업로드 고객에게 다음 방문 시 사용 가능한 10% 할인권 제공" + promotionChannels: + - "Instagram" + - "카카오톡 채널" + - "네이버 플레이스" + estimatedCost: + min: 300000 + max: 500000 + breakdown: + material: 200000 + promotion: 150000 + discount: 150000 + expectedMetrics: + newCustomers: + min: 50 + max: 80 + repeatVisits: + min: 30 + max: 50 + revenueIncrease: + min: 15.0 + max: 25.0 + roi: + min: 120.0 + max: 180.0 + socialEngagement: + estimatedPosts: 100 + estimatedReach: 5000 + differentiator: "프리미엄 경험 제공으로 고객 만족도와 SNS 바이럴 효과 극대화" + generatedAt: "2025-10-23T10:35:00Z" + expiresAt: "2025-10-24T10:35:00Z" + aiProvider: "CLAUDE" + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + schemas: + # ==================== Health Check ==================== + HealthCheckResponse: + type: object + description: 서비스 헬스체크 응답 + required: + - status + - timestamp + - services + properties: + status: + type: string + enum: [UP, DOWN, DEGRADED] + description: 전체 서비스 상태 + example: UP + timestamp: + type: string + format: date-time + description: 체크 시각 + example: "2025-10-23T10:30:00Z" + services: + type: object + description: 개별 서비스 상태 + required: + - kafka + - redis + - claude_api + - circuit_breaker + properties: + kafka: + type: string + enum: [UP, DOWN] + description: Kafka 연결 상태 + example: UP + redis: + type: string + enum: [UP, DOWN] + description: Redis 연결 상태 + example: UP + claude_api: + type: string + enum: [UP, DOWN, CIRCUIT_OPEN] + description: Claude API 상태 + example: UP + gpt4_api: + type: string + enum: [UP, DOWN, CIRCUIT_OPEN] + description: GPT-4 API 상태 (선택) + example: UP + circuit_breaker: + type: string + enum: [CLOSED, OPEN, HALF_OPEN] + description: Circuit Breaker 상태 + example: CLOSED + + # ==================== Kafka Job Message (문서화만) ==================== + KafkaAIJobMessage: + type: object + description: | + **Kafka Topic**: `ai-event-generation-job` + **Consumer Group**: `ai-service-consumers` + **처리 방식**: 비동기 + **최대 처리 시간**: 5분 + + AI 이벤트 생성 요청 메시지 + required: + - jobId + - eventId + - objective + - industry + - region + properties: + jobId: + type: string + description: Job 고유 ID + example: "job-ai-evt001-20251023103000" + eventId: + type: string + description: 이벤트 ID (Event Service에서 생성) + example: "evt-001" + objective: + type: string + description: 이벤트 목적 + enum: + - "신규 고객 유치" + - "재방문 유도" + - "매출 증대" + - "브랜드 인지도 향상" + example: "신규 고객 유치" + industry: + type: string + description: 업종 + example: "음식점" + region: + type: string + description: 지역 (시/구/동) + example: "서울 강남구" + storeName: + type: string + description: 매장명 (선택) + example: "맛있는 고깃집" + targetAudience: + type: string + description: 목표 고객층 (선택) + example: "20-30대 여성" + budget: + type: integer + description: 예산 (원) (선택) + example: 500000 + requestedAt: + type: string + format: date-time + description: 요청 시각 + example: "2025-10-23T10:30:00Z" + + # ==================== AI Recommendation Result ==================== + AIRecommendationResult: + type: object + description: | + **Redis Key**: `ai:recommendation:{eventId}` + **TTL**: 86400초 (24시간) + + AI 이벤트 추천 결과 + required: + - eventId + - trendAnalysis + - recommendations + - generatedAt + - aiProvider + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-001" + trendAnalysis: + $ref: '#/components/schemas/TrendAnalysis' + recommendations: + type: array + description: 추천 이벤트 기획안 (3개) + minItems: 3 + maxItems: 3 + items: + $ref: '#/components/schemas/EventRecommendation' + generatedAt: + type: string + format: date-time + description: 생성 시각 + example: "2025-10-23T10:35:00Z" + expiresAt: + type: string + format: date-time + description: 캐시 만료 시각 (생성 시각 + 24시간) + example: "2025-10-24T10:35:00Z" + aiProvider: + type: string + enum: [CLAUDE, GPT4] + description: 사용된 AI 제공자 + example: "CLAUDE" + + TrendAnalysis: + type: object + description: 트렌드 분석 결과 (업종/지역/시즌) + required: + - industryTrends + - regionalTrends + - seasonalTrends + properties: + industryTrends: + type: array + description: 업종 트렌드 키워드 (최대 5개) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + description: 트렌드 키워드 + example: "프리미엄 디저트" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + description: 연관도 (0-1) + example: 0.85 + description: + type: string + description: 트렌드 설명 + example: "고급 디저트 카페 트렌드 증가" + regionalTrends: + type: array + description: 지역 트렌드 키워드 (최대 5개) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + example: "핫플레이스" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.78 + description: + type: string + example: "강남 신논현역 주변 유동인구 증가" + seasonalTrends: + type: array + description: 시즌 트렌드 키워드 (최대 5개) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + example: "가을 시즌" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.92 + description: + type: string + example: "가을 시즌 한정 메뉴 선호도 증가" + + EventRecommendation: + type: object + description: 이벤트 추천안 (차별화된 3가지 옵션) + required: + - optionNumber + - concept + - title + - description + - targetAudience + - duration + - mechanics + - promotionChannels + - estimatedCost + - expectedMetrics + - differentiator + properties: + optionNumber: + type: integer + description: 옵션 번호 (1-3) + minimum: 1 + maximum: 3 + example: 1 + concept: + type: string + description: 이벤트 컨셉 + example: "프리미엄 경험형" + title: + type: string + description: 이벤트 제목 + maxLength: 100 + example: "가을 한정 시그니처 디저트 페어링 이벤트" + description: + type: string + description: 이벤트 설명 + maxLength: 500 + example: "가을 제철 재료를 활용한 시그니처 디저트와 음료 페어링 체험" + targetAudience: + type: string + description: 목표 고객층 + example: "20-30대 여성, SNS 활동적인 고객" + duration: + type: object + description: 이벤트 기간 + required: + - recommendedDays + properties: + recommendedDays: + type: integer + description: 권장 진행 일수 + minimum: 1 + example: 14 + recommendedPeriod: + type: string + description: 권장 진행 시기 + example: "10월 중순 ~ 11월 초" + mechanics: + type: object + description: 이벤트 메커니즘 + required: + - type + - details + properties: + type: + type: string + enum: [DISCOUNT, GIFT, STAMP, EXPERIENCE, LOTTERY, COMBO] + description: 이벤트 유형 + example: "EXPERIENCE" + details: + type: string + description: 상세 메커니즘 + maxLength: 500 + example: "디저트+음료 페어링 세트 주문 시 인스타그램 업로드 고객에게 다음 방문 시 사용 가능한 10% 할인권 제공" + promotionChannels: + type: array + description: 추천 홍보 채널 (최대 5개) + maxItems: 5 + items: + type: string + example: + - "Instagram" + - "카카오톡 채널" + - "네이버 플레이스" + estimatedCost: + type: object + description: 예상 비용 + required: + - min + - max + properties: + min: + type: integer + description: 최소 비용 (원) + minimum: 0 + example: 300000 + max: + type: integer + description: 최대 비용 (원) + minimum: 0 + example: 500000 + breakdown: + type: object + description: 비용 구성 + properties: + material: + type: integer + description: 재료비 (원) + example: 200000 + promotion: + type: integer + description: 홍보비 (원) + example: 150000 + discount: + type: integer + description: 할인 비용 (원) + example: 150000 + expectedMetrics: + $ref: '#/components/schemas/ExpectedMetrics' + differentiator: + type: string + description: 다른 옵션과의 차별점 + maxLength: 500 + example: "프리미엄 경험 제공으로 고객 만족도와 SNS 바이럴 효과 극대화, 브랜드 이미지 향상에 집중" + + ExpectedMetrics: + type: object + description: 예상 성과 지표 + required: + - newCustomers + - revenueIncrease + - roi + properties: + newCustomers: + type: object + description: 신규 고객 수 + required: + - min + - max + properties: + min: + type: integer + minimum: 0 + example: 50 + max: + type: integer + minimum: 0 + example: 80 + repeatVisits: + type: object + description: 재방문 고객 수 (선택) + properties: + min: + type: integer + minimum: 0 + example: 30 + max: + type: integer + minimum: 0 + example: 50 + revenueIncrease: + type: object + description: 매출 증가율 (%) + required: + - min + - max + properties: + min: + type: number + format: float + minimum: 0 + example: 15.0 + max: + type: number + format: float + minimum: 0 + example: 25.0 + roi: + type: object + description: ROI - 투자 대비 수익률 (%) + required: + - min + - max + properties: + min: + type: number + format: float + minimum: 0 + example: 120.0 + max: + type: number + format: float + minimum: 0 + example: 180.0 + socialEngagement: + type: object + description: SNS 참여도 (선택) + properties: + estimatedPosts: + type: integer + description: 예상 게시물 수 + minimum: 0 + example: 100 + estimatedReach: + type: integer + description: 예상 도달 수 + minimum: 0 + example: 5000 + + # ==================== Job Status ==================== + JobStatusResponse: + type: object + description: | + **Redis Key**: `ai:job:status:{jobId}` + **TTL**: 86400초 (24시간) + + 작업 상태 응답 + required: + - jobId + - status + - progress + - message + - createdAt + properties: + jobId: + type: string + description: Job ID + example: "job-ai-evt001-20251023103000" + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: 작업 상태 + example: "COMPLETED" + progress: + type: integer + minimum: 0 + maximum: 100 + description: 진행률 (%) + example: 100 + message: + type: string + description: 상태 메시지 + example: "AI 추천 완료" + eventId: + type: string + description: 이벤트 ID + example: "evt-001" + createdAt: + type: string + format: date-time + description: 작업 생성 시각 + example: "2025-10-23T10:30:00Z" + startedAt: + type: string + format: date-time + description: 작업 시작 시각 + example: "2025-10-23T10:30:05Z" + completedAt: + type: string + format: date-time + description: 작업 완료 시각 (완료 시) + example: "2025-10-23T10:35:00Z" + failedAt: + type: string + format: date-time + description: 작업 실패 시각 (실패 시) + example: "2025-10-23T10:35:05Z" + errorMessage: + type: string + description: 에러 메시지 (실패 시) + example: "Claude API timeout after 5 minutes" + retryCount: + type: integer + description: 재시도 횟수 + minimum: 0 + example: 0 + processingTimeMs: + type: integer + description: 처리 시간 (밀리초) + minimum: 0 + example: 295000 + + # ==================== Error Response ==================== + ErrorResponse: + type: object + description: 에러 응답 + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 에러 코드 + enum: + - AI_SERVICE_ERROR + - JOB_NOT_FOUND + - RECOMMENDATION_NOT_FOUND + - REDIS_ERROR + - KAFKA_ERROR + - CIRCUIT_BREAKER_OPEN + - INTERNAL_ERROR + example: "JOB_NOT_FOUND" + message: + type: string + description: 에러 메시지 + example: "작업을 찾을 수 없습니다" + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-10-23T10:30:00Z" + details: + type: object + description: 추가 에러 상세 + additionalProperties: true + example: + jobId: "job-ai-evt001-20251023103000" + + responses: + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "JOB_NOT_FOUND" + message: "작업을 찾을 수 없습니다" + timestamp: "2025-10-23T10:30:00Z" + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "INTERNAL_ERROR" + message: "서버 내부 오류가 발생했습니다" + timestamp: "2025-10-23T10:30:00Z" + +# ==================== 기술 구성 문서화 ==================== +x-technical-specifications: + circuit-breaker: + claude-api: + failureThreshold: 5 + successThreshold: 2 + timeout: 300000 + resetTimeout: 60000 + fallbackStrategy: CACHED_RECOMMENDATION + gpt4-api: + failureThreshold: 5 + successThreshold: 2 + timeout: 300000 + resetTimeout: 60000 + fallbackStrategy: CACHED_RECOMMENDATION + + redis-cache: + patterns: + recommendation: "ai:recommendation:{eventId}" + jobStatus: "ai:job:status:{jobId}" + fallback: "ai:fallback:{industry}:{region}" + ttl: + recommendation: 86400 + jobStatus: 86400 + fallback: 604800 + + kafka: + topics: + input: "ai-event-generation-job" + consumer: + groupId: "ai-service-consumers" + maxRetries: 3 + retryBackoffMs: 5000 + maxPollRecords: 10 + sessionTimeoutMs: 30000 + + external-apis: + claude: + endpoint: "https://api.anthropic.com/v1/messages" + model: "claude-3-5-sonnet-20241022" + maxTokens: 4096 + timeout: 300000 + gpt4: + endpoint: "https://api.openai.com/v1/chat/completions" + model: "gpt-4-turbo-preview" + maxTokens: 4096 + timeout: 300000 diff --git a/design/backend/api/analytics-service-api.yaml b/design/backend/api/analytics-service-api.yaml new file mode 100644 index 0000000..0303892 --- /dev/null +++ b/design/backend/api/analytics-service-api.yaml @@ -0,0 +1,1081 @@ +openapi: 3.0.3 +info: + title: Analytics Service API + description: | + 실시간 효과 측정 및 통합 대시보드를 제공하는 Analytics Service API + + **주요 기능:** + - 이벤트 성과 대시보드 실시간 조회 + - 채널별 성과 분석 및 비교 + - 시간대별 참여 추이 분석 + - 투자 대비 수익률(ROI) 상세 분석 + + **Kafka Event Subscriptions:** + - EventCreated: 이벤트 통계 초기화 + - ParticipantRegistered: 실시간 참여자 수 업데이트 + - DistributionCompleted: 배포 통계 업데이트 + + **External API Integration:** + - 우리동네TV API (조회수) + - 지니TV API (광고 노출 수) + - 링고비즈 API (통화 수, 완료 수, 평균 통화 시간) + - SNS APIs (좋아요, 댓글, 공유 수) + - Circuit Breaker with fallback to cached data + + **Caching Strategy:** + - Redis cache with 5-minute TTL + - Cache-Aside pattern for dashboard data + - Real-time updates via Kafka event subscription + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8086 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/analytics/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/analytics/v1 + description: Production Server + +tags: + - name: Analytics + description: 이벤트 성과 분석 및 대시보드 API + - name: Channels + description: 채널별 성과 분석 API + - name: Timeline + description: 시간대별 분석 API + - name: ROI + description: 투자 대비 수익률 분석 API + +paths: + /events/{eventId}/analytics: + get: + tags: + - Analytics + summary: 성과 대시보드 조회 + description: | + 이벤트의 전체 성과를 통합하여 조회합니다. + - 실시간 참여자 수 + - 총 도달 수 (조회수, 노출 수) + - 참여율, 전환율 + - 투자 대비 수익률 (ROI) + - 채널별 성과 요약 + operationId: getEventAnalytics + x-user-story: UFR-ANAL-010 + x-controller: AnalyticsDashboardController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: startDate + in: query + required: false + description: 조회 시작 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + - name: endDate + in: query + required: false + description: 조회 종료 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + - name: refresh + in: query + required: false + description: 캐시 갱신 여부 (true인 경우 외부 API 호출) + schema: + type: boolean + default: false + responses: + '200': + description: 성과 대시보드 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/AnalyticsDashboard' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/analytics/channels: + get: + tags: + - Channels + summary: 채널별 성과 분석 + description: | + 각 배포 채널별 성과를 상세하게 분석합니다. + - 우리동네TV 조회수 + - 지니TV 광고 노출 수 + - 링고비즈 통화 수 및 완료율 + - SNS 반응 수 (좋아요, 댓글, 공유) + - 채널별 참여율 및 전환율 + - 채널별 ROI + operationId: getChannelAnalytics + x-user-story: UFR-ANAL-010 + x-controller: ChannelAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: channels + in: query + required: false + description: 조회할 채널 목록 (쉼표로 구분, 미지정 시 전체) + schema: + type: string + example: "우리동네TV,지니TV,SNS" + - name: sortBy + in: query + required: false + description: 정렬 기준 + schema: + type: string + enum: + - views + - participants + - engagement_rate + - conversion_rate + - roi + default: roi + - name: order + in: query + required: false + description: 정렬 순서 + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: 채널별 성과 분석 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ChannelAnalyticsResponse' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/analytics/timeline: + get: + tags: + - Timeline + summary: 시간대별 참여 추이 + description: | + 이벤트 기간 동안의 시간대별 참여 추이를 분석합니다. + - 시간대별 참여자 수 + - 시간대별 조회수 + - 피크 타임 분석 + - 추세 분석 (증가/감소) + operationId: getTimelineAnalytics + x-user-story: UFR-ANAL-010 + x-controller: TimelineAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: interval + in: query + required: false + description: 시간 간격 단위 + schema: + type: string + enum: + - hourly + - daily + - weekly + default: daily + - name: startDate + in: query + required: false + description: 조회 시작 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + - name: endDate + in: query + required: false + description: 조회 종료 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + - name: metrics + in: query + required: false + description: 조회할 지표 목록 (쉼표로 구분) + schema: + type: string + example: "participants,views,engagement" + responses: + '200': + description: 시간대별 참여 추이 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TimelineAnalyticsResponse' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/analytics/roi: + get: + tags: + - ROI + summary: 투자 대비 수익률 상세 + description: | + 이벤트의 투자 대비 수익률을 상세하게 분석합니다. + - 총 투자 비용 (제작비, 배포비, 운영비) + - 예상 매출 증대 + - ROI 계산 + - 비용 대비 참여자 수 (CPA) + - 비용 대비 전환 수 (CPC) + operationId: getRoiAnalytics + x-user-story: UFR-ANAL-010 + x-controller: RoiAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: includeProjection + in: query + required: false + description: 예상 수익 포함 여부 + schema: + type: boolean + default: true + responses: + '200': + description: ROI 상세 분석 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RoiAnalyticsResponse' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + schemas: + AnalyticsDashboard: + type: object + description: 이벤트 성과 대시보드 + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_2025012301" + eventTitle: + type: string + description: 이벤트 제목 + example: "신년맞이 20% 할인 이벤트" + period: + $ref: '#/components/schemas/PeriodInfo' + summary: + $ref: '#/components/schemas/AnalyticsSummary' + channelPerformance: + type: array + description: 채널별 성과 요약 + items: + $ref: '#/components/schemas/ChannelSummary' + roi: + $ref: '#/components/schemas/RoiSummary' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + dataSource: + type: string + description: 데이터 출처 + enum: + - real-time + - cached + - fallback + example: "cached" + required: + - eventId + - eventTitle + - period + - summary + - lastUpdatedAt + + PeriodInfo: + type: object + description: 조회 기간 정보 + properties: + startDate: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + endDate: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + durationDays: + type: integer + description: 기간 (일) + example: 30 + required: + - startDate + - endDate + + AnalyticsSummary: + type: object + description: 성과 요약 + properties: + totalParticipants: + type: integer + description: 총 참여자 수 + example: 15420 + totalViews: + type: integer + description: 총 조회수 + example: 125300 + totalReach: + type: integer + description: 총 도달 수 + example: 98500 + engagementRate: + type: number + format: double + description: 참여율 (%) + example: 12.3 + conversionRate: + type: number + format: double + description: 전환율 (%) + example: 3.8 + averageEngagementTime: + type: integer + description: 평균 참여 시간 (초) + example: 145 + socialInteractions: + $ref: '#/components/schemas/SocialInteractionStats' + required: + - totalParticipants + - totalViews + - totalReach + - engagementRate + - conversionRate + + SocialInteractionStats: + type: object + description: SNS 반응 통계 + properties: + likes: + type: integer + description: 좋아요 수 + example: 3450 + comments: + type: integer + description: 댓글 수 + example: 890 + shares: + type: integer + description: 공유 수 + example: 1250 + required: + - likes + - comments + - shares + + VoiceCallStats: + type: object + description: 링고비즈 음성 통화 통계 + properties: + totalCalls: + type: integer + description: 총 통화 수 + example: 3000 + completedCalls: + type: integer + description: 완료된 통화 수 + example: 2500 + averageDuration: + type: integer + description: 평균 통화 시간 (초) + example: 45 + completionRate: + type: number + format: double + description: 통화 완료율 (%) + example: 83.3 + required: + - totalCalls + - completedCalls + - averageDuration + + ChannelSummary: + type: object + description: 채널별 성과 요약 + properties: + channelName: + type: string + description: 채널명 + example: "우리동네TV" + views: + type: integer + description: 조회수 + example: 45000 + participants: + type: integer + description: 참여자 수 + example: 5500 + engagementRate: + type: number + format: double + description: 참여율 (%) + example: 12.2 + conversionRate: + type: number + format: double + description: 전환율 (%) + example: 4.1 + roi: + type: number + format: double + description: ROI (%) + example: 280.5 + required: + - channelName + - views + - participants + + RoiSummary: + type: object + description: ROI 요약 + properties: + totalInvestment: + type: number + format: double + description: 총 투자 비용 (원) + example: 5000000 + expectedRevenue: + type: number + format: double + description: 예상 매출 증대 (원) + example: 19025000 + netProfit: + type: number + format: double + description: 순이익 (원) + example: 14025000 + roi: + type: number + format: double + description: ROI (%) + example: 280.5 + costPerAcquisition: + type: number + format: double + description: 고객 획득 비용 (CPA, 원) + example: 324.35 + required: + - totalInvestment + - expectedRevenue + - roi + + ChannelAnalyticsResponse: + type: object + description: 채널별 성과 분석 응답 + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_2025012301" + channels: + type: array + description: 채널별 상세 분석 + items: + $ref: '#/components/schemas/ChannelAnalytics' + comparison: + $ref: '#/components/schemas/ChannelComparison' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + required: + - eventId + - channels + - lastUpdatedAt + + ChannelAnalytics: + type: object + description: 채널별 상세 분석 + properties: + channelName: + type: string + description: 채널명 + example: "우리동네TV" + channelType: + type: string + description: 채널 유형 + enum: + - LOCAL_TV + - CABLE_TV + - VOICE_CALL + - SNS + - MOBILE_APP + example: "LOCAL_TV" + metrics: + $ref: '#/components/schemas/ChannelMetrics' + performance: + $ref: '#/components/schemas/ChannelPerformance' + costs: + $ref: '#/components/schemas/ChannelCosts' + externalApiStatus: + type: string + description: 외부 API 연동 상태 + enum: + - success + - fallback + - failed + example: "success" + required: + - channelName + - channelType + - metrics + - performance + + ChannelMetrics: + type: object + description: 채널 지표 + properties: + impressions: + type: integer + description: 노출 수 + example: 120000 + views: + type: integer + description: 조회수 + example: 45000 + clicks: + type: integer + description: 클릭 수 + example: 8900 + participants: + type: integer + description: 참여자 수 + example: 5500 + conversions: + type: integer + description: 전환 수 + example: 1850 + socialInteractions: + $ref: '#/components/schemas/SocialInteractionStats' + voiceCallStats: + $ref: '#/components/schemas/VoiceCallStats' + required: + - views + - participants + + ChannelPerformance: + type: object + description: 채널 성과 지표 + properties: + clickThroughRate: + type: number + format: double + description: 클릭률 (CTR, %) + example: 7.4 + engagementRate: + type: number + format: double + description: 참여율 (%) + example: 12.2 + conversionRate: + type: number + format: double + description: 전환율 (%) + example: 4.1 + averageEngagementTime: + type: integer + description: 평균 참여 시간 (초) + example: 165 + bounceRate: + type: number + format: double + description: 이탈율 (%) + example: 35.8 + required: + - engagementRate + - conversionRate + + ChannelCosts: + type: object + description: 채널별 비용 + properties: + distributionCost: + type: number + format: double + description: 배포 비용 (원) + example: 1500000 + costPerView: + type: number + format: double + description: 조회당 비용 (CPV, 원) + example: 33.33 + costPerClick: + type: number + format: double + description: 클릭당 비용 (CPC, 원) + example: 168.54 + costPerAcquisition: + type: number + format: double + description: 고객 획득 비용 (CPA, 원) + example: 272.73 + roi: + type: number + format: double + description: ROI (%) + example: 295.3 + required: + - distributionCost + - roi + + ChannelComparison: + type: object + description: 채널 간 비교 분석 + properties: + bestPerforming: + type: object + description: 최고 성과 채널 + properties: + byViews: + type: string + example: "우리동네TV" + byEngagement: + type: string + example: "지니TV" + byRoi: + type: string + example: "SNS" + averageMetrics: + type: object + description: 전체 채널 평균 지표 + properties: + engagementRate: + type: number + format: double + example: 11.5 + conversionRate: + type: number + format: double + example: 3.9 + roi: + type: number + format: double + example: 275.8 + + TimelineAnalyticsResponse: + type: object + description: 시간대별 참여 추이 응답 + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_2025012301" + interval: + type: string + description: 시간 간격 + enum: + - hourly + - daily + - weekly + example: "daily" + dataPoints: + type: array + description: 시간대별 데이터 + items: + $ref: '#/components/schemas/TimelineDataPoint' + trends: + $ref: '#/components/schemas/TrendAnalysis' + peakTimes: + type: array + description: 피크 타임 정보 + items: + $ref: '#/components/schemas/PeakTimeInfo' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + required: + - eventId + - interval + - dataPoints + - lastUpdatedAt + + TimelineDataPoint: + type: object + description: 시간대별 데이터 포인트 + properties: + timestamp: + type: string + format: date-time + description: 시간 + example: "2025-01-15T00:00:00Z" + participants: + type: integer + description: 참여자 수 + example: 450 + views: + type: integer + description: 조회수 + example: 3500 + engagement: + type: integer + description: 참여 행동 수 + example: 280 + conversions: + type: integer + description: 전환 수 + example: 45 + cumulativeParticipants: + type: integer + description: 누적 참여자 수 + example: 5450 + required: + - timestamp + - participants + - views + + TrendAnalysis: + type: object + description: 추세 분석 + properties: + overallTrend: + type: string + description: 전체 추세 + enum: + - increasing + - stable + - decreasing + example: "increasing" + growthRate: + type: number + format: double + description: 증가율 (%) + example: 15.3 + projectedParticipants: + type: integer + description: 예상 참여자 수 (기간 종료 시점) + example: 18500 + peakPeriod: + type: string + description: 피크 기간 + example: "2025-01-15 ~ 2025-01-18" + required: + - overallTrend + + PeakTimeInfo: + type: object + description: 피크 타임 정보 + properties: + timestamp: + type: string + format: date-time + description: 피크 시간 + example: "2025-01-15T14:00:00Z" + metric: + type: string + description: 피크 지표 + enum: + - participants + - views + - engagement + - conversions + example: "participants" + value: + type: integer + description: 피크 값 + example: 1250 + description: + type: string + description: 피크 설명 + example: "주말 오후 최대 참여" + + RoiAnalyticsResponse: + type: object + description: ROI 상세 분석 응답 + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_2025012301" + investment: + $ref: '#/components/schemas/InvestmentDetails' + revenue: + $ref: '#/components/schemas/RevenueDetails' + roi: + $ref: '#/components/schemas/RoiCalculation' + costEfficiency: + $ref: '#/components/schemas/CostEfficiency' + projection: + $ref: '#/components/schemas/RevenueProjection' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + required: + - eventId + - investment + - revenue + - roi + - lastUpdatedAt + + InvestmentDetails: + type: object + description: 투자 비용 상세 + properties: + contentCreation: + type: number + format: double + description: 콘텐츠 제작비 (원) + example: 2000000 + distribution: + type: number + format: double + description: 배포 비용 (원) + example: 2500000 + operation: + type: number + format: double + description: 운영 비용 (원) + example: 500000 + total: + type: number + format: double + description: 총 투자 비용 (원) + example: 5000000 + breakdown: + type: array + description: 채널별 비용 상세 + items: + type: object + properties: + channelName: + type: string + example: "우리동네TV" + cost: + type: number + format: double + example: 1500000 + required: + - total + + RevenueDetails: + type: object + description: 수익 상세 + properties: + directSales: + type: number + format: double + description: 직접 매출 (원) + example: 12500000 + expectedSales: + type: number + format: double + description: 예상 추가 매출 (원) + example: 6525000 + brandValue: + type: number + format: double + description: 브랜드 가치 향상 추정액 (원) + example: 3000000 + total: + type: number + format: double + description: 총 수익 (원) + example: 19025000 + required: + - total + + RoiCalculation: + type: object + description: ROI 계산 + properties: + netProfit: + type: number + format: double + description: 순이익 (원) + example: 14025000 + roiPercentage: + type: number + format: double + description: ROI (%) + example: 280.5 + breakEvenPoint: + type: string + format: date-time + description: 손익분기점 도달 시점 + example: "2025-01-10T15:30:00Z" + paybackPeriod: + type: integer + description: 투자 회수 기간 (일) + example: 9 + required: + - netProfit + - roiPercentage + + CostEfficiency: + type: object + description: 비용 효율성 + properties: + costPerParticipant: + type: number + format: double + description: 참여자당 비용 (원) + example: 324.35 + costPerConversion: + type: number + format: double + description: 전환당 비용 (원) + example: 850.34 + costPerView: + type: number + format: double + description: 조회당 비용 (원) + example: 39.90 + revenuePerParticipant: + type: number + format: double + description: 참여자당 수익 (원) + example: 1234.25 + required: + - costPerParticipant + + RevenueProjection: + type: object + description: 수익 예측 + properties: + currentRevenue: + type: number + format: double + description: 현재 누적 수익 (원) + example: 12500000 + projectedFinalRevenue: + type: number + format: double + description: 예상 최종 수익 (원) + example: 21000000 + confidenceLevel: + type: number + format: double + description: 예측 신뢰도 (%) + example: 85.5 + basedOn: + type: string + description: 예측 기반 + example: "현재 추세 및 과거 유사 이벤트 데이터" + + ErrorResponse: + type: object + description: 오류 응답 + properties: + timestamp: + type: string + format: date-time + description: 오류 발생 시간 + example: "2025-01-23T10:30:00Z" + status: + type: integer + description: HTTP 상태 코드 + example: 404 + error: + type: string + description: 오류 유형 + example: "Not Found" + message: + type: string + description: 오류 메시지 + example: "이벤트를 찾을 수 없습니다." + path: + type: string + description: 요청 경로 + example: "/api/events/evt_2025012301/analytics" + errorCode: + type: string + description: 내부 오류 코드 + example: "ANAL_001" + required: + - timestamp + - status + - error + - message + - path + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰 기반 인증 + +security: + - bearerAuth: [] diff --git a/design/backend/api/content-service-api.yaml b/design/backend/api/content-service-api.yaml new file mode 100644 index 0000000..d8f9f45 --- /dev/null +++ b/design/backend/api/content-service-api.yaml @@ -0,0 +1,1158 @@ +openapi: 3.0.3 +info: + title: Content Service API + version: 1.0.0 + description: | + # KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Content Service API + + ## 주요 기능 + - **SNS 이미지 생성** (UFR-CONT-010): AI 기반 이벤트 이미지 자동 생성 + - **콘텐츠 편집** (UFR-CONT-020): 생성된 이미지 조회, 재생성, 삭제 + - **3가지 스타일**: 심플(SIMPLE), 화려한(FANCY), 트렌디(TRENDY) + - **3개 플랫폼 최적화**: Instagram (1080x1080), Naver (800x600), Kakao (800x800) + - **Redis 캐싱**: TTL 7일, 동일 eventDraftId 재요청 시 캐시 반환 + - **CDN 이미지 저장**: Azure Blob Storage 기반 + + ## 비동기 처리 아키텍처 + + ### Kafka Job Consumer + **Topic**: `image-generation-job` + + **처리 흐름**: + 1. Kafka에서 이미지 생성 Job 수신 (EventService에서 발행) + 2. Redis에서 AI Service 이벤트 데이터 조회 + 3. Redis 캐시에서 기존 이미지 확인 (동일 eventDraftId) + 4. 외부 이미지 생성 API 호출 (Stable Diffusion / DALL-E) + - **Circuit Breaker**: 5분 타임아웃, 실패율 50% 초과 시 Open + - **Fallback**: Stable Diffusion → DALL-E → 기본 템플릿 이미지 + 5. 생성된 이미지 CDN(Azure Blob) 업로드 + 6. Redis에 이미지 URL 저장 (TTL 7일) + 7. Job 상태 업데이트 (PENDING → PROCESSING → COMPLETED/FAILED) + + **Job Payload Schema**: `ImageGenerationJob` (components/schemas 참조) + + ## 외부 API 연동 + - **Image Generation API**: Stable Diffusion / DALL-E + - **Circuit Breaker**: 5분 타임아웃, 50% 실패율 임계값 + - **CDN**: Azure Blob Storage (이미지 업로드) + + ## 출력 형식 + - 스타일별 3개 × 플랫폼별 3개 = **총 9개 이미지** 생성 (현재는 Instagram만) + - CDN URL 반환 + + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8082 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/content/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/content/v1 + description: Production Server + +tags: + - name: Job Status + description: 이미지 생성 작업 상태 조회 (비동기 폴링) + - name: Content Management + description: 생성된 콘텐츠 조회 및 관리 (UFR-CONT-020) + - name: Image Management + description: 이미지 재생성 및 삭제 (UFR-CONT-020) + +paths: + /content/images/generate: + post: + tags: + - Job Status + summary: SNS 이미지 생성 요청 (비동기) + description: | + 이벤트 정보를 기반으로 3가지 스타일의 SNS 이미지 생성을 비동기로 요청합니다. + + ## 처리 방식 + - **비동기 처리**: Kafka `image-generation-job` 토픽에 Job 발행 + - **폴링 조회**: jobId로 생성 상태 조회 (GET /content/images/jobs/{jobId}) + - **캐싱**: 동일한 eventDraftId 재요청 시 캐시 반환 (TTL 7일) + + ## 생성 스타일 + 1. **심플 스타일 (SIMPLE)**: 깔끔한 디자인, 텍스트 중심 + 2. **화려한 스타일 (FANCY)**: 눈에 띄는 디자인, 풍부한 색상 + 3. **트렌디 스타일 (TRENDY)**: 최신 트렌드, MZ세대 타겟 + + ## Resilience 패턴 + - Circuit Breaker (5분 타임아웃, 실패율 50% 초과 시 Open) + - Fallback (Stable Diffusion → DALL-E → 기본 템플릿) + + operationId: generateImages + x-user-story: UFR-CONT-010 + x-controller: ImageGenerationController.generateImages + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ImageGenerationRequest" + examples: + basicEvent: + summary: 기본 이벤트 + value: + eventDraftId: "evt-draft-12345" + eventInfo: + title: "봄맞이 커피 할인 이벤트" + giftName: "아메리카노 1+1" + brandColor: "#FF5733" + logoUrl: "https://cdn.example.com/logo.png" + withoutLogo: + summary: 로고 없는 이벤트 + value: + eventDraftId: "evt-draft-67890" + eventInfo: + title: "신메뉴 출시 기념 경품 추첨" + giftName: "스타벅스 기프티콘 5000원권" + brandColor: "#00704A" + + responses: + "202": + description: | + 이미지 생성 요청이 성공적으로 접수되었습니다. + jobId를 사용하여 생성 상태를 폴링 조회하세요. + content: + application/json: + schema: + $ref: "#/components/schemas/ImageGenerationAcceptedResponse" + examples: + accepted: + summary: 요청 접수 성공 + value: + jobId: "job-img-abc123" + eventDraftId: "evt-draft-12345" + status: "PENDING" + estimatedCompletionTime: 5 + message: "이미지 생성 요청이 접수되었습니다. jobId로 결과를 조회하세요." + + "400": + description: 잘못된 요청 (필수 필드 누락, 유효하지 않은 데이터) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + missingField: + summary: 필수 필드 누락 + value: + code: "BAD_REQUEST" + message: "eventInfo.title은 필수 항목입니다." + timestamp: "2025-10-22T14:30:00Z" + invalidColor: + summary: 유효하지 않은 색상 코드 + value: + code: "BAD_REQUEST" + message: "brandColor는 HEX 색상 코드 형식이어야 합니다." + timestamp: "2025-10-22T14:30:00Z" + + "429": + description: 요청 제한 초과 (Rate Limiting) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + rateLimitExceeded: + summary: 요청 제한 초과 + value: + code: "RATE_LIMIT_EXCEEDED" + message: "요청 한도를 초과했습니다. 1분 후 다시 시도하세요." + timestamp: "2025-10-22T14:30:00Z" + retryAfter: 60 + + "500": + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + internalError: + summary: 서버 내부 오류 + value: + code: "INTERNAL_SERVER_ERROR" + message: "이미지 생성 요청 처리 중 오류가 발생했습니다." + timestamp: "2025-10-22T14:30:00Z" + + security: + - BearerAuth: [] + + /content/images/jobs/{jobId}: + get: + tags: + - Job Status + summary: 이미지 생성 작업 상태 조회 (폴링) + description: | + jobId로 이미지 생성 상태를 조회합니다. + + ## 폴링 권장사항 + - **폴링 간격**: 2초 + - **최대 폴링 시간**: 30초 + - **Timeout 후 처리**: 에러 메시지 표시 및 재시도 옵션 제공 + + ## 상태 종류 + - **PENDING**: 대기 중 (Kafka Queue에서 대기) + - **PROCESSING**: 생성 중 (AI API 호출 진행) + - **COMPLETED**: 완료 (3가지 이미지 URL 반환) + - **FAILED**: 실패 (에러 메시지 포함, Fallback 이미지 제공) + + ## 캐싱 + - COMPLETED 상태는 Redis 캐싱 (TTL 7일) + - 동일한 eventDraftId 재요청 시 즉시 반환 + + operationId: getImageGenerationStatus + x-user-story: UFR-CONT-010 + x-controller: ImageGenerationController.getJobStatus + parameters: + - name: jobId + in: path + required: true + description: 이미지 생성 Job ID + schema: + type: string + example: "job-img-abc123" + + responses: + "200": + description: 이미지 생성 상태 조회 성공 + content: + application/json: + schema: + $ref: "#/components/schemas/ImageGenerationStatusResponse" + examples: + pending: + summary: 대기 중 + value: + jobId: "job-img-abc123" + status: "PENDING" + message: "이미지 생성 대기 중입니다." + estimatedCompletionTime: 5 + + processing: + summary: 생성 중 + value: + jobId: "job-img-abc123" + status: "PROCESSING" + message: "AI가 이벤트에 어울리는 이미지를 생성하고 있어요..." + estimatedCompletionTime: 3 + + completed: + summary: 생성 완료 + value: + jobId: "job-img-abc123" + status: "COMPLETED" + message: "이미지 생성이 완료되었습니다." + images: + - style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + completedAt: "2025-10-22T14:30:05Z" + fromCache: false + + completedFromCache: + summary: 캐시에서 반환 + value: + jobId: "job-img-def456" + status: "COMPLETED" + message: "이미지 생성이 완료되었습니다. (캐시)" + images: + - style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + completedAt: "2025-10-22T14:28:00Z" + fromCache: true + + failed: + summary: 생성 실패 + value: + jobId: "job-img-abc123" + status: "FAILED" + message: "이미지 생성에 실패했습니다." + error: + code: "IMAGE_GENERATION_FAILED" + detail: "외부 AI API 응답 시간 초과. 기본 템플릿으로 대체되었습니다." + completedAt: "2025-10-22T14:30:25Z" + + "404": + description: Job ID를 찾을 수 없음 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + notFound: + summary: Job ID 없음 + value: + code: "NOT_FOUND" + message: "Job ID를 찾을 수 없습니다." + timestamp: "2025-10-22T14:30:00Z" + + "500": + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + internalError: + summary: 서버 내부 오류 + value: + code: "INTERNAL_SERVER_ERROR" + message: "상태 조회 중 오류가 발생했습니다." + timestamp: "2025-10-22T14:30:00Z" + + security: + - BearerAuth: [] + + /content/events/{eventDraftId}: + get: + tags: + - Content Management + summary: 이벤트의 생성된 콘텐츠 조회 + description: | + 특정 이벤트의 생성된 모든 콘텐츠(이미지) 조회 + - Redis 캐시에서 조회 + - TTL 7일 이내 데이터만 조회 가능 + - 캐시 만료 시 404 반환 + + operationId: getContentByEventId + x-user-story: UFR-CONT-020 + x-controller: ContentController.getContentByEventId + parameters: + - name: eventDraftId + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + example: "evt-draft-12345" + + responses: + "200": + description: 콘텐츠 조회 성공 + content: + application/json: + schema: + $ref: "#/components/schemas/ContentResponse" + examples: + success: + summary: 콘텐츠 조회 성공 + value: + eventDraftId: "evt-draft-12345" + images: + - imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + - imageId: "img-12345-fancy" + style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:10Z" + - imageId: "img-12345-trendy" + style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:15Z" + totalCount: 3 + createdAt: "2025-10-22T14:30:00Z" + expiresAt: "2025-10-29T14:30:00Z" + + "404": + description: 콘텐츠를 찾을 수 없음 (생성 중이거나 만료됨) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + notFound: + summary: 콘텐츠 없음 + value: + code: "CONTENT_NOT_FOUND" + message: "해당 이벤트의 콘텐츠를 찾을 수 없습니다." + timestamp: "2025-10-22T14:30:00Z" + + "500": + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + /content/events/{eventDraftId}/images: + get: + tags: + - Content Management + summary: 이벤트의 이미지 목록 조회 (필터링) + description: | + 특정 이벤트의 모든 생성 이미지 목록 조회 + - 스타일별, 플랫폼별 필터링 지원 + + operationId: getImages + x-user-story: UFR-CONT-020 + x-controller: ContentController.getImages + parameters: + - name: eventDraftId + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + example: "evt-draft-12345" + - name: style + in: query + required: false + description: 이미지 스타일 필터 + schema: + type: string + enum: [SIMPLE, FANCY, TRENDY] + example: "SIMPLE" + - name: platform + in: query + required: false + description: 플랫폼 필터 + schema: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + example: "INSTAGRAM" + + responses: + "200": + description: 이미지 목록 조회 성공 + content: + application/json: + schema: + type: object + properties: + eventDraftId: + type: string + totalCount: + type: integer + images: + type: array + items: + $ref: "#/components/schemas/GeneratedImage" + examples: + allImages: + summary: 전체 이미지 조회 + value: + eventDraftId: "evt-draft-12345" + totalCount: 3 + images: + - imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + + "404": + description: 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + /content/images/{imageId}: + get: + tags: + - Image Management + summary: 특정 이미지 상세 조회 + description: 이미지 ID로 특정 이미지의 상세 정보 조회 + + operationId: getImageById + x-user-story: UFR-CONT-020 + x-controller: ContentController.getImageById + parameters: + - name: imageId + in: path + required: true + description: 이미지 ID + schema: + type: string + example: "img-12345-simple" + + responses: + "200": + description: 이미지 조회 성공 + content: + application/json: + schema: + $ref: "#/components/schemas/GeneratedImage" + examples: + success: + summary: 이미지 조회 성공 + value: + imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + + "404": + description: 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + delete: + tags: + - Image Management + summary: 생성된 이미지 삭제 + description: | + 특정 이미지 삭제 + - Redis 캐시에서 제거 + - CDN 이미지는 유지 (비용 고려) + + operationId: deleteImage + x-user-story: UFR-CONT-020 + x-controller: ContentController.deleteImage + parameters: + - name: imageId + in: path + required: true + description: 이미지 ID + schema: + type: string + example: "img-12345-simple" + + responses: + "204": + description: 이미지 삭제 성공 + + "404": + description: 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + /content/images/{imageId}/regenerate: + post: + tags: + - Image Management + summary: 이미지 재생성 요청 + description: | + 특정 이미지를 재생성 (콘텐츠 편집) + - 동일한 스타일/플랫폼으로 재생성 + - 프롬프트 수정 가능 + - 비동기 처리 (Kafka Job 발행) + + operationId: regenerateImage + x-user-story: UFR-CONT-020 + x-controller: ContentController.regenerateImage + parameters: + - name: imageId + in: path + required: true + description: 이미지 ID + schema: + type: string + example: "img-12345-simple" + + requestBody: + required: false + description: 재생성 옵션 (선택사항) + content: + application/json: + schema: + $ref: "#/components/schemas/ImageRegenerationRequest" + examples: + modifyPrompt: + summary: 프롬프트 수정 + value: + content: "밝은 분위기로 변경" + changeStyle: + summary: 스타일 변경 + value: + style: "FANCY" + + responses: + "202": + description: 재생성 요청 접수 (비동기 처리) + content: + application/json: + schema: + type: object + required: + - message + - jobId + - estimatedTime + properties: + message: + type: string + example: "이미지 재생성 요청이 접수되었습니다" + jobId: + type: string + example: "job-regen-abc123" + estimatedTime: + type: integer + description: 예상 소요 시간 (초) + example: 10 + + "404": + description: 원본 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + "503": + description: 이미지 생성 서비스 장애 (Circuit Breaker Open) + content: + application/json: + schema: + $ref: "#/components/schemas/CircuitBreakerErrorResponse" + + security: + - BearerAuth: [] + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰을 Authorization 헤더에 포함 (Bearer {token}) + + schemas: + # ======================================== + # Kafka Job Schema (비동기 처리) + # ======================================== + ImageGenerationJob: + type: object + description: | + **Kafka Topic**: `image-generation-job` + + Event Service에서 발행하여 Content Service가 소비하는 Job Payload + - Content Service의 Kafka Consumer가 처리 + - 비동기 이미지 생성 작업 수행 + required: + - jobId + - eventDraftId + - eventInfo + properties: + jobId: + type: string + description: Job ID (작업 추적용) + example: "job-img-abc123" + + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt-draft-12345" + + eventInfo: + type: object + description: 이벤트 정보 (AI Service에서 생성) + required: + - title + - giftName + properties: + title: + type: string + description: 이벤트 제목 + example: "봄맞이 커피 할인 이벤트" + giftName: + type: string + description: 경품명 + example: "아메리카노 1+1" + brandColor: + type: string + description: 브랜드 컬러 (HEX) + pattern: "^#[0-9A-Fa-f]{6}$" + example: "#FF5733" + logoUrl: + type: string + format: uri + description: 로고 이미지 URL (선택) + example: "https://cdn.example.com/logo.png" + + styles: + type: array + description: 생성할 스타일 목록 (기본값 전체) + items: + type: string + enum: [SIMPLE, FANCY, TRENDY] + example: ["SIMPLE", "FANCY", "TRENDY"] + + platforms: + type: array + description: 생성할 플랫폼 목록 (기본값 Instagram) + items: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + example: ["INSTAGRAM"] + + priority: + type: integer + description: 우선순위 (1-10, 높을수록 우선) + minimum: 1 + maximum: 10 + example: 5 + + requestedAt: + type: string + format: date-time + description: 요청 시각 + example: "2025-10-22T14:00:00Z" + + # ======================================== + # Request Schemas + # ======================================== + ImageGenerationRequest: + type: object + required: + - eventDraftId + - eventInfo + properties: + eventDraftId: + type: string + description: | + 이벤트 초안 ID (Event Service에서 발급) + 동일한 eventDraftId로 재요청 시 캐시된 이미지 반환 + example: "evt-draft-12345" + + eventInfo: + type: object + required: + - title + - giftName + properties: + title: + type: string + description: 이벤트 제목 (최대 50자) + minLength: 1 + maxLength: 50 + example: "봄맞이 커피 할인 이벤트" + + giftName: + type: string + description: 경품명 (최대 30자) + minLength: 1 + maxLength: 30 + example: "아메리카노 1+1" + + brandColor: + type: string + description: | + 브랜드 컬러 (HEX 색상 코드) + 사용자 프로필에서 가져오거나 기본값 사용 + pattern: "^#[0-9A-Fa-f]{6}$" + example: "#FF5733" + + logoUrl: + type: string + format: uri + description: | + 로고 이미지 URL (선택) + 업로드된 경우에만 제공 + example: "https://cdn.example.com/logo.png" + + ImageGenerationAcceptedResponse: + type: object + required: + - jobId + - eventDraftId + - status + - message + properties: + jobId: + type: string + description: 이미지 생성 Job ID (폴링 조회에 사용) + example: "job-img-abc123" + + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt-draft-12345" + + status: + type: string + enum: [PENDING] + description: 초기 상태는 항상 PENDING + example: "PENDING" + + estimatedCompletionTime: + type: integer + description: 예상 완료 시간 (초) + example: 5 + + message: + type: string + description: 응답 메시지 + example: "이미지 생성 요청이 접수되었습니다. jobId로 결과를 조회하세요." + + ImageGenerationStatusResponse: + type: object + required: + - jobId + - status + - message + properties: + jobId: + type: string + description: 이미지 생성 Job ID + example: "job-img-abc123" + + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: | + Job 상태 + - PENDING: 대기 중 + - PROCESSING: 생성 중 + - COMPLETED: 완료 + - FAILED: 실패 + example: "COMPLETED" + + message: + type: string + description: 상태 메시지 + example: "이미지 생성이 완료되었습니다." + + estimatedCompletionTime: + type: integer + description: 예상 완료 시간 (초, PENDING/PROCESSING 상태에서만) + example: 3 + + images: + type: array + description: 생성된 이미지 배열 (COMPLETED 상태에서만) + items: + $ref: "#/components/schemas/GeneratedImage" + + completedAt: + type: string + format: date-time + description: 완료 시각 (COMPLETED/FAILED 상태에서만) + example: "2025-10-22T14:30:05Z" + + fromCache: + type: boolean + description: 캐시에서 반환 여부 (COMPLETED 상태에서만) + example: false + + error: + $ref: "#/components/schemas/JobError" + + GeneratedImage: + type: object + required: + - style + - url + - platform + - size + properties: + style: + type: string + enum: [SIMPLE, FANCY, TRENDY] + description: | + 이미지 스타일 + - SIMPLE: 심플 스타일 (깔끔한 디자인, 텍스트 중심) + - FANCY: 화려한 스타일 (눈에 띄는 디자인, 풍부한 색상) + - TRENDY: 트렌디 스타일 (최신 트렌드, MZ세대 타겟) + example: "SIMPLE" + + url: + type: string + format: uri + description: CDN 이미지 URL + example: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + + platform: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: | + 플랫폼별 최적화 + - INSTAGRAM: 1080x1080 + - NAVER_BLOG: 800x600 + - KAKAO_CHANNEL: 800x800 + example: "INSTAGRAM" + + size: + type: object + required: + - width + - height + properties: + width: + type: integer + description: 이미지 너비 (픽셀) + example: 1080 + + height: + type: integer + description: 이미지 높이 (픽셀) + example: 1080 + + JobError: + type: object + required: + - code + - detail + description: Job 실패 시 에러 정보 (FAILED 상태에서만) + properties: + code: + type: string + description: 에러 코드 + example: "IMAGE_GENERATION_FAILED" + + detail: + type: string + description: 상세 에러 메시지 + example: "외부 AI API 응답 시간 초과. 기본 템플릿으로 대체되었습니다." + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 에러 코드 + example: "BAD_REQUEST" + + message: + type: string + description: 에러 메시지 + example: "eventInfo.title은 필수 항목입니다." + + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-10-22T14:30:00Z" + + retryAfter: + type: integer + description: 재시도 대기 시간 (초, Rate Limiting 에러에서만) + example: 60 + + # ======================================== + # Content Management Schemas (UFR-CONT-020) + # ======================================== + ContentResponse: + type: object + description: 이벤트의 생성된 콘텐츠 전체 정보 + required: + - eventDraftId + - images + - totalCount + - createdAt + - expiresAt + properties: + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt-draft-12345" + + images: + type: array + description: 생성된 이미지 목록 + items: + $ref: "#/components/schemas/ImageDetail" + + totalCount: + type: integer + description: 총 이미지 개수 + example: 3 + + createdAt: + type: string + format: date-time + description: 콘텐츠 생성 시각 + example: "2025-10-22T14:30:00Z" + + expiresAt: + type: string + format: date-time + description: 캐시 만료 시각 (TTL 7일) + example: "2025-10-29T14:30:00Z" + + ImageDetail: + type: object + description: 상세 이미지 정보 (생성 시각 포함) + required: + - imageId + - style + - url + - platform + - size + - createdAt + properties: + imageId: + type: string + description: 이미지 ID + example: "img-12345-simple" + + style: + type: string + enum: [SIMPLE, FANCY, TRENDY] + description: 이미지 스타일 + example: "SIMPLE" + + url: + type: string + format: uri + description: CDN 이미지 URL (Azure Blob Storage) + example: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + + platform: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: 플랫폼 + example: "INSTAGRAM" + + size: + type: object + required: + - width + - height + properties: + width: + type: integer + description: 이미지 너비 (픽셀) + example: 1080 + height: + type: integer + description: 이미지 높이 (픽셀) + example: 1080 + + createdAt: + type: string + format: date-time + description: 이미지 생성 시각 + example: "2025-10-22T14:30:05Z" + + fallbackUsed: + type: boolean + description: Fallback 이미지 사용 여부 + example: false + + ImageRegenerationRequest: + type: object + description: 이미지 재생성 요청 (콘텐츠 편집) + properties: + content: + type: string + description: 수정된 프롬프트 (선택사항) + example: "밝은 분위기로 변경" + + style: + type: string + description: 변경할 스타일 (선택사항) + enum: [SIMPLE, FANCY, TRENDY] + example: "FANCY" + + CircuitBreakerErrorResponse: + type: object + description: Circuit Breaker 오류 응답 (외부 API 장애) + required: + - code + - message + - timestamp + - circuitBreakerState + properties: + code: + type: string + description: 에러 코드 + example: "IMAGE_GENERATION_SERVICE_UNAVAILABLE" + + message: + type: string + description: 에러 메시지 + example: "이미지 생성 서비스가 일시적으로 사용 불가능합니다" + + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-10-22T14:30:00Z" + + circuitBreakerState: + type: string + enum: [OPEN, HALF_OPEN, CLOSED] + description: Circuit Breaker 상태 + example: "OPEN" + + fallbackAvailable: + type: boolean + description: Fallback 이미지 사용 가능 여부 + example: true + + fallbackImageUrl: + type: string + format: uri + description: Fallback 템플릿 이미지 URL (사용 가능한 경우) + example: "https://cdn.kt-event.com/templates/default_event.png" + + retryAfter: + type: integer + description: 재시도 가능 시간 (초) + example: 300 diff --git a/design/backend/api/distribution-service-api.yaml b/design/backend/api/distribution-service-api.yaml new file mode 100644 index 0000000..938b3a8 --- /dev/null +++ b/design/backend/api/distribution-service-api.yaml @@ -0,0 +1,651 @@ +openapi: 3.0.3 +info: + title: Distribution Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스의 다중 채널 배포 관리 API + + ## 주요 기능 + - 다중 채널 동시 배포 (우리동네TV, 링고비즈, 지니TV, SNS) + - 배포 상태 실시간 모니터링 + - Circuit Breaker 기반 장애 격리 + - Retry 패턴 및 Fallback 처리 + + ## 배포 채널 + - **우리동네TV**: 영상 콘텐츠 업로드 + - **링고비즈**: 연결음 업데이트 + - **지니TV**: 광고 등록 + - **SNS**: Instagram, Naver Blog, Kakao Channel + + ## Resilience 패턴 + - Circuit Breaker: 채널별 독립적 장애 격리 + - Retry: 지수 백오프 (1s, 2s, 4s) 최대 3회 + - Bulkhead: 리소스 격리 + - Fallback: 실패 채널 스킵 및 알림 + + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8085 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/distribution/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/distribution/v1 + description: Production Server + +tags: + - name: Distribution + description: 다중 채널 배포 관리 + - name: Monitoring + description: 배포 상태 모니터링 + +paths: + /distribution/distribute: + post: + tags: + - Distribution + summary: 다중 채널 배포 요청 + description: | + 이벤트 콘텐츠를 선택된 채널들에 동시 배포합니다. + + ## 처리 흐름 + 1. 배포 요청 검증 (이벤트 ID, 채널 목록, 콘텐츠 데이터) + 2. 채널별 병렬 배포 실행 (1분 이내 완료 목표) + 3. Circuit Breaker로 장애 채널 격리 + 4. 실패 시 Retry (지수 백오프: 1s, 2s, 4s) + 5. Fallback: 실패 채널 스킵 및 알림 + 6. 배포 결과 집계 및 로그 저장 + 7. DistributionCompleted 이벤트 Kafka 발행 + + ## Resilience 처리 + - 각 채널별 독립적인 Circuit Breaker 적용 + - 최대 3회 재시도 (지수 백오프) + - 일부 채널 실패 시에도 성공 채널은 유지 + - 실패 채널 정보는 응답에 포함 + + x-user-story: UFR-DIST-010 + x-controller: DistributionController + operationId: distributeToChannels + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionRequest' + examples: + multiChannel: + summary: 다중 채널 배포 예시 + value: + eventId: "evt-12345" + channels: + - type: "WOORIDONGNE_TV" + config: + radius: "1km" + timeSlots: + - "weekday_evening" + - "weekend_lunch" + - type: "INSTAGRAM" + config: + scheduledTime: "2025-11-01T10:00:00Z" + - type: "NAVER_BLOG" + config: + scheduledTime: "2025-11-01T10:30:00Z" + contentUrls: + instagram: "https://cdn.example.com/images/event-instagram.jpg" + naverBlog: "https://cdn.example.com/images/event-naver.jpg" + kakaoChannel: "https://cdn.example.com/images/event-kakao.jpg" + responses: + '200': + description: 배포 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionResponse' + examples: + allSuccess: + summary: 모든 채널 배포 성공 + value: + distributionId: "dist-12345" + eventId: "evt-12345" + status: "COMPLETED" + completedAt: "2025-11-01T09:00:00Z" + results: + - channel: "WOORIDONGNE_TV" + status: "SUCCESS" + distributionId: "wtv-uuid-12345" + estimatedViews: 1000 + message: "배포 완료" + - channel: "INSTAGRAM" + status: "SUCCESS" + postUrl: "https://instagram.com/p/generated-post-id" + postId: "ig-post-12345" + message: "게시 완료" + - channel: "NAVER_BLOG" + status: "SUCCESS" + postUrl: "https://blog.naver.com/store123/generated-post" + message: "게시 완료" + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidEventId: + summary: 유효하지 않은 이벤트 ID + value: + error: "BAD_REQUEST" + message: "유효하지 않은 이벤트 ID입니다" + timestamp: "2025-11-01T09:00:00Z" + noChannels: + summary: 선택된 채널 없음 + value: + error: "BAD_REQUEST" + message: "최소 1개 이상의 채널을 선택해야 합니다" + timestamp: "2025-11-01T09:00:00Z" + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + eventNotFound: + summary: 존재하지 않는 이벤트 + value: + error: "NOT_FOUND" + message: "이벤트를 찾을 수 없습니다: evt-12345" + timestamp: "2025-11-01T09:00:00Z" + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + internalError: + summary: 서버 오류 + value: + error: "INTERNAL_SERVER_ERROR" + message: "배포 처리 중 오류가 발생했습니다" + timestamp: "2025-11-01T09:00:00Z" + + /distribution/{eventId}/status: + get: + tags: + - Monitoring + summary: 배포 상태 조회 + description: | + 특정 이벤트의 배포 상태를 실시간으로 조회합니다. + + ## 조회 정보 + - 전체 배포 상태 (진행중, 완료, 부분성공, 실패) + - 채널별 배포 상태 및 결과 + - 실패 채널 상세 정보 (오류 유형, 재시도 횟수) + - 배포 시작/완료 시간 및 소요 시간 + - 외부 채널 ID 및 배포 URL + + ## 상태 값 + - **IN_PROGRESS**: 배포 진행 중 + - **COMPLETED**: 모든 채널 배포 완료 + - **PARTIAL_SUCCESS**: 일부 채널 배포 성공 + - **FAILED**: 모든 채널 배포 실패 + + x-user-story: UFR-DIST-020 + x-controller: DistributionController + operationId: getDistributionStatus + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt-12345" + responses: + '200': + description: 배포 상태 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionStatusResponse' + examples: + completed: + summary: 배포 완료 상태 + value: + eventId: "evt-12345" + overallStatus: "COMPLETED" + completedAt: "2025-11-01T09:00:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + completedAt: "2025-11-01T09:00:00Z" + - channel: "RINGO_BIZ" + status: "COMPLETED" + updateTimestamp: "2025-11-01T09:00:00Z" + - channel: "GENIE_TV" + status: "COMPLETED" + adId: "gtv-uuid-12345" + impressionSchedule: + - "2025-11-01 18:00-20:00" + - "2025-11-02 12:00-14:00" + - channel: "INSTAGRAM" + status: "COMPLETED" + postUrl: "https://instagram.com/p/generated-post-id" + postId: "ig-post-12345" + - channel: "NAVER_BLOG" + status: "COMPLETED" + postUrl: "https://blog.naver.com/store123/generated-post" + - channel: "KAKAO_CHANNEL" + status: "COMPLETED" + messageId: "kakao-msg-12345" + inProgress: + summary: 배포 진행중 상태 + value: + eventId: "evt-12345" + overallStatus: "IN_PROGRESS" + startedAt: "2025-11-01T08:58:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + - channel: "INSTAGRAM" + status: "IN_PROGRESS" + progress: 50 + - channel: "NAVER_BLOG" + status: "PENDING" + partialFailure: + summary: 일부 채널 실패 상태 + value: + eventId: "evt-12345" + overallStatus: "PARTIAL_FAILURE" + completedAt: "2025-11-01T09:00:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + - channel: "INSTAGRAM" + status: "FAILED" + errorMessage: "Instagram API 타임아웃" + retries: 3 + lastRetryAt: "2025-11-01T08:59:30Z" + - channel: "NAVER_BLOG" + status: "COMPLETED" + postUrl: "https://blog.naver.com/store123/generated-post" + '404': + description: 배포 이력을 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 배포 이력 없음 + value: + error: "NOT_FOUND" + message: "배포 이력을 찾을 수 없습니다: evt-12345" + timestamp: "2025-11-01T09:00:00Z" + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + schemas: + DistributionRequest: + type: object + required: + - eventId + - channels + - contentUrls + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + channels: + type: array + description: 배포할 채널 목록 + minItems: 1 + items: + $ref: '#/components/schemas/ChannelConfig' + contentUrls: + type: object + description: 플랫폼별 콘텐츠 URL + properties: + wooridongneTV: + type: string + description: 우리동네TV 영상 URL (15초) + example: "https://cdn.example.com/videos/event-15s.mp4" + ringoBiz: + type: string + description: 링고비즈 연결음 파일 URL + example: "https://cdn.example.com/audio/ringtone.mp3" + genieTV: + type: string + description: 지니TV 광고 영상 URL + example: "https://cdn.example.com/videos/event-ad.mp4" + instagram: + type: string + description: Instagram 이미지 URL (1080x1080) + example: "https://cdn.example.com/images/event-instagram.jpg" + naverBlog: + type: string + description: Naver Blog 이미지 URL (800x600) + example: "https://cdn.example.com/images/event-naver.jpg" + kakaoChannel: + type: string + description: Kakao Channel 이미지 URL (800x800) + example: "https://cdn.example.com/images/event-kakao.jpg" + + ChannelConfig: + type: object + required: + - type + properties: + type: + type: string + description: 채널 타입 + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + config: + type: object + description: 채널별 설정 (채널에 따라 다름) + additionalProperties: true + example: + scheduledTime: "2025-11-01T10:00:00Z" + caption: "이벤트 안내" + hashtags: + - "이벤트" + - "할인" + + DistributionResponse: + type: object + required: + - distributionId + - eventId + - status + - results + properties: + distributionId: + type: string + description: 배포 ID + example: "dist-12345" + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + status: + type: string + description: 전체 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - PARTIAL_FAILURE + - FAILED + example: "COMPLETED" + startedAt: + type: string + format: date-time + description: 배포 시작 시각 + example: "2025-11-01T08:59:00Z" + completedAt: + type: string + format: date-time + description: 배포 완료 시각 + example: "2025-11-01T09:00:00Z" + results: + type: array + description: 채널별 배포 결과 + items: + $ref: '#/components/schemas/ChannelResult' + + ChannelResult: + type: object + required: + - channel + - status + properties: + channel: + type: string + description: 채널 타입 + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + status: + type: string + description: 채널별 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - SUCCESS + - FAILED + example: "SUCCESS" + distributionId: + type: string + description: 채널별 배포 ID (우리동네TV, 지니TV) + example: "wtv-uuid-12345" + estimatedViews: + type: integer + description: 예상 노출 수 (우리동네TV, 지니TV) + example: 1500 + updateTimestamp: + type: string + format: date-time + description: 업데이트 완료 시각 (링고비즈) + example: "2025-11-01T09:00:00Z" + adId: + type: string + description: 광고 ID (지니TV) + example: "gtv-uuid-12345" + impressionSchedule: + type: array + description: 노출 스케줄 (지니TV) + items: + type: string + example: + - "2025-11-01 18:00-20:00" + - "2025-11-02 12:00-14:00" + postUrl: + type: string + description: 게시물 URL (Instagram, Naver Blog) + example: "https://instagram.com/p/generated-post-id" + postId: + type: string + description: 게시물 ID (Instagram) + example: "ig-post-12345" + messageId: + type: string + description: 메시지 ID (Kakao Channel) + example: "kakao-msg-12345" + message: + type: string + description: 결과 메시지 + example: "배포 완료" + errorMessage: + type: string + description: 오류 메시지 (실패 시) + example: "Instagram API 타임아웃" + retries: + type: integer + description: 재시도 횟수 + example: 0 + lastRetryAt: + type: string + format: date-time + description: 마지막 재시도 시각 + example: "2025-11-01T08:59:30Z" + + DistributionStatusResponse: + type: object + required: + - eventId + - overallStatus + - channels + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + overallStatus: + type: string + description: 전체 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - PARTIAL_FAILURE + - FAILED + - NOT_FOUND + example: "COMPLETED" + startedAt: + type: string + format: date-time + description: 배포 시작 시각 + example: "2025-11-01T08:59:00Z" + completedAt: + type: string + format: date-time + description: 배포 완료 시각 + example: "2025-11-01T09:00:00Z" + channels: + type: array + description: 채널별 배포 상태 + items: + $ref: '#/components/schemas/ChannelStatus' + + ChannelStatus: + type: object + required: + - channel + - status + properties: + channel: + type: string + description: 채널 타입 + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + status: + type: string + description: 채널별 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - FAILED + example: "COMPLETED" + progress: + type: integer + description: 진행률 (0-100, IN_PROGRESS 상태일 때) + minimum: 0 + maximum: 100 + example: 75 + distributionId: + type: string + description: 채널별 배포 ID + example: "wtv-uuid-12345" + estimatedViews: + type: integer + description: 예상 노출 수 + example: 1500 + updateTimestamp: + type: string + format: date-time + description: 업데이트 완료 시각 + example: "2025-11-01T09:00:00Z" + adId: + type: string + description: 광고 ID + example: "gtv-uuid-12345" + impressionSchedule: + type: array + description: 노출 스케줄 + items: + type: string + example: + - "2025-11-01 18:00-20:00" + postUrl: + type: string + description: 게시물 URL + example: "https://instagram.com/p/generated-post-id" + postId: + type: string + description: 게시물 ID + example: "ig-post-12345" + messageId: + type: string + description: 메시지 ID + example: "kakao-msg-12345" + completedAt: + type: string + format: date-time + description: 완료 시각 + example: "2025-11-01T09:00:00Z" + errorMessage: + type: string + description: 오류 메시지 + example: "Instagram API 타임아웃" + retries: + type: integer + description: 재시도 횟수 + example: 3 + lastRetryAt: + type: string + format: date-time + description: 마지막 재시도 시각 + example: "2025-11-01T08:59:30Z" + + ErrorResponse: + type: object + required: + - error + - message + - timestamp + properties: + error: + type: string + description: 오류 코드 + enum: + - BAD_REQUEST + - NOT_FOUND + - INTERNAL_SERVER_ERROR + example: "BAD_REQUEST" + message: + type: string + description: 오류 메시지 + example: "유효하지 않은 이벤트 ID입니다" + timestamp: + type: string + format: date-time + description: 오류 발생 시각 + example: "2025-11-01T09:00:00Z" + details: + type: object + description: 추가 오류 정보 (선택 사항) + additionalProperties: true diff --git a/design/backend/api/event-service-api.yaml b/design/backend/api/event-service-api.yaml new file mode 100644 index 0000000..c179b53 --- /dev/null +++ b/design/backend/api/event-service-api.yaml @@ -0,0 +1,1384 @@ +openapi: 3.0.3 +info: + title: Event Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Event Service API + + 이벤트 전체 생명주기 관리 (생성, 조회, 수정, 배포, 종료) + - AI 기반 이벤트 추천 및 커스터마이징 + - 이미지 생성 및 편집 오케스트레이션 + - 배포 채널 관리 및 최종 배포 + - 이벤트 상태 관리 (DRAFT, PUBLISHED, ENDED) + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8080 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/event/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/event/v1 + description: Production Server + +security: + - BearerAuth: [] + +tags: + - name: Dashboard + description: 대시보드 및 이벤트 목록 조회 + - name: Event Creation + description: 이벤트 생성 플로우 + - name: Event Management + description: 이벤트 수정, 삭제, 종료 + - name: Job Status + description: 비동기 작업 상태 조회 + +paths: + /events: + get: + tags: + - Dashboard + summary: 이벤트 목록 조회 + description: | + 사용자의 이벤트 목록을 조회합니다 (대시보드, 전체보기). + 필터, 검색, 페이징을 지원합니다. + operationId: getEvents + x-user-story: UFR-EVENT-010, UFR-EVENT-070 + x-controller: EventController.getEvents + parameters: + - name: status + in: query + description: 이벤트 상태 필터 (DRAFT, PUBLISHED, ENDED) + required: false + schema: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + example: PUBLISHED + - name: objective + in: query + description: 이벤트 목적 필터 + required: false + schema: + type: string + example: 신규 고객 유치 + - name: search + in: query + description: 검색어 (이벤트명) + required: false + schema: + type: string + example: 봄맞이 + - name: page + in: query + description: 페이지 번호 (0부터 시작) + required: false + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + - name: size + in: query + description: 페이지 크기 + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + example: 20 + - name: sort + in: query + description: 정렬 기준 (createdAt, startDate, endDate) + required: false + schema: + type: string + enum: [createdAt, startDate, endDate] + default: createdAt + example: createdAt + - name: order + in: query + description: 정렬 순서 (asc, desc) + required: false + schema: + type: string + enum: [asc, desc] + default: desc + example: desc + responses: + '200': + description: 이벤트 목록 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventListResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}: + get: + tags: + - Dashboard + summary: 이벤트 상세 조회 + description: 특정 이벤트의 상세 정보를 조회합니다. + operationId: getEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.getEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '200': + description: 이벤트 상세 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + put: + tags: + - Event Management + summary: 이벤트 수정 + description: | + 기존 이벤트의 정보를 수정합니다. + DRAFT 상태의 이벤트만 전체 수정 가능하며, + PUBLISHED 상태에서는 제한적 수정만 가능합니다. + operationId: updateEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.updateEvent + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateEventRequest' + responses: + '200': + description: 이벤트 수정 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태로 인해 수정 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_MODIFIABLE + message: PUBLISHED 상태의 이벤트는 제한적으로만 수정 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + delete: + tags: + - Event Management + summary: 이벤트 삭제 + description: | + 이벤트를 삭제합니다. + DRAFT 상태의 이벤트만 삭제 가능합니다. + operationId: deleteEvent + x-user-story: UFR-EVENT-070 + x-controller: EventController.deleteEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '204': + description: 이벤트 삭제 성공 + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태로 인해 삭제 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_DELETABLE + message: DRAFT 상태의 이벤트만 삭제 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/objectives: + post: + tags: + - Event Creation + summary: 이벤트 목적 선택 (Step 1) + description: | + 이벤트 생성 플로우의 첫 단계입니다. + 사용자가 이벤트 목적을 선택하고 DRAFT 상태의 이벤트를 생성합니다. + operationId: selectObjective + x-user-story: UFR-EVENT-020 + x-controller: EventController.selectObjective + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectObjectiveRequest' + responses: + '201': + description: 이벤트 생성 성공 (DRAFT 상태) + content: + application/json: + schema: + $ref: '#/components/schemas/EventCreatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/ai-recommendations: + post: + tags: + - Event Creation + summary: AI 추천 요청 (Step 2) + description: | + AI 서비스에 이벤트 추천 생성을 요청합니다. + Kafka Job을 발행하고 jobId를 반환합니다. + Job 상태는 /jobs/{jobId}로 폴링하여 확인합니다. + operationId: requestAiRecommendations + x-user-story: UFR-EVENT-030 + x-controller: EventController.requestAiRecommendations + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AiRecommendationRequest' + responses: + '202': + description: AI 추천 요청 접수 + content: + application/json: + schema: + $ref: '#/components/schemas/JobAcceptedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: DRAFT 상태의 이벤트만 AI 추천을 요청할 수 있습니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/recommendations: + put: + tags: + - Event Creation + summary: AI 추천 선택 및 커스터마이징 (Step 2-2) + description: | + AI가 생성한 추천 중 하나를 선택하고, + 필요시 이벤트명, 문구, 기간 등을 커스터마이징합니다. + operationId: selectRecommendation + x-user-story: UFR-EVENT-030 + x-controller: EventController.selectRecommendation + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectRecommendationRequest' + responses: + '200': + description: AI 추천 선택 및 커스터마이징 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: AI 추천이 완료된 DRAFT 상태의 이벤트만 선택 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/images: + post: + tags: + - Event Creation + summary: 이미지 생성 요청 (Step 3) + description: | + Content Service에 이미지 생성을 요청합니다. + Kafka Job을 발행하고 jobId를 반환합니다. + Job 상태는 /jobs/{jobId}로 폴링하여 확인합니다. + operationId: requestImageGeneration + x-user-story: UFR-CONT-010 + x-controller: EventController.requestImageGeneration + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageGenerationRequest' + responses: + '202': + description: 이미지 생성 요청 접수 + content: + application/json: + schema: + $ref: '#/components/schemas/JobAcceptedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: AI 추천이 선택된 DRAFT 상태의 이벤트만 이미지 생성이 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/images/{imageId}/select: + put: + tags: + - Event Creation + summary: 이미지 선택 (Step 3-2) + description: | + 생성된 이미지 중 하나를 선택합니다. + 선택된 이미지는 이벤트의 대표 이미지로 설정됩니다. + operationId: selectImage + x-user-story: UFR-CONT-010 + x-controller: EventController.selectImage + parameters: + - $ref: '#/components/parameters/EventId' + - name: imageId + in: path + description: 이미지 ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440006" + responses: + '200': + description: 이미지 선택 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 또는 이미지 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_IMAGE_STATE + message: 해당 이미지는 이 이벤트에 속하지 않습니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/images/{imageId}/edit: + put: + tags: + - Event Creation + summary: 이미지 편집 (Step 3-3) + description: | + 선택된 이미지를 편집합니다. + Content Service에 편집 요청을 보내고 새로운 이미지 URL을 받습니다. + operationId: editImage + x-user-story: UFR-CONT-020 + x-controller: EventController.editImage + parameters: + - $ref: '#/components/parameters/EventId' + - name: imageId + in: path + description: 이미지 ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440006" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageEditRequest' + responses: + '200': + description: 이미지 편집 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ImageEditResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이미지 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: IMAGE_NOT_EDITABLE + message: 선택된 이미지만 편집 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/channels: + put: + tags: + - Event Creation + summary: 배포 채널 선택 (Step 4) + description: | + 이벤트를 배포할 채널을 선택합니다. + (웹사이트, 카카오톡, Instagram, Facebook 등) + operationId: selectChannels + x-user-story: UFR-EVENT-040 + x-controller: EventController.selectChannels + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectChannelsRequest' + responses: + '200': + description: 배포 채널 선택 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: 이미지가 선택된 DRAFT 상태의 이벤트만 채널 선택이 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/publish: + post: + tags: + - Event Creation + summary: 최종 승인 및 배포 (Step 5) + description: | + 이벤트를 최종 승인하고 선택된 채널에 배포합니다. + Distribution Service를 동기 호출하여 배포하고, + 이벤트 상태를 PUBLISHED로 변경합니다. + Kafka Event (EventCreated)를 발행합니다. + operationId: publishEvent + x-user-story: UFR-EVENT-050 + x-controller: EventController.publishEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '200': + description: 이벤트 배포 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventPublishedResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_PUBLISHABLE + message: 배포 채널이 선택된 DRAFT 상태의 이벤트만 배포 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + '503': + description: Distribution Service 호출 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: DISTRIBUTION_SERVICE_UNAVAILABLE + message: 배포 서비스를 일시적으로 사용할 수 없습니다. + + /events/{eventId}/end: + post: + tags: + - Event Management + summary: 이벤트 조기 종료 + description: | + 진행 중인 이벤트를 조기 종료합니다. + PUBLISHED 상태의 이벤트만 종료 가능하며, + 종료 시 상태가 ENDED로 변경됩니다. + operationId: endEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.endEvent + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EndEventRequest' + responses: + '200': + description: 이벤트 종료 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태로 인해 종료 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_ENDABLE + message: PUBLISHED 상태의 이벤트만 종료 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /jobs/{jobId}: + get: + tags: + - Job Status + summary: Job 상태 폴링 + description: | + 비동기 작업(AI 추천 생성, 이미지 생성)의 상태를 조회합니다. + 클라이언트는 COMPLETED 또는 FAILED가 될 때까지 폴링합니다. + COMPLETED 시 Redis에서 결과를 조회할 수 있습니다. + operationId: getJobStatus + x-user-story: UFR-EVENT-030, UFR-CONT-010 + x-controller: JobController.getJobStatus + parameters: + - name: jobId + in: path + description: Job ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440005" + responses: + '200': + description: Job 상태 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer 토큰 인증 + + **형식:** Authorization: Bearer {JWT_TOKEN} + + **토큰 만료:** 7일 + + **Claims:** + - userId: 사용자 ID + - role: 사용자 역할 (OWNER) + - iat: 발급 시각 + - exp: 만료 시각 + + parameters: + EventId: + name: eventId + in: path + description: 이벤트 ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + + schemas: + EventListResponse: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/EventSummary' + page: + $ref: '#/components/schemas/PageInfo' + required: + - content + - page + + EventSummary: + type: object + properties: + eventId: + type: string + format: uuid + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + eventName: + type: string + description: 이벤트명 + example: "봄맞이 20% 할인 이벤트" + objective: + type: string + description: 이벤트 목적 + example: "신규 고객 유치" + status: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + description: 이벤트 상태 + example: "PUBLISHED" + startDate: + type: string + format: date + description: 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 종료일 + example: "2025-03-31" + thumbnailUrl: + type: string + format: uri + description: 썸네일 이미지 URL + example: "https://cdn.kt-event.com/images/event-thumb-001.jpg" + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-15T10:30:00Z" + required: + - eventId + - eventName + - objective + - status + - startDate + - endDate + - createdAt + + EventDetailResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + userId: + type: string + format: uuid + description: 사용자 ID + example: "550e8400-e29b-41d4-a716-446655440001" + storeId: + type: string + format: uuid + description: 매장 ID + example: "550e8400-e29b-41d4-a716-446655440002" + eventName: + type: string + description: 이벤트명 + example: "봄맞이 20% 할인 이벤트" + objective: + type: string + description: 이벤트 목적 + example: "신규 고객 유치" + description: + type: string + description: 이벤트 설명 + example: "봄을 맞이하여 모든 상품 20% 할인 행사를 진행합니다." + targetAudience: + type: string + description: 타겟 고객 + example: "20-30대 여성" + promotionType: + type: string + description: 프로모션 유형 + example: "할인" + discountRate: + type: integer + description: 할인율 (%) + example: 20 + startDate: + type: string + format: date + description: 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 종료일 + example: "2025-03-31" + status: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + description: 이벤트 상태 + example: "PUBLISHED" + selectedImageId: + type: string + format: uuid + description: 선택된 이미지 ID + example: "550e8400-e29b-41d4-a716-446655440006" + selectedImageUrl: + type: string + format: uri + description: 선택된 이미지 URL + example: "https://cdn.kt-event.com/images/event-img-001.jpg" + generatedImages: + type: array + description: 생성된 이미지 목록 + items: + $ref: '#/components/schemas/GeneratedImage' + channels: + type: array + description: 배포 채널 목록 + items: + type: string + example: "WEBSITE" + aiRecommendations: + type: array + description: AI 추천 목록 + items: + $ref: '#/components/schemas/AiRecommendation' + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-15T10:30:00Z" + updatedAt: + type: string + format: date-time + description: 수정일시 + example: "2025-02-20T14:45:00Z" + required: + - eventId + - userId + - storeId + - eventName + - objective + - status + - startDate + - endDate + - createdAt + + GeneratedImage: + type: object + properties: + imageId: + type: string + format: uuid + description: 이미지 ID + example: "550e8400-e29b-41d4-a716-446655440006" + imageUrl: + type: string + format: uri + description: 이미지 URL + example: "https://cdn.kt-event.com/images/event-img-001.jpg" + isSelected: + type: boolean + description: 선택 여부 + example: true + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-16T11:00:00Z" + required: + - imageId + - imageUrl + - isSelected + - createdAt + + AiRecommendation: + type: object + properties: + recommendationId: + type: string + format: uuid + description: 추천 ID + example: "550e8400-e29b-41d4-a716-446655440007" + eventName: + type: string + description: 추천 이벤트명 + example: "봄맞이 20% 할인 이벤트" + description: + type: string + description: 추천 설명 + example: "봄을 맞이하여 모든 상품 20% 할인 행사를 진행합니다." + promotionType: + type: string + description: 추천 프로모션 유형 + example: "할인" + targetAudience: + type: string + description: 추천 타겟 고객 + example: "20-30대 여성" + isSelected: + type: boolean + description: 선택 여부 + example: true + required: + - recommendationId + - eventName + - description + - isSelected + + SelectObjectiveRequest: + type: object + properties: + objective: + type: string + description: 이벤트 목적 + example: "신규 고객 유치" + required: + - objective + + EventCreatedResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: 생성된 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + status: + type: string + enum: [DRAFT] + description: 이벤트 상태 (항상 DRAFT) + example: "DRAFT" + objective: + type: string + description: 선택된 이벤트 목적 + example: "신규 고객 유치" + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-15T10:30:00Z" + required: + - eventId + - status + - objective + - createdAt + + AiRecommendationRequest: + type: object + properties: + storeInfo: + type: object + description: 매장 정보 (User Service에서 조회) + properties: + storeId: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440002" + storeName: + type: string + example: "우진네 고깃집" + category: + type: string + example: "음식점" + description: + type: string + example: "신선한 한우를 제공하는 고깃집" + required: + - storeId + - storeName + - category + required: + - storeInfo + + JobAcceptedResponse: + type: object + properties: + jobId: + type: string + format: uuid + description: 생성된 Job ID + example: "550e8400-e29b-41d4-a716-446655440005" + status: + type: string + enum: [PENDING] + description: Job 상태 (초기 상태는 PENDING) + example: "PENDING" + message: + type: string + description: 안내 메시지 + example: "AI 추천 생성 요청이 접수되었습니다. /jobs/{jobId}로 상태를 확인하세요." + required: + - jobId + - status + - message + + JobStatusResponse: + type: object + properties: + jobId: + type: string + format: uuid + description: Job ID + example: "550e8400-e29b-41d4-a716-446655440005" + jobType: + type: string + enum: [AI_RECOMMENDATION, IMAGE_GENERATION] + description: Job 유형 + example: "AI_RECOMMENDATION" + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: Job 상태 + example: "COMPLETED" + progress: + type: integer + minimum: 0 + maximum: 100 + description: 진행률 (%) + example: 100 + resultKey: + type: string + description: Redis 결과 키 (COMPLETED 시) + example: "ai:recommendation:550e8400-e29b-41d4-a716-446655440005" + errorMessage: + type: string + description: 에러 메시지 (FAILED 시) + example: "AI 서비스 연결 실패" + createdAt: + type: string + format: date-time + description: Job 생성일시 + example: "2025-02-15T10:31:00Z" + completedAt: + type: string + format: date-time + description: Job 완료일시 + example: "2025-02-15T10:31:30Z" + required: + - jobId + - jobType + - status + - progress + - createdAt + + SelectRecommendationRequest: + type: object + properties: + recommendationId: + type: string + format: uuid + description: 선택한 추천 ID + example: "550e8400-e29b-41d4-a716-446655440007" + customizations: + type: object + description: 커스터마이징 항목 + properties: + eventName: + type: string + description: 수정된 이벤트명 + example: "봄맞이 특별 할인 이벤트" + description: + type: string + description: 수정된 설명 + example: "봄을 맞이하여 전 메뉴 20% 할인" + startDate: + type: string + format: date + description: 수정된 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 수정된 종료일 + example: "2025-03-31" + discountRate: + type: integer + description: 수정된 할인율 + example: 20 + required: + - recommendationId + + ImageGenerationRequest: + type: object + properties: + eventInfo: + type: object + description: 이벤트 정보 (이미지 생성에 필요한 정보) + properties: + eventName: + type: string + example: "봄맞이 20% 할인 이벤트" + description: + type: string + example: "봄을 맞이하여 모든 상품 20% 할인 행사를 진행합니다." + promotionType: + type: string + example: "할인" + required: + - eventName + - description + imageCount: + type: integer + minimum: 1 + maximum: 5 + description: 생성할 이미지 개수 + default: 3 + example: 3 + required: + - eventInfo + + ImageEditRequest: + type: object + properties: + editType: + type: string + enum: [TEXT_OVERLAY, COLOR_ADJUST, CROP, FILTER] + description: 편집 유형 + example: "TEXT_OVERLAY" + parameters: + type: object + description: 편집 파라미터 (편집 유형에 따라 다름) + additionalProperties: true + example: + text: "20% 할인" + fontSize: 48 + color: "#FF0000" + position: "center" + required: + - editType + - parameters + + ImageEditResponse: + type: object + properties: + imageId: + type: string + format: uuid + description: 편집된 이미지 ID + example: "550e8400-e29b-41d4-a716-446655440008" + imageUrl: + type: string + format: uri + description: 편집된 이미지 URL + example: "https://cdn.kt-event.com/images/event-img-001-edited.jpg" + editedAt: + type: string + format: date-time + description: 편집일시 + example: "2025-02-16T15:20:00Z" + required: + - imageId + - imageUrl + - editedAt + + SelectChannelsRequest: + type: object + properties: + channels: + type: array + description: 배포 채널 목록 + items: + type: string + enum: [WEBSITE, KAKAO, INSTAGRAM, FACEBOOK, NAVER_BLOG] + example: ["WEBSITE", "KAKAO", "INSTAGRAM"] + minItems: 1 + required: + - channels + + EventPublishedResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + status: + type: string + enum: [PUBLISHED] + description: 이벤트 상태 (항상 PUBLISHED) + example: "PUBLISHED" + publishedAt: + type: string + format: date-time + description: 배포일시 + example: "2025-02-20T16:00:00Z" + channels: + type: array + description: 배포된 채널 목록 + items: + type: string + example: "WEBSITE" + distributionResults: + type: array + description: 채널별 배포 결과 + items: + $ref: '#/components/schemas/DistributionResult' + required: + - eventId + - status + - publishedAt + - channels + - distributionResults + + DistributionResult: + type: object + properties: + channel: + type: string + description: 채널명 + example: "WEBSITE" + success: + type: boolean + description: 배포 성공 여부 + example: true + url: + type: string + format: uri + description: 배포된 URL + example: "https://store.kt-event.com/event/550e8400-e29b-41d4-a716-446655440000" + message: + type: string + description: 배포 결과 메시지 + example: "웹사이트에 성공적으로 배포되었습니다." + required: + - channel + - success + + UpdateEventRequest: + type: object + properties: + eventName: + type: string + description: 이벤트명 + example: "봄맞이 특별 할인 이벤트" + description: + type: string + description: 이벤트 설명 + example: "봄을 맞이하여 전 메뉴 20% 할인" + startDate: + type: string + format: date + description: 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 종료일 + example: "2025-03-31" + discountRate: + type: integer + description: 할인율 + example: 20 + + EndEventRequest: + type: object + properties: + reason: + type: string + description: 종료 사유 + example: "목표 달성으로 조기 종료" + required: + - reason + + PageInfo: + type: object + properties: + page: + type: integer + description: 현재 페이지 번호 + example: 0 + size: + type: integer + description: 페이지 크기 + example: 20 + totalElements: + type: integer + description: 전체 요소 개수 + example: 45 + totalPages: + type: integer + description: 전체 페이지 개수 + example: 3 + required: + - page + - size + - totalElements + - totalPages + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 에러 코드 + example: "INVALID_REQUEST" + message: + type: string + description: 에러 메시지 + example: "요청 파라미터가 올바르지 않습니다." + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-02-15T10:30:00Z" + details: + type: array + description: 상세 에러 정보 (선택 사항) + items: + type: string + example: ["objective 필드는 필수입니다."] + + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_REQUEST + message: 요청 파라미터가 올바르지 않습니다. + details: + - "objective 필드는 필수입니다." + timestamp: "2025-02-15T10:30:00Z" + + Unauthorized: + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: UNAUTHORIZED + message: 인증에 실패했습니다. + timestamp: "2025-02-15T10:30:00Z" + + Forbidden: + description: 권한 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: FORBIDDEN + message: 해당 리소스에 접근할 권한이 없습니다. + timestamp: "2025-02-15T10:30:00Z" + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: NOT_FOUND + message: 요청한 리소스를 찾을 수 없습니다. + timestamp: "2025-02-15T10:30:00Z" + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INTERNAL_SERVER_ERROR + message: 서버 내부 오류가 발생했습니다. + timestamp: "2025-02-15T10:30:00Z" diff --git a/design/backend/api/participation-service-api.yaml b/design/backend/api/participation-service-api.yaml new file mode 100644 index 0000000..645da39 --- /dev/null +++ b/design/backend/api/participation-service-api.yaml @@ -0,0 +1,820 @@ +openapi: 3.0.3 +info: + title: Participation Service API + description: | + 이벤트 참여 및 당첨자 관리 서비스 API + - 이벤트 참여 등록 + - 참여자 목록 조회 및 관리 + - 당첨자 추첨 및 관리 + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8084 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/participation/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/participation/v1 + description: Production Server + +tags: + - name: participation + description: 이벤트 참여 관리 + - name: participant + description: 참여자 조회 및 관리 + - name: winner + description: 당첨자 추첨 및 관리 + +paths: + /events/{eventId}/participate: + post: + tags: + - participation + summary: 이벤트 참여 + description: | + 고객이 이벤트에 참여합니다. + - 중복 참여 검증 (전화번호 기반) + - 이벤트 진행 상태 검증 + - Kafka 이벤트 발행 (ParticipantRegistered) + operationId: participateEvent + x-user-story: UFR-PART-010 + x-controller: ParticipationController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipationRequest' + examples: + standard: + summary: 일반 참여 + value: + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + agreeMarketing: true + agreePrivacy: true + storeVisited: false + storeVisit: + summary: 매장 방문 참여 + value: + name: "김철수" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + agreeMarketing: false + agreePrivacy: true + storeVisited: true + responses: + '201': + description: 참여 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipationResponse' + examples: + success: + summary: 참여 성공 + value: + success: true + message: "이벤트 참여가 완료되었습니다" + data: + participantId: "prt_20250123_001" + eventId: "evt_20250123_001" + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidPhone: + summary: 유효하지 않은 전화번호 + value: + success: false + error: + code: "INVALID_PHONE_NUMBER" + message: "유효하지 않은 전화번호 형식입니다" + duplicateParticipation: + summary: 중복 참여 + value: + success: false + error: + code: "DUPLICATE_PARTICIPATION" + message: "이미 참여하신 이벤트입니다" + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 이벤트 없음 + value: + success: false + error: + code: "EVENT_NOT_FOUND" + message: "이벤트를 찾을 수 없습니다" + '409': + description: 이벤트 진행 불가 상태 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notActive: + summary: 진행중이 아닌 이벤트 + value: + success: false + error: + code: "EVENT_NOT_ACTIVE" + message: "현재 참여할 수 없는 이벤트입니다" + + /events/{eventId}/participants: + get: + tags: + - participant + summary: 참여자 목록 조회 + description: | + 이벤트의 참여자 목록을 조회합니다. + - 페이징 지원 + - 참여일시 기준 정렬 + - 매장 방문 여부 필터링 + operationId: getParticipants + x-user-story: UFR-PART-020 + x-controller: ParticipantController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + - name: page + in: query + description: 페이지 번호 (0부터 시작) + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + - name: size + in: query + description: 페이지 크기 + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + example: 20 + - name: storeVisited + in: query + description: 매장 방문 여부 필터 + schema: + type: boolean + example: true + responses: + '200': + description: 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantListResponse' + examples: + success: + summary: 참여자 목록 + value: + success: true + message: "참여자 목록을 조회했습니다" + data: + participants: + - participantId: "prt_20250123_001" + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + isWinner: false + - participantId: "prt_20250123_002" + name: "김철수" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + participatedAt: "2025-01-23T11:15:00Z" + storeVisited: true + bonusEntries: 2 + isWinner: true + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 156 + totalPages: 8 + hasNext: true + hasPrevious: false + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/participants/{participantId}: + get: + tags: + - participant + summary: 참여자 상세 조회 + description: 특정 참여자의 상세 정보를 조회합니다. + operationId: getParticipantDetail + x-user-story: UFR-PART-020 + x-controller: ParticipantController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + - name: participantId + in: path + required: true + description: 참여자 ID + schema: + type: string + example: "prt_20250123_001" + responses: + '200': + description: 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantDetailResponse' + examples: + success: + summary: 참여자 상세 정보 + value: + success: true + message: "참여자 정보를 조회했습니다" + data: + participantId: "prt_20250123_001" + eventId: "evt_20250123_001" + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + agreeMarketing: true + agreePrivacy: true + isWinner: false + winnerInfo: null + '404': + description: 참여자를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 참여자 없음 + value: + success: false + error: + code: "PARTICIPANT_NOT_FOUND" + message: "참여자를 찾을 수 없습니다" + + /events/{eventId}/draw-winners: + post: + tags: + - winner + summary: 당첨자 추첨 + description: | + 이벤트 당첨자를 추첨합니다. + - 랜덤 추첨 알고리즘 사용 + - 매장 방문 보너스 가중치 적용 + - 중복 당첨 방지 + operationId: drawWinners + x-user-story: UFR-PART-030 + x-controller: WinnerController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersRequest' + examples: + standard: + summary: 일반 추첨 + value: + winnerCount: 10 + applyStoreVisitBonus: true + responses: + '200': + description: 추첨 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersResponse' + examples: + success: + summary: 추첨 완료 + value: + success: true + message: "당첨자 추첨이 완료되었습니다" + data: + eventId: "evt_20250123_001" + totalParticipants: 156 + winnerCount: 10 + drawnAt: "2025-01-24T15:00:00Z" + winners: + - participantId: "prt_20250123_002" + name: "김철수" + phoneNumber: "010-9876-5432" + rank: 1 + - participantId: "prt_20250123_045" + name: "이영희" + phoneNumber: "010-5555-1234" + rank: 2 + - participantId: "prt_20250123_089" + name: "박민수" + phoneNumber: "010-7777-8888" + rank: 3 + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidCount: + summary: 잘못된 당첨자 수 + value: + success: false + error: + code: "INVALID_WINNER_COUNT" + message: "당첨자 수가 참여자 수보다 많습니다" + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: 이미 추첨 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + alreadyDrawn: + summary: 추첨 완료 상태 + value: + success: false + error: + code: "ALREADY_DRAWN" + message: "이미 당첨자 추첨이 완료되었습니다" + + /events/{eventId}/winners: + get: + tags: + - winner + summary: 당첨자 목록 조회 + description: | + 이벤트의 당첨자 목록을 조회합니다. + - 당첨 순위별 정렬 + - 페이징 지원 + operationId: getWinners + x-user-story: UFR-PART-030 + x-controller: WinnerController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + - name: page + in: query + description: 페이지 번호 (0부터 시작) + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + - name: size + in: query + description: 페이지 크기 + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + example: 20 + responses: + '200': + description: 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/WinnerListResponse' + examples: + success: + summary: 당첨자 목록 + value: + success: true + message: "당첨자 목록을 조회했습니다" + data: + eventId: "evt_20250123_001" + drawnAt: "2025-01-24T15:00:00Z" + totalWinners: 10 + winners: + - participantId: "prt_20250123_002" + name: "김철수" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + rank: 1 + wonAt: "2025-01-24T15:00:00Z" + - participantId: "prt_20250123_045" + name: "이영희" + phoneNumber: "010-5555-1234" + email: "lee@example.com" + rank: 2 + wonAt: "2025-01-24T15:00:00Z" + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 10 + totalPages: 1 + hasNext: false + hasPrevious: false + '404': + description: 이벤트를 찾을 수 없음 또는 당첨자가 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + noWinners: + summary: 당첨자 없음 + value: + success: false + error: + code: "NO_WINNERS_YET" + message: "아직 당첨자 추첨이 진행되지 않았습니다" + +components: + schemas: + ParticipationRequest: + type: object + required: + - name + - phoneNumber + - agreePrivacy + properties: + name: + type: string + description: 참여자 이름 + minLength: 2 + maxLength: 50 + example: "홍길동" + phoneNumber: + type: string + description: 참여자 전화번호 (하이픈 포함) + pattern: '^\d{3}-\d{3,4}-\d{4}$' + example: "010-1234-5678" + email: + type: string + format: email + description: 참여자 이메일 + example: "hong@example.com" + agreeMarketing: + type: boolean + description: 마케팅 정보 수신 동의 + default: false + example: true + agreePrivacy: + type: boolean + description: 개인정보 수집 및 이용 동의 (필수) + example: true + storeVisited: + type: boolean + description: 매장 방문 여부 + default: false + example: false + + ParticipationResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "이벤트 참여가 완료되었습니다" + data: + $ref: '#/components/schemas/ParticipantInfo' + + ParticipantInfo: + type: object + properties: + participantId: + type: string + description: 참여자 ID + example: "prt_20250123_001" + eventId: + type: string + description: 이벤트 ID + example: "evt_20250123_001" + name: + type: string + description: 참여자 이름 + example: "홍길동" + phoneNumber: + type: string + description: 참여자 전화번호 + example: "010-1234-5678" + email: + type: string + description: 참여자 이메일 + example: "hong@example.com" + participatedAt: + type: string + format: date-time + description: 참여 일시 + example: "2025-01-23T10:30:00Z" + storeVisited: + type: boolean + description: 매장 방문 여부 + example: false + bonusEntries: + type: integer + description: 보너스 응모권 수 (매장 방문 시 +1) + minimum: 1 + example: 1 + isWinner: + type: boolean + description: 당첨 여부 + example: false + + ParticipantDetailInfo: + allOf: + - $ref: '#/components/schemas/ParticipantInfo' + - type: object + properties: + agreeMarketing: + type: boolean + description: 마케팅 정보 수신 동의 + example: true + agreePrivacy: + type: boolean + description: 개인정보 수집 및 이용 동의 + example: true + winnerInfo: + $ref: '#/components/schemas/WinnerInfo' + nullable: true + + ParticipantListResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "참여자 목록을 조회했습니다" + data: + type: object + properties: + participants: + type: array + items: + $ref: '#/components/schemas/ParticipantInfo' + pagination: + $ref: '#/components/schemas/Pagination' + + ParticipantDetailResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "참여자 정보를 조회했습니다" + data: + $ref: '#/components/schemas/ParticipantDetailInfo' + + DrawWinnersRequest: + type: object + required: + - winnerCount + properties: + winnerCount: + type: integer + description: 당첨자 수 + minimum: 1 + example: 10 + applyStoreVisitBonus: + type: boolean + description: 매장 방문 보너스 적용 여부 + default: true + example: true + + DrawWinnersResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "당첨자 추첨이 완료되었습니다" + data: + type: object + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_20250123_001" + totalParticipants: + type: integer + description: 전체 참여자 수 + example: 156 + winnerCount: + type: integer + description: 당첨자 수 + example: 10 + drawnAt: + type: string + format: date-time + description: 추첨 일시 + example: "2025-01-24T15:00:00Z" + winners: + type: array + description: 당첨자 목록 + items: + $ref: '#/components/schemas/WinnerSummary' + + WinnerSummary: + type: object + properties: + participantId: + type: string + description: 참여자 ID + example: "prt_20250123_002" + name: + type: string + description: 당첨자 이름 + example: "김철수" + phoneNumber: + type: string + description: 당첨자 전화번호 + example: "010-9876-5432" + rank: + type: integer + description: 당첨 순위 + minimum: 1 + example: 1 + + WinnerInfo: + type: object + properties: + participantId: + type: string + description: 참여자 ID + example: "prt_20250123_002" + name: + type: string + description: 당첨자 이름 + example: "김철수" + phoneNumber: + type: string + description: 당첨자 전화번호 + example: "010-9876-5432" + email: + type: string + description: 당첨자 이메일 + example: "kim@example.com" + rank: + type: integer + description: 당첨 순위 + minimum: 1 + example: 1 + wonAt: + type: string + format: date-time + description: 당첨 일시 + example: "2025-01-24T15:00:00Z" + + WinnerListResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "당첨자 목록을 조회했습니다" + data: + type: object + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_20250123_001" + drawnAt: + type: string + format: date-time + description: 추첨 일시 + example: "2025-01-24T15:00:00Z" + totalWinners: + type: integer + description: 전체 당첨자 수 + example: 10 + winners: + type: array + items: + $ref: '#/components/schemas/WinnerInfo' + pagination: + $ref: '#/components/schemas/Pagination' + + Pagination: + type: object + properties: + currentPage: + type: integer + description: 현재 페이지 번호 (0부터 시작) + minimum: 0 + example: 0 + pageSize: + type: integer + description: 페이지 크기 + minimum: 1 + example: 20 + totalElements: + type: integer + description: 전체 요소 수 + minimum: 0 + example: 156 + totalPages: + type: integer + description: 전체 페이지 수 + minimum: 0 + example: 8 + hasNext: + type: boolean + description: 다음 페이지 존재 여부 + example: true + hasPrevious: + type: boolean + description: 이전 페이지 존재 여부 + example: false + + ErrorResponse: + type: object + properties: + success: + type: boolean + example: false + error: + type: object + properties: + code: + type: string + description: 에러 코드 + example: "DUPLICATE_PARTICIPATION" + message: + type: string + description: 에러 메시지 + example: "이미 참여하신 이벤트입니다" + details: + type: object + description: 추가 에러 상세 정보 + additionalProperties: true + nullable: true diff --git a/design/backend/api/user-service-api.yaml b/design/backend/api/user-service-api.yaml new file mode 100644 index 0000000..e1c486f --- /dev/null +++ b/design/backend/api/user-service-api.yaml @@ -0,0 +1,991 @@ +openapi: 3.0.3 +info: + title: User Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service API + + 사용자 인증 및 매장정보 관리를 담당하는 마이크로서비스 + + **주요 기능:** + - 회원가입 + - 로그인/로그아웃 + - 프로필 조회 및 수정 + - 비밀번호 변경 + + **보안:** + - JWT Bearer 토큰 기반 인증 + - bcrypt 비밀번호 해싱 + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8081 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/user/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/user/v1 + description: Production Server + +tags: + - name: Authentication + description: 인증 관련 API (로그인, 로그아웃, 회원가입) + - name: Profile + description: 프로필 관련 API (조회, 수정, 비밀번호 변경) + +paths: + /users/register: + post: + tags: + - Authentication + summary: 회원가입 + description: | + 소상공인 회원가입 API + + **유저스토리:** UFR-USER-010 + + **주요 기능:** + - 기본 정보 및 매장 정보 등록 + - 비밀번호 bcrypt 해싱 + - JWT 토큰 자동 발급 + + **처리 흐름:** + 1. 중복 사용자 확인 (전화번호 기반) + 2. 비밀번호 해싱 (bcrypt) + 3. User/Store 데이터베이스 트랜잭션 처리 + 4. JWT 토큰 생성 및 세션 저장 (Redis) + operationId: registerUser + x-user-story: UFR-USER-010 + x-controller: UserController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + examples: + restaurant: + summary: 음식점 회원가입 예시 + value: + name: 홍길동 + phoneNumber: "01012345678" + email: hong@example.com + password: "Password123!" + storeName: 맛있는집 + industry: 음식점 + address: 서울시 강남구 테헤란로 123 + businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00" + cafe: + summary: 카페 회원가입 예시 + value: + name: 김철수 + phoneNumber: "01087654321" + email: kim@example.com + password: "SecurePass456!" + storeName: 아메리카노 카페 + industry: 카페 + address: 서울시 서초구 서초대로 456 + businessHours: "매일 09:00-20:00" + responses: + '201': + description: 회원가입 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterResponse' + examples: + success: + summary: 회원가입 성공 응답 + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: 123 + userName: 홍길동 + storeId: 456 + storeName: 맛있는집 + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: 중복 사용자 + value: + code: USER_001 + message: 이미 가입된 전화번호입니다 + timestamp: 2025-10-22T10:30:00Z + validationError: + summary: 입력 검증 오류 + value: + code: VALIDATION_ERROR + message: 비밀번호는 8자 이상이어야 합니다 + timestamp: 2025-10-22T10:30:00Z + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /users/login: + post: + tags: + - Authentication + summary: 로그인 + description: | + 소상공인 로그인 API + + **유저스토리:** UFR-USER-020 + + **주요 기능:** + - 전화번호/비밀번호 인증 + - JWT 토큰 발급 + - Redis 세션 저장 + - 최종 로그인 시각 업데이트 (비동기) + + **보안:** + - Timing Attack 방어 (에러 메시지 통일) + - bcrypt 비밀번호 검증 + - JWT 토큰 7일 만료 + operationId: loginUser + x-user-story: UFR-USER-020 + x-controller: UserController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + examples: + default: + summary: 로그인 요청 예시 + value: + phoneNumber: "01012345678" + password: "Password123!" + responses: + '200': + description: 로그인 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + examples: + success: + summary: 로그인 성공 응답 + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: 123 + userName: 홍길동 + role: OWNER + email: hong@example.com + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authFailed: + summary: 인증 실패 + value: + code: AUTH_001 + message: 전화번호 또는 비밀번호를 확인해주세요 + timestamp: 2025-10-22T10:30:00Z + + /users/logout: + post: + tags: + - Authentication + summary: 로그아웃 + description: | + 로그아웃 API + + **유저스토리:** UFR-USER-040 + + **주요 기능:** + - Redis 세션 삭제 + - JWT 토큰 Blacklist 추가 + - 멱등성 보장 + + **처리 흐름:** + 1. JWT 토큰 검증 + 2. Redis 세션 삭제 + 3. JWT Blacklist 추가 (남은 만료 시간만큼 TTL 설정) + 4. 로그아웃 이벤트 발행 + operationId: logoutUser + x-user-story: UFR-USER-040 + x-controller: UserController + security: + - BearerAuth: [] + responses: + '200': + description: 로그아웃 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/LogoutResponse' + examples: + success: + summary: 로그아웃 성공 응답 + value: + success: true + message: 안전하게 로그아웃되었습니다 + '401': + description: 인증 실패 (유효하지 않은 토큰) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidToken: + summary: 유효하지 않은 토큰 + value: + code: AUTH_002 + message: 유효하지 않은 토큰입니다 + timestamp: 2025-10-22T10:30:00Z + + /users/profile: + get: + tags: + - Profile + summary: 프로필 조회 + description: | + 사용자 프로필 조회 API + + **유저스토리:** UFR-USER-030 + + **조회 정보:** + - 기본 정보 (이름, 전화번호, 이메일) + - 매장 정보 (매장명, 업종, 주소, 영업시간) + operationId: getProfile + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + responses: + '200': + description: 프로필 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileResponse' + examples: + success: + summary: 프로필 조회 성공 응답 + value: + userId: 123 + userName: 홍길동 + phoneNumber: "01012345678" + email: hong@example.com + role: OWNER + storeId: 456 + storeName: 맛있는집 + industry: 음식점 + address: 서울시 강남구 테헤란로 123 + businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00" + createdAt: 2025-09-01T10:00:00Z + lastLoginAt: 2025-10-22T09:00:00Z + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 사용자를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 사용자 없음 + value: + code: USER_003 + message: 사용자를 찾을 수 없습니다 + timestamp: 2025-10-22T10:30:00Z + + put: + tags: + - Profile + summary: 프로필 수정 + description: | + 사용자 프로필 수정 API + + **유저스토리:** UFR-USER-030 + + **수정 가능 항목:** + - 기본 정보: 이름, 전화번호, 이메일 + - 매장 정보: 매장명, 업종, 주소, 영업시간 + + **주의사항:** + - 비밀번호 변경은 별도 API 사용 (/users/password) + - 전화번호 변경 시 향후 재인증 필요 (현재는 직접 변경 가능) + - Optimistic Locking으로 동시성 제어 + operationId: updateProfile + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProfileRequest' + examples: + fullUpdate: + summary: 전체 정보 수정 + value: + name: 홍길동 + phoneNumber: "01012345678" + email: hong.new@example.com + storeName: 맛있는집 (리뉴얼) + industry: 퓨전음식점 + address: 서울시 강남구 테헤란로 456 + businessHours: "매일 11:00-23:00" + partialUpdate: + summary: 일부 정보 수정 (이메일, 영업시간) + value: + email: hong.updated@example.com + businessHours: "월-금 10:00-22:00, 토-일 휴무" + responses: + '200': + description: 프로필 수정 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProfileResponse' + examples: + success: + summary: 프로필 수정 성공 응답 + value: + userId: 123 + userName: 홍길동 + email: hong.new@example.com + storeId: 456 + storeName: 맛있는집 (리뉴얼) + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 사용자를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: 동시성 충돌 (다른 세션에서 수정) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + conflict: + summary: 동시성 충돌 + value: + code: USER_005 + message: 다른 세션에서 프로필을 수정했습니다. 새로고침 후 다시 시도하세요 + timestamp: 2025-10-22T10:30:00Z + + /users/password: + put: + tags: + - Profile + summary: 비밀번호 변경 + description: | + 비밀번호 변경 API + + **유저스토리:** UFR-USER-030 + + **주요 기능:** + - 현재 비밀번호 확인 필수 + - 새 비밀번호 규칙 검증 (8자 이상, 영문/숫자/특수문자 포함) + - bcrypt 해싱 + + **보안:** + - 현재 비밀번호 검증 실패 시 400 Bad Request + - 비밀번호 변경 후 기존 세션 유지 (로그아웃 불필요) + operationId: changePassword + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordRequest' + examples: + default: + summary: 비밀번호 변경 요청 + value: + currentPassword: "Password123!" + newPassword: "NewSecurePass456!" + responses: + '200': + description: 비밀번호 변경 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordResponse' + examples: + success: + summary: 비밀번호 변경 성공 응답 + value: + success: true + message: 비밀번호가 성공적으로 변경되었습니다 + '400': + description: 현재 비밀번호 불일치 또는 새 비밀번호 규칙 위반 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidCurrentPassword: + summary: 현재 비밀번호 불일치 + value: + code: USER_004 + message: 현재 비밀번호가 일치하지 않습니다 + timestamp: 2025-10-22T10:30:00Z + invalidNewPassword: + summary: 새 비밀번호 규칙 위반 + value: + code: VALIDATION_ERROR + message: 비밀번호는 8자 이상이어야 하며 영문/숫자/특수문자를 포함해야 합니다 + timestamp: 2025-10-22T10:30:00Z + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /users/{userId}/store: + get: + tags: + - Profile + summary: 매장정보 조회 (서비스 연동용) + description: | + 특정 사용자의 매장정보를 조회하는 API (내부 서비스 연동용) + + **사용 목적:** + - Event Service에서 이벤트 생성 시 매장정보 조회 + - Content Service에서 매장정보 기반 콘텐츠 생성 + - Service-to-Service 통신용 내부 API + + **주의사항:** + - Internal API로 외부 노출 금지 + - API Gateway에서 인증된 서비스만 접근 허용 + - 매장정보는 Redis 캐시 우선 조회 (TTL 30분) + operationId: getStoreByUserId + x-user-story: Service Integration + x-controller: UserController + security: + - BearerAuth: [] + parameters: + - name: userId + in: path + required: true + description: 사용자 ID + schema: + type: integer + format: int64 + example: 123 + responses: + '200': + description: 매장정보 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/StoreDetailResponse' + examples: + success: + summary: 매장정보 조회 성공 응답 + value: + userId: 123 + storeId: 456 + storeName: 맛있는집 + industry: 음식점 + address: 서울시 강남구 테헤란로 123 + businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00" + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + unauthorized: + summary: 인증 실패 + value: + code: AUTH_002 + message: 유효하지 않은 토큰입니다 + timestamp: 2025-10-22T10:30:00Z + '403': + description: 권한 없음 (내부 서비스만 접근 가능) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + forbidden: + summary: 권한 없음 + value: + code: AUTH_003 + message: 이 API는 내부 서비스만 접근 가능합니다 + timestamp: 2025-10-22T10:30:00Z + '404': + description: 사용자 또는 매장을 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 사용자 또는 매장 없음 + value: + code: USER_003 + message: 사용자 또는 매장을 찾을 수 없습니다 + timestamp: 2025-10-22T10:30:00Z + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer 토큰 인증 + + **형식:** Authorization: Bearer {JWT_TOKEN} + + **토큰 만료:** 7일 + + **Claims:** + - userId: 사용자 ID + - role: 사용자 역할 (OWNER) + - iat: 발급 시각 + - exp: 만료 시각 + + schemas: + RegisterRequest: + type: object + required: + - name + - phoneNumber + - email + - password + - storeName + - industry + - address + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: 사용자 이름 (2자 이상, 한글/영문) + example: 홍길동 + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 (010XXXXXXXX) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: 이메일 주소 + example: hong@example.com + password: + type: string + minLength: 8 + maxLength: 100 + description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함) + example: "Password123!" + storeName: + type: string + minLength: 2 + maxLength: 100 + description: 매장명 + example: 맛있는집 + industry: + type: string + maxLength: 50 + description: 업종 (예 음식점, 카페, 소매점 등) + example: 음식점 + address: + type: string + minLength: 5 + maxLength: 200 + description: 매장 주소 + example: 서울시 강남구 테헤란로 123 + businessHours: + type: string + maxLength: 200 + description: 영업시간 (선택 사항) + example: "월-금 11:00-22:00, 토-일 12:00-21:00" + + RegisterResponse: + type: object + required: + - token + - userId + - userName + - storeId + - storeName + properties: + token: + type: string + description: JWT 토큰 (7일 만료) + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 + + LoginRequest: + type: object + required: + - phoneNumber + - password + properties: + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 + example: "01012345678" + password: + type: string + minLength: 8 + description: 비밀번호 + example: "Password123!" + + LoginResponse: + type: object + required: + - token + - userId + - userName + - role + - email + properties: + token: + type: string + description: JWT 토큰 (7일 만료) + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + role: + type: string + enum: [OWNER, ADMIN] + description: 사용자 역할 + example: OWNER + email: + type: string + format: email + description: 이메일 주소 + example: hong@example.com + + LogoutResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: 로그아웃 성공 여부 + example: true + message: + type: string + description: 응답 메시지 + example: 안전하게 로그아웃되었습니다 + + ProfileResponse: + type: object + required: + - userId + - userName + - phoneNumber + - email + - role + - storeId + - storeName + - industry + - address + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + phoneNumber: + type: string + description: 휴대폰 번호 + example: "01012345678" + email: + type: string + format: email + description: 이메일 주소 + example: hong@example.com + role: + type: string + enum: [OWNER, ADMIN] + description: 사용자 역할 + example: OWNER + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 + industry: + type: string + description: 업종 + example: 음식점 + address: + type: string + description: 매장 주소 + example: 서울시 강남구 테헤란로 123 + businessHours: + type: string + description: 영업시간 + example: "월-금 11:00-22:00, 토-일 12:00-21:00" + createdAt: + type: string + format: date-time + description: 가입 일시 + example: 2025-09-01T10:00:00Z + lastLoginAt: + type: string + format: date-time + description: 최종 로그인 일시 + example: 2025-10-22T09:00:00Z + + UpdateProfileRequest: + type: object + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: 사용자 이름 (선택 사항) + example: 홍길동 + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 (선택 사항, 향후 재인증 필요) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: 이메일 주소 (선택 사항) + example: hong.new@example.com + storeName: + type: string + minLength: 2 + maxLength: 100 + description: 매장명 (선택 사항) + example: 맛있는집 (리뉴얼) + industry: + type: string + maxLength: 50 + description: 업종 (선택 사항) + example: 퓨전음식점 + address: + type: string + minLength: 5 + maxLength: 200 + description: 매장 주소 (선택 사항) + example: 서울시 강남구 테헤란로 456 + businessHours: + type: string + maxLength: 200 + description: 영업시간 (선택 사항) + example: "매일 11:00-23:00" + + UpdateProfileResponse: + type: object + required: + - userId + - userName + - email + - storeId + - storeName + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + email: + type: string + format: email + description: 이메일 주소 + example: hong.new@example.com + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 (리뉴얼) + + ChangePasswordRequest: + type: object + required: + - currentPassword + - newPassword + properties: + currentPassword: + type: string + minLength: 8 + description: 현재 비밀번호 + example: "Password123!" + newPassword: + type: string + minLength: 8 + maxLength: 100 + description: 새 비밀번호 (8자 이상, 영문/숫자/특수문자 포함) + example: "NewSecurePass456!" + + ChangePasswordResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: 비밀번호 변경 성공 여부 + example: true + message: + type: string + description: 응답 메시지 + example: 비밀번호가 성공적으로 변경되었습니다 + + StoreDetailResponse: + type: object + required: + - userId + - storeId + - storeName + - industry + - address + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 + industry: + type: string + description: 업종 + example: 음식점 + address: + type: string + description: 매장 주소 + example: 서울시 강남구 테헤란로 123 + businessHours: + type: string + description: 영업시간 + example: "월-금 11:00-22:00, 토-일 12:00-21:00" + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 에러 코드 + example: USER_001 + enum: + - USER_001 # 중복 사용자 + - USER_003 # 사용자 없음 + - USER_004 # 현재 비밀번호 불일치 + - USER_005 # 동시성 충돌 + - AUTH_001 # 인증 실패 + - AUTH_002 # 유효하지 않은 토큰 + - AUTH_003 # 권한 없음 (내부 서비스만 접근) + - VALIDATION_ERROR # 입력 검증 오류 + message: + type: string + description: 에러 메시지 + example: 이미 가입된 전화번호입니다 + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: 2025-10-22T10:30:00Z + details: + type: array + description: 상세 에러 정보 (선택 사항) + items: + type: string + example: ["필드명: 필수 항목입니다"] diff --git a/design/backend/logical/logical-architecture.md b/design/backend/logical/logical-architecture.md new file mode 100644 index 0000000..949ef44 --- /dev/null +++ b/design/backend/logical/logical-architecture.md @@ -0,0 +1,869 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 논리 아키텍처 + +## 문서 정보 +- **작성일**: 2025-10-21 +- **최종 수정일**: 2025-10-22 +- **버전**: 2.0 (CQRS + Event-Driven 전환) +- **작성자**: System Architect +- **관련 문서**: + - [유저스토리](../../userstory.md) + - [아키텍처 패턴](../../pattern/architecture-pattern.md) + - [UI/UX 설계서](../../uiux/uiux.md) + +## 버전 이력 +- **v1.0** (2025-10-21): 초기 마이크로서비스 아키텍처 설계 +- **v2.0** (2025-10-22): CQRS 패턴 및 Event-Driven 아키텍처 전환, Resilience 패턴 전면 적용 +- **v2.1** (2025-10-22): 서비스 구조 간소화, Kafka 통합 (Event Bus + Job Queue), Distribution 비동기 처리 +- **v2.2** (2025-10-22): Distribution Service 동기 호출 전환 (REST API 직접 호출) + +--- + +## 목차 +1. [개요](#1-개요) +2. [서비스 아키텍처](#2-서비스-아키텍처) +3. [주요 사용자 플로우](#3-주요-사용자-플로우) +4. [데이터 흐름 및 캐싱 전략](#4-데이터-흐름-및-캐싱-전략) +5. [확장성 및 성능 고려사항](#5-확장성-및-성능-고려사항) +6. [보안 고려사항](#6-보안-고려사항) +7. [논리 아키텍처 다이어그램](#7-논리-아키텍처-다이어그램) + +--- + +## 1. 개요 + +### 1.1 설계 원칙 + +본 논리 아키텍처는 다음 원칙을 기반으로 설계되었습니다: + +#### 유저스토리 기반 설계 +- 20개 유저스토리와 정확히 매칭 +- 불필요한 추가 기능 배제 +- 비즈니스 요구사항 우선 반영 + +#### Event-Driven 아키텍처 +- **Kafka 기반 통합**: Event Bus와 Job Queue를 Kafka로 통합 +- **비동기 메시징**: Kafka Topics를 통한 서비스 간 통신 +- **느슨한 결합**: 서비스 간 직접 의존성 제거 +- **확장성**: 이벤트 구독자 추가로 기능 확장 용이 +- **장애 격리**: 이벤트 발행/구독 실패 시 서비스 독립성 유지 + +#### Kafka 통합 전략 +- **Event Topics**: 도메인 이벤트 발행/구독 (EventCreated, ParticipantRegistered 등) +- **Job Topics**: 비동기 작업 요청/처리 (ai 이벤트 생성, 이미지 생성) +- **단일 메시징 플랫폼**: 운영 복잡도 감소 및 일관된 메시지 처리 + +#### Resilience 패턴 적용 +- **Circuit Breaker**: 외부 API 장애 시 빠른 실패 및 복구 (Hystrix/Resilience4j) +- **Retry Pattern**: 일시적 장애 시 자동 재시도 (지수 백오프) +- **Timeout Pattern**: 응답 시간 제한으로 리소스 점유 방지 +- **Bulkhead Pattern**: 리소스 격리로 장애 전파 차단 +- **Fallback Pattern**: 장애 시 대체 로직 실행 (캐시 응답 등) + +### 1.2 핵심 컴포넌트 정의 + +#### Core Services +1. **User Service**: 사용자 인증 및 매장정보 관리 + - 회원가입/로그인 (JWT 발급) + - 프로필 CRUD + - Event Service로 회원정보 제공 + +2. **Event Service**: 이벤트 전체 생명주기 관리 + - 이벤트 생성/수정/삭제/조회 + - 이벤트 생성 플로우 오케스트레이션 + - Kafka Job 발행 (AI, 이미지) + - Distribution Service 동기 호출 (배포) + - Kafka Event 발행 (EventCreated) + +3. **Participation Service**: 참여 및 당첨자 관리 + - 참여 접수 및 중복 체크 + - 참여자 목록 조회 + - 당첨자 추첨 및 조회 + - Kafka Event 발행 (ParticipantRegistered) + +4. **Analytics Service**: 실시간 성과 분석 및 대시보드 + - 대시보드 데이터 조회 (Redis 캐싱) + - Kafka Event 구독 (EventCreated, ParticipantRegistered, DistributionCompleted) + - 외부 채널 통계 수집 (Circuit Breaker + Fallback) + - ROI 계산 및 성과 분석 + +#### Async Services (비동기 처리) +1. **AI Service**: AI 기반 이벤트 추천 + - Kafka Job 구독 (ai 이벤트 생성) + - 외부 AI API 호출 (Circuit Breaker, Timeout 5분) + - 결과 Redis 저장 (TTL 24시간) + +2. **Content Service**: SNS 이미지 생성 + - Redis에서 AI 데이터 읽기 + - 외부 이미지 생성 API 호출 (Circuit Breaker, Timeout 5분) + - 생성된 이미지 Redis 저장 (CDN URL, TTL 7일) + +3. **Distribution Service**: 다중 채널 배포 (동기) + - REST API 제공 (Event Service에서 호출) + - 병렬 배포 (Circuit Breaker, Retry, Bulkhead) + - Kafka Event 발행 (DistributionCompleted) + +#### Kafka (통합 메시징 플랫폼) +**Event Topics** (도메인 이벤트): +- **EventCreated**: 이벤트 생성 시 +- **ParticipantRegistered**: 참여자 등록 시 +- **DistributionCompleted**: 배포 완료 시 + +**Job Topics** (비동기 작업): +- **ai 이벤트 생성**: AI 추천 작업 +- **이미지 생성**: 이미지 생성 작업 + +**특징**: +- At-Least-Once Delivery 보장 +- Partition Key 기반 순서 보장 +- Dead Letter Queue 지원 + +#### Data Layer +- **Redis Cache**: 세션, AI 결과, 이미지 URL, 대시보드 캐싱 +- **PostgreSQL**: 서비스별 독립 데이터베이스 + - User DB, Event DB, Participation DB, Analytics DB + +#### External Systems +- **AI APIs**: Claude/GPT-4 (트렌드 분석) +- **이미지 생성 APIs**: Stable Diffusion/DALL-E +- **배포 채널 APIs**: 우리동네TV, 링고비즈, 지니TV, SNS APIs (비동기 배포) + +--- + +## 2. 서비스 아키텍처 + +### 2.1 서비스별 책임 + +#### User Service +**핵심 책임**: +- 회원가입/로그인 (JWT 토큰 발급) +- 프로필 CRUD (매장 정보 포함) +- 세션 관리 +- Event Service로 회원정보 제공 + +**관련 유저스토리**: UFR-USER-010, 020, 030, 040 + +**서비스 간 호출**: +- **Event Service**: 회원정보 조회 API 제공 (매장 정보 포함) + +**데이터 저장**: +- User DB: users, stores 테이블 +- Redis: 세션 정보 (TTL 7일) + +#### Event Service +**핵심 책임**: +- 이벤트 생성/수정/삭제/조회 +- 이벤트 생성 플로우 오케스트레이션 +- Kafka Job 발행 (AI, 이미지, 배포) +- Kafka Event 발행 (EventCreated) + +**관련 유저스토리**: UFR-EVENT-010, 020, 030, 040, 050, 060, 070 + +**Kafka 이벤트 발행**: +1. **EventCreated**: 이벤트 생성 완료 시 + - Payload: eventId, storeId, title, objective, createdAt + - 구독자: Analytics Service + +**Kafka Job 발행**: +1. **ai 이벤트 생성**: AI 추천 요청 +2. **이미지 생성**: 이미지 생성 요청 + +**서비스 간 호출**: +- **Distribution Service**: 다중 채널 배포 (동기 호출, Circuit Breaker 적용) + +**주요 플로우**: +1. 이벤트 목적 선택 → Event DB 저장 → EventCreated 발행 +2. AI 추천 요청 → ai 이벤트 생성 발행 +3. 이미지 생성 요청 → 이미지 생성 발행 +4. 배포 승인 → Distribution Service 동기 호출 + +**데이터 저장**: +- Event DB: events, event_objectives, event_prizes 테이블 + +#### Participation Service +**핵심 책임**: +- 이벤트 참여 접수 및 검증 +- 참여자 목록 조회 +- 당첨자 추첨 및 조회 +- Kafka Event 발행 (ParticipantRegistered) + +**관련 유저스토리**: UFR-PART-010, 020, 030 + +**Kafka 이벤트 발행**: +1. **ParticipantRegistered**: 참여자 등록 시 + - Payload: participantId, eventId, phoneNumber, registeredAt + - 구독자: Analytics Service + +**주요 기능**: +- 중복 참여 체크 (전화번호 기반) +- 참여자 목록 조회 (페이지네이션 지원) +- 난수 기반 무작위 추첨 +- 매장 방문 고객 가산점 적용 +- 당첨자 조회 + +**데이터 저장**: +- Participation DB: participants, winners 테이블 + +#### Analytics Service +**핵심 책임**: +- 실시간 성과 대시보드 조회 +- 채널별 성과 분석 및 통계 +- ROI 계산 및 성과 집계 + +**관련 유저스토리**: UFR-ANAL-010 + +**Kafka 이벤트 구독**: +- **EventCreated**: 이벤트 기본 정보 초기화 +- **ParticipantRegistered**: 참여자 수 실시간 증가 +- **DistributionCompleted**: 배포 완료 통계 업데이트 + +**Resilience 패턴**: +- **Circuit Breaker**: 외부 채널 API 조회 시 (실패율 50% 초과 시 Open) +- **Fallback**: 캐시된 이전 데이터 반환 +- **Cache-Aside**: Redis 캐싱 (TTL 5분) + +**데이터 통합**: +- Event Service: 이벤트 정보 조회 (DB 직접 또는 REST) +- Participation Service: 참여자/당첨자 데이터 조회 +- 외부 APIs: 우리동네TV, 지니TV, SNS 통계 수집 + +**데이터 저장**: +- Analytics DB: event_stats, channel_stats +- Redis: 대시보드 데이터 (TTL 5분) + +### 2.2 Async Services (비동기 처리) + +#### AI Service +**핵심 책임**: +- 업종/지역/시즌 트렌드 분석 +- 3가지 이벤트 기획안 자동 생성 +- 예상 성과 계산 + +**관련 유저스토리**: UFR-AI-010 + +**Kafka Job 구독**: +- **ai 이벤트 생성**: AI 추천 작업 요청 + +**Resilience 패턴**: +- **Circuit Breaker**: AI API 호출 시 (실패율 50% 초과 시 Open) +- **Timeout**: 5분 (300초) +- **Fallback**: 캐시된 이전 추천 결과 + 안내 메시지 +- **Cache-Aside**: Redis 캐싱 (TTL 24시간) + +**처리 시간**: +- 캐시 HIT: 0.1초 +- 캐시 MISS: 5분 이내 (비동기 처리) + +**데이터 저장**: +- Redis: AI 추천 결과 (TTL 24시간) +- Redis: Job 상태 정보 (TTL 1시간) + +#### Content Service +**핵심 책임**: +- 3가지 스타일 SNS 이미지 자동 생성 +- 플랫폼별 이미지 최적화 +- 이미지 편집 기능 + +**관련 유저스토리**: UFR-CONT-010, 020 + +**데이터 읽기**: +- Redis에서 AI Service가 저장한 이벤트 데이터 읽기 + +**Resilience 패턴**: +- **Circuit Breaker**: 이미지 생성 API 호출 시 (실패율 50% 초과 시 Open) +- **Timeout**: 5분 (300초) +- **Fallback**: 기본 템플릿 이미지 제공 +- **Cache-Aside**: Redis 캐싱 (TTL 7일) + +**처리 시간**: +- 캐시 HIT: 0.1초 +- 캐시 MISS: 5분 이내 (비동기 처리) + +**데이터 저장**: +- Redis: 이미지 생성 결과 (CDN URL, TTL 7일) +- CDN: 생성된 이미지 파일 + +#### Distribution Service +**핵심 책임**: +- 다중 채널 병렬 배포 (동기) +- 배포 상태 모니터링 +- Kafka Event 발행 (DistributionCompleted) + +**관련 유저스토리**: UFR-DIST-010, 020 + +**주요 API**: +- `POST /api/distribution/distribute`: 다중 채널 배포 요청 (Event Service에서 호출) + +**Kafka 이벤트 발행**: +- **DistributionCompleted**: 배포 완료 시 + - Payload: eventId, distributedChannels, completedAt + - 구독자: Analytics Service + +**Resilience 패턴**: +- **Circuit Breaker**: 각 외부 채널 API별 독립 적용 (실패율 50% 초과 시 Open) +- **Retry**: 최대 3회 재시도 (지수 백오프: 1초, 2초, 4초) +- **Bulkhead**: 채널별 스레드 풀 격리 (장애 전파 방지) +- **Fallback**: 실패 채널 스킵 + 알림 + +**처리 시간**: 1분 이내 (모든 채널 배포 완료) + +**배포 채널**: +- 우리동네TV API (영상 업로드) +- 링고비즈 API (연결음 업데이트) +- 지니TV API (TV 광고 등록) +- SNS APIs (Instagram, Naver, Kakao 자동 포스팅) + +**데이터 저장**: +- Event DB: distribution_logs 테이블 + +### 2.3 Kafka 통신 전략 + +#### Kafka 아키텍처 +**기술 스택**: Apache Kafka (Event Topics + Job Topics 통합) +**보장 수준**: At-Least-Once Delivery +**메시지 포맷**: JSON + +#### Event Topics (도메인 이벤트) + +| 토픽명 | 발행자 | 구독자 | Payload | 용도 | +|---------|--------|--------|---------|------| +| **EventCreated** | Event Service | Analytics Service | eventId, storeId, title, objective, createdAt | 이벤트 생성 시 통계 초기화 | +| **ParticipantRegistered** | Participation Service | Analytics Service | participantId, eventId, phoneNumber, registeredAt | 참여자 등록 시 실시간 통계 업데이트 | +| **DistributionCompleted** | Distribution Service | Analytics Service | eventId, distributedChannels, completedAt | 배포 완료 시 통계 업데이트 | + +#### Job Topics (비동기 작업) + +| 토픽명 | 발행자 | 구독자 | Payload | 용도 | +|---------|--------|--------|---------|------| +| **ai 이벤트 생성** | Event Service | AI Service | eventId, objective, industry, region | AI 트렌드 분석 및 이벤트 추천 요청 | +| **이미지 생성** | Event Service | Content Service | eventId, content, style | SNS 이미지 생성 요청 (3가지 스타일) | + +#### 통신 패턴별 설계 + +**1. Event Topics (도메인 이벤트)** +- **사용 시나리오**: 서비스 간 상태 변경 알림 및 동기화 +- **통신 방식**: Kafka Pub/Sub +- **장점**: + - 서비스 독립성 보장 + - 장애 격리 + - 확장 용이 +- **단점**: + - 최종 일관성 (Eventual Consistency) + - 디버깅 복잡도 증가 + +**2. Job Topics (비동기 작업)** +- **사용 시나리오**: 장시간 작업 (AI 추천, 이미지 생성) +- **통신 방식**: Kafka 메시지 큐 +- **패턴**: Asynchronous Request-Reply +- **처리 플로우**: + 1. Event Service → Kafka Job Topic: Job 발행 + 2. Async Service → Kafka: Job 수신 및 처리 + 3. Client → Event Service: Job 상태 폴링 (5초 간격) + 4. Async Service → Redis: 결과 캐싱 + 5. Event Service → Client: 완료 응답 + +**3. 서비스 간 동기 호출** +- **사용 시나리오**: 다중 채널 배포 (Distribution Service) +- **통신 방식**: REST API (HTTP/JSON) +- **패턴**: Synchronous Request-Reply +- **처리 플로우**: + 1. Event Service → Distribution Service: POST /api/distribution/distribute + 2. Distribution Service: 다중 채널 병렬 배포 (1분 이내) + 3. Distribution Service → Event Service: 배포 완료 응답 + 4. Distribution Service → Kafka: DistributionCompleted 이벤트 발행 +- **Resilience**: Circuit Breaker 적용 (실패율 50% 초과 시 Open) + +**4. 데이터베이스 직접 조회** +- **사용 시나리오**: Analytics Service가 이벤트/참여 데이터 필요 시 +- **패턴**: Database-per-Service 원칙 유지, 필요 시 이벤트로 데이터 동기화 +- **통신 방식**: Kafka 이벤트 구독 → Analytics DB 저장 → 로컬 조회 +- **특징**: 서비스 간 직접 API 호출 최소화 + +#### Cache-Aside 전략 + +| 서비스 | 캐시 키 패턴 | TTL | 히트율 목표 | 효과 | +|--------|-------------|-----|-----------|------| +| AI Service | `ai:recommendation:{업종}:{지역}:{목적}` | 24시간 | 80% | 10초 → 0.1초 (99% 개선) | +| Content Service | `content:image:{이벤트ID}:{스타일}` | 7일 | 80% | 5초 → 0.1초 (98% 개선) | +| User Service | `user:business:{사업자번호}` | 7일 | 90% | - | +| Analytics Query | `analytics:dashboard:{이벤트ID}` | 5분 | 95% | 3초 → 0.5초 (83% 개선) | + +#### Resilience 패턴 적용 + +**1. Circuit Breaker 패턴** +- **적용 대상**: 모든 외부 API 호출 +- **라이브러리**: Resilience4j 또는 Hystrix +- **설정**: + ```yaml + circuit-breaker: + failure-rate-threshold: 50% # 실패율 50% 초과 시 Open + slow-call-rate-threshold: 50% # 느린 호출 50% 초과 시 Open + slow-call-duration-threshold: 5s # 5초 초과 시 느린 호출로 간주 + wait-duration-in-open-state: 30s # Open 상태 30초 유지 후 Half-Open + permitted-calls-in-half-open: 3 # Half-Open 상태에서 3개 요청 테스트 + ``` + +**2. Retry 패턴** +- **적용 대상**: 일시적 장애가 예상되는 외부 API +- **재시도 전략**: 지수 백오프 (Exponential Backoff) +- **설정**: + ```yaml + retry: + max-attempts: 3 # 최대 3회 재시도 + wait-duration: 1s # 초기 대기 시간 1초 + exponential-backoff-multiplier: 2 # 2배씩 증가 (1초, 2초, 4초) + retry-exceptions: + - java.net.SocketTimeoutException + - java.net.ConnectException + ``` + +**3. Timeout 패턴** +- **적용 대상**: 모든 외부 API 호출 +- **설정**: + | 서비스 | Timeout | 이유 | + |--------|---------|------| + | AI Service (AI API) | 5분 (300초) | 복잡한 분석 작업 | + | Content Service (이미지 API) | 5분 (300초) | 이미지 생성 시간 고려 | + | Distribution Service (채널 APIs) | 10초 | 빠른 배포 필요 | + +**4. Bulkhead 패턴** +- **적용 대상**: Distribution Service (다중 채널 배포) +- **목적**: 채널별 리소스 격리로 장애 전파 차단 +- **설정**: + ```yaml + bulkhead: + max-concurrent-calls: 10 # 채널당 최대 10개 동시 호출 + max-wait-duration: 0s # 대기 없이 즉시 실패 + ``` + +**5. Fallback 패턴** +- **적용 대상**: 모든 외부 API 호출 +- **전략**: + | 서비스 | Fallback 전략 | + |--------|---------------| + | AI Service | 캐시된 이전 추천 결과 + 안내 메시지 | + | Content Service | 기본 템플릿 이미지 제공 | + | Distribution Service | 실패 채널 스킵 + 알림 | + | Analytics Service | 캐시된 이전 데이터 반환 | + +#### 이벤트 순서 보장 +- **Kafka Partition Key**: eventId 기준으로 파티션 할당 +- **동일 이벤트의 모든 이벤트**: 동일 파티션 → 순서 보장 +- **다른 이벤트**: 독립적 처리 → 병렬 처리 가능 + +#### 이벤트 재처리 (At-Least-Once) +- **멱등성 보장**: 구독자는 동일 이벤트 중복 처리 시 멱등성 유지 +- **방법**: 이벤트 ID 기반 중복 체크 (Redis Set 사용) + +--- + +## 3. 주요 사용자 플로우 + +### 3.1 이벤트 생성 플로우 (Event-Driven + Kafka) + +``` +1. [이벤트 목적 선택] + ┌─────────────────────────────────────────────────────────────┐ + │ Client → Event Service │ + │ - POST /api/events (목적, 매장 정보) │ + │ - Event DB에 저장 │ + │ - EventCreated 이벤트 발행 → Kafka │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Kafka → Analytics Service │ + │ - EventCreated 이벤트 구독 │ + │ - Analytics DB에 기본 통계 초기화 │ + └─────────────────────────────────────────────────────────────┘ + +2. [AI 이벤트 추천] + ┌─────────────────────────────────────────────────────────────┐ + │ Client → Event Service │ + │ - POST /api/events/{id}/ai-recommendations │ + │ - Kafka ai 이벤트 생성 토픽 발행 (AI 작업 요청) │ + │ - Job ID 즉시 반환 (0.1초) │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ AI Service (Background) │ + │ - Kafka ai 이벤트 생성 토픽 구독 │ + │ - Redis 캐시 확인 (Cache-Aside) │ + │ - 캐시 MISS: Claude API 호출 (5분) [Circuit Breaker] │ + │ - AI 추천 결과를 Redis에 저장 (TTL 24시간) │ + │ - Job 상태 완료로 업데이트 │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Client (Polling) │ + │ - GET /api/jobs/{id} (5초 간격) │ + │ - 완료 시: AI 추천 결과 반환 (3가지 옵션) │ + └─────────────────────────────────────────────────────────────┘ + +3. [SNS 이미지 생성] + ┌─────────────────────────────────────────────────────────────┐ + │ Content Service (Background) │ + │ - Redis에서 AI Service가 저장한 이벤트 데이터 읽기 │ + │ - Redis 캐시 확인 (이미지 생성 여부) │ + │ - 캐시 MISS: Stable Diffusion API (5분) [Circuit Breaker] │ + │ - 이미지 CDN 업로드 │ + │ - 생성된 이미지 URL을 Redis에 저장 (TTL 7일) │ + │ - Job 상태 완료로 업데이트 │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Client (Polling) │ + │ - GET /api/jobs/{id} (3초 간격) │ + │ - 완료 시: 3가지 스타일 이미지 URL 반환 │ + └─────────────────────────────────────────────────────────────┘ + +4. [최종 승인 및 배포] + ┌─────────────────────────────────────────────────────────────┐ + │ Client → Event Service │ + │ - POST /api/events/{id}/publish │ + │ - Redis의 이벤트 관련 정보(AI 추천, 이미지 URL)를 조회 │ + │ - Event DB에 이벤트 정보 저장 │ + │ - Event 상태 변경 (DRAFT → PUBLISHED) │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Event Service → Distribution Service │ + │ - POST /api/distribution/distribute (동기 호출) │ + │ - 다중 채널 병렬 배포 [Circuit Breaker + Bulkhead] │ + │ * 우리동네TV API (영상 업로드) [Retry: 3회] │ + │ * 링고비즈 API (연결음 업데이트) [Retry: 3회] │ + │ * 지니TV API (광고 등록) [Retry: 3회] │ + │ * SNS APIs (Instagram, Naver, Kakao) [Retry: 3회] │ + │ - 배포 완료: DistributionCompleted 이벤트 발행 → Kafka │ + │ - Event Service로 배포 완료 응답 (1분 이내) │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Kafka → Analytics Service │ + │ - DistributionCompleted 이벤트 구독 │ + │ - Analytics DB 배포 통계 업데이트 │ + │ - 대시보드 캐시 무효화 (다음 조회 시 갱신) │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Event Service → Client │ + │ - 배포 완료 응답 반환 │ + └─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 고객 참여 플로우 (Event-Driven) + +``` +1. [이벤트 참여] + ┌─────────────────────────────────────────────────────────────┐ + │ Client → Participation Service │ + │ - POST /api/events/{id}/participate │ + │ - 중복 참여 체크 (전화번호 기반) │ + │ - Participation DB에 저장 │ + │ - ParticipantRegistered 이벤트 발행 → Kafka │ + │ - 응모 번호 즉시 반환 │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Kafka → Analytics Service │ + │ - ParticipantRegistered 이벤트 구독 │ + │ - 실시간 참여자 수 증가 │ + │ - Analytics DB에 참여 통계 업데이트 │ + │ - 대시보드 캐시 무효화 │ + └─────────────────────────────────────────────────────────────┘ + +2. [당첨자 추첨] + ┌─────────────────────────────────────────────────────────────┐ + │ Client → Participation Service │ + │ - POST /api/events/{id}/draw-winners │ + │ - 난수 기반 무작위 추첨 │ + │ - Winners DB에 저장 │ + └─────────────────────────────────────────────────────────────┘ +``` + +### 3.3 성과 분석 플로우 (Event-Driven) + +``` +1. [실시간 대시보드 조회] + ┌─────────────────────────────────────────────────────────────┐ + │ Client → Analytics Service │ + │ - GET /api/events/{id}/analytics │ + │ - Redis 캐시 확인 (TTL 5분) │ + │ * 캐시 HIT: 즉시 반환 (0.5초) │ + │ * 캐시 MISS: 아래 데이터 통합 │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ Analytics Service (데이터 통합) │ + │ - Analytics DB: 이벤트/참여 통계 조회 (로컬 DB) │ + │ - 외부 APIs: 채널별 노출/클릭 수 [Circuit Breaker + Fallback] │ + │ * 우리동네TV API (조회수) │ + │ * 지니TV API (광고 노출 수) │ + │ * SNS APIs (좋아요, 댓글, 공유 수) │ + │ - Redis 캐싱 (TTL 5분) │ + │ - 대시보드 데이터 반환 │ + └─────────────────────────────────────────────────────────────┘ + +2. [실시간 업데이트 (Event 구독)] + ┌─────────────────────────────────────────────────────────────┐ + │ Analytics Service (Background) │ + │ - EventCreated 구독: 이벤트 기본 정보 초기화 │ + │ - ParticipantRegistered 구독: 참여자 수 실시간 증가 │ + │ - DistributionCompleted 구독: 배포 채널 통계 업데이트 │ + │ - 캐시 무효화: 다음 조회 시 최신 데이터 갱신 │ + └─────────────────────────────────────────────────────────────┘ +``` + +### 3.4 플로우 특징 + +#### Kafka 통합 이점 +- **단일 메시징 플랫폼**: Event Bus와 Job Queue를 Kafka로 통합, 운영 복잡도 감소 +- **일관된 메시지 처리**: 모든 비동기 통신이 Kafka를 통해 이루어져 모니터링 및 디버깅 용이 +- **확장성**: Kafka의 높은 처리량으로 대규모 이벤트 처리 지원 + +#### Event-Driven 이점 +- **느슨한 결합**: 서비스 간 직접 의존성 제거 +- **장애 격리**: 한 서비스 장애가 다른 서비스에 영향 없음 +- **확장 용이**: 새로운 구독자 추가로 기능 확장 +- **비동기 처리**: 사용자 응답 시간 단축 + +#### Resilience 이점 +- **Circuit Breaker**: 외부 API 장애 시 빠른 실패 및 복구 +- **Retry**: 일시적 장애 자동 복구 +- **Fallback**: 장애 시에도 서비스 지속 (Graceful Degradation) +- **Bulkhead**: 리소스 격리로 장애 전파 차단 + +--- + +## 4. 데이터 흐름 및 캐싱 전략 + +### 4.1 데이터 흐름 + +#### 읽기 플로우 (Cache-Aside 패턴) +``` +1. Application → Cache 확인 + - Cache HIT: 캐시된 데이터 즉시 반환 + - Cache MISS: + 2. Application → Database/External API 조회 + 3. Database/External API → Application 데이터 반환 + 4. Application → Cache 데이터 저장 (TTL 설정) + 5. Application → Client 데이터 반환 +``` + +#### 쓰기 플로우 (Write-Through 패턴) +``` +1. Application → Database 쓰기 +2. Database → Application 성공 응답 +3. Application → Cache 무효화 또는 업데이트 +4. Application → Client 성공 응답 +``` + +### 4.2 캐싱 전략 + +#### Redis 캐시 구조 + +| 서비스 | 캐시 키 패턴 | 데이터 타입 | TTL | 예상 크기 | 히트율 목표 | +|--------|-------------|-----------|-----|----------|-----------| +| User | `user:session:{token}` | String | 7일 | 1KB | - | +| AI | `ai:recommendation:{업종}:{지역}:{목적}` | Hash | 24시간 | 10KB | 80% | +| AI | `ai:event:{이벤트ID}` | Hash | 24시간 | 10KB | - | +| Content | `content:image:{이벤트ID}:{스타일}` | String | 7일 | 0.2KB (URL) | 80% | +| Analytics | `analytics:dashboard:{이벤트ID}` | Hash | 5분 | 5KB | 95% | +| AI | `job:{jobId}` | Hash | 1시간 | 1KB | - | +| Content | `job:{jobId}` | Hash | 1시간 | 1KB | - | + +#### Redis 메모리 산정 +- **예상 동시 사용자**: 100명 +- **예상 이벤트 수**: 50개 +- **예상 캐시 항목 수**: 10,000개 +- **예상 총 메모리**: 약 50MB (운영 환경 2GB 할당) + +#### 캐시 무효화 전략 +- **TTL 기반 자동 만료**: 대부분의 캐시 +- **수동 무효화**: 이벤트 수정/삭제 시 관련 캐시 삭제 +- **Lazy 무효화**: 데이터 변경 시 다음 조회 시점에 갱신 + +### 4.3 데이터베이스 전략 + +#### 서비스별 독립 데이터베이스 +- **User DB**: users, stores +- **Event DB**: events, event_objectives, event_prizes, distribution_logs +- **Participation DB**: participants, winners +- **Analytics DB**: event_stats, channel_stats + +#### 데이터 일관성 전략 +- **Eventual Consistency**: 서비스 간 데이터는 최종 일관성 보장 +- **Strong Consistency**: 서비스 내부 트랜잭션은 강한 일관성 보장 +- **Saga 패턴**: 이벤트 생성 플로우 (보상 트랜잭션) + +--- + +## 5. 확장성 및 성능 고려사항 + +### 5.1 수평 확장 전략 + +#### 서비스별 확장 전략 +| 서비스 | 초기 인스턴스 | 확장 조건 | 최대 인스턴스 | Auto-scaling 메트릭 | +|--------|-------------|----------|-------------|-------------------| +| User | 2 | CPU > 70% | 5 | CPU, 메모리 | +| Event | 2 | CPU > 70% | 10 | CPU, 메모리 | +| AI | 1 | Job Queue > 10 | 3 | Queue 길이 | +| Content | 1 | Job Queue > 10 | 3 | Queue 길이 | +| Distribution | 2 | CPU > 70% | 5 | CPU, 메모리 | +| Participation | 1 | CPU > 70% | 3 | CPU, 메모리 | +| Analytics | 1 | CPU > 70% | 3 | CPU, 메모리 | + +#### Redis Cluster +- **초기 구성**: 3 노드 (Master 3, Replica 3) +- **확장**: 노드 추가를 통한 수평 확장 +- **HA**: Redis Sentinel을 통한 자동 Failover + +#### Database Replication +- **Primary-Replica 구조**: 읽기 부하 분산 +- **읽기 확장**: Read Replica 추가 (필요 시) +- **쓰기 확장**: Sharding (Phase 2 이후) + +### 5.2 성능 목표 + +#### 응답 시간 목표 +| 기능 | 목표 시간 | 캐시 HIT | 캐시 MISS | +|------|----------|---------|----------| +| 로그인 | 0.5초 | - | - | +| 이벤트 목록 조회 | 0.3초 | - | - | +| AI 트렌드 분석 + 추천 | 0.1초 | ✅ | 10초 (비동기) | +| SNS 이미지 생성 | 0.1초 | ✅ | 5초 (비동기) | +| 다중 채널 배포 | 1분 | - | - | +| 대시보드 로딩 | 0.5초 | ✅ | 3초 | + +#### 처리량 목표 +- **동시 사용자**: 100명 (MVP 목표) +- **API 요청**: 1,000 req/min +- **AI 작업**: 10 jobs/min +- **이미지 생성**: 10 jobs/min + +### 5.3 성능 최적화 기법 + +#### Frontend 최적화 +- **Code Splitting**: 페이지별 번들 분할 +- **Lazy Loading**: 차트 라이브러리 지연 로딩 +- **CDN**: 정적 자산 CDN 배포 +- **Compression**: Gzip/Brotli 압축 + +#### Backend 최적화 +- **Connection Pooling**: 데이터베이스 연결 풀 관리 +- **Query Optimization**: 인덱스 최적화, N+1 쿼리 방지 +- **Batch Processing**: 대량 데이터 일괄 처리 +- **Pagination**: 목록 조회 페이지네이션 + +#### Cache 최적화 +- **Multi-Level Caching**: Browser Cache → CDN → Redis → Database +- **Cache Warming**: 자주 사용되는 데이터 사전 로딩 +- **Cache Preloading**: 피크 시간 전 캐시 준비 + +--- + +## 6. 보안 고려사항 + +### 6.1 인증 및 인가 + +#### JWT 기반 인증 +- **토큰 발급**: User Service에서 로그인 시 JWT 토큰 발급 +- **토큰 검증**: API Gateway에서 모든 요청의 JWT 토큰 검증 +- **토큰 저장**: Redis에 세션 정보 저장 (TTL 7일) +- **토큰 갱신**: Refresh Token 패턴 (선택) + +#### 역할 기반 접근 제어 (RBAC) +- **역할**: OWNER (매장 사장님), CUSTOMER (이벤트 참여자) +- **권한 관리**: API별 필요 역할 정의 +- **API Gateway 검증**: 요청자의 역할 확인 + +### 6.2 데이터 보안 + +#### 민감 정보 암호화 +- **비밀번호**: bcrypt 해싱 (Cost Factor: 10) +- **사업자번호**: AES-256 암호화 저장 +- **개인정보**: 전화번호 마스킹 (010-****-1234) + +#### 전송 보안 +- **HTTPS**: 모든 통신 TLS 1.3 암호화 +- **API Key**: 외부 API 호출 시 안전한 Key 관리 (AWS Secrets Manager) + +#### 데이터 접근 통제 +- **Database**: 서비스별 독립 계정, 최소 권한 원칙 +- **Redis**: 비밀번호 설정, ACL 적용 +- **백업**: 암호화된 백업 저장 + +### 6.3 보안 모니터링 + +#### 위협 탐지 +- **Rate Limiting**: API Gateway에서 사용자당 100 req/min +- **Brute Force 방지**: 로그인 5회 실패 시 계정 잠금 (삭제됨, 향후 추가 가능) +- **SQL Injection 방지**: Prepared Statement 사용 +- **XSS 방지**: 입력 데이터 Sanitization + +#### 로깅 및 감사 +- **Access Log**: 모든 API 요청 로깅 +- **Audit Log**: 민감 작업 (로그인, 이벤트 생성, 당첨자 추첨) 감사 로그 +- **중앙집중식 로깅**: ELK Stack 또는 CloudWatch Logs + +--- + +## 7. 논리 아키텍처 다이어그램 + +논리 아키텍처 다이어그램은 별도 Mermaid 파일로 작성되었습니다. + +**파일 위치**: `logical-architecture.mmd` + +**다이어그램 확인 방법**: +1. https://mermaid.live/edit 접속 +2. `logical-architecture.mmd` 파일 내용 붙여넣기 +3. 다이어그램 시각적 확인 + +**다이어그램 구성**: +- Services: 4개 핵심 서비스 (User, Event, Participation, Analytics) +- Async Services: 3개 비동기 서비스 (AI, Content, Distribution) +- Kafka: Event Topics + Job Topics 통합 메시징 플랫폼 +- External System: 통합된 외부 시스템 (국세청 API, AI API, 이미지 생성 API, 배포 채널 APIs) + +**의존성 표현**: +- 굵은 화살표 (==>): Kafka Event Topics 발행 +- 실선 화살표 (-->): Kafka Job Topics 발행 또는 외부 시스템 호출 +- 점선 화살표 (-.->): Kafka 구독 + +--- + +## 부록 + +### A. 참고 문서 +- [유저스토리](../../userstory.md) +- [아키텍처 패턴](../../pattern/architecture-pattern.md) +- [UI/UX 설계서](../../uiux/uiux.md) +- [클라우드 디자인 패턴](../../../claude/cloud-design-patterns.md) + +### B. 주요 결정사항 +1. **Kafka 통합 메시징 플랫폼 채택**: Event Bus와 Job Queue를 Kafka로 통합하여 운영 복잡도 감소 +2. **Event-Driven 아키텍처 채택**: Kafka를 통한 서비스 간 느슨한 결합 및 비동기 통신 +3. **도메인 이벤트 정의**: 3개 Event Topics (EventCreated, ParticipantRegistered, DistributionCompleted) +4. **Job Topics 정의**: 2개 Job Topics (ai 이벤트 생성, 이미지 생성)로 장시간 비동기 작업 처리 +5. **Resilience 패턴 전면 적용**: Circuit Breaker, Retry, Timeout, Bulkhead, Fallback +6. **At-Least-Once Delivery**: Kafka 메시지 보장 및 멱등성 설계 +7. **Cache-Aside 패턴**: AI/이미지 생성 결과 캐싱으로 응답 시간 90% 개선 +8. **Redis 기반 서비스 간 데이터 공유**: AI Service → Redis → Content Service 데이터 흐름 +9. **Redis to DB 영구 저장**: 이벤트 생성 완료 시 Redis 데이터를 Event DB에 저장 +10. **동기 배포**: Event Service가 Distribution Service를 REST API로 직접 호출하여 다중 채널 배포 동기 처리 +11. **서비스별 독립 Database**: Database-per-Service 패턴으로 서비스 독립성 보장 +12. **장시간 작업 Timeout 조정**: AI/Content Service Timeout을 5분으로 설정하여 복잡한 생성 작업 지원 + +### C. 향후 개선 방안 (Phase 2 이후) +1. **Event Sourcing 완전 적용**: 모든 상태 변경을 이벤트로 저장하여 시간 여행 및 감사 추적 강화 +2. **Saga 패턴 적용**: 복잡한 분산 트랜잭션 보상 로직 체계화 +3. **Service Mesh 도입**: Istio를 통한 서비스 간 통신 관찰성 및 보안 강화 +4. **Database Sharding**: Event/Participation Write DB 샤딩으로 쓰기 확장성 개선 +5. **WebSocket 기반 실시간 푸시**: 대시보드 실시간 업데이트 (폴링 대체) +6. **GraphQL API Gateway**: 클라이언트 맞춤형 데이터 조회 최적화 +7. **Dead Letter Queue 고도화**: 실패 이벤트 재처리 및 알림 자동화 + +--- + +**문서 버전**: 2.2 +**최종 수정일**: 2025-10-22 +**작성자**: System Architect +**변경 사항**: Distribution Service 동기 호출 전환 (REST API 직접 호출) diff --git a/design/backend/logical/logical-architecture.mmd b/design/backend/logical/logical-architecture.mmd new file mode 100644 index 0000000..d84f619 --- /dev/null +++ b/design/backend/logical/logical-architecture.mmd @@ -0,0 +1,71 @@ +graph TB + %% KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 논리 아키텍처 (Event-Driven + Kafka) + + %% Services + subgraph "Services" + UserSvc["User Service
• 회원가입/로그인
• 프로필 관리
• 회원정보 제공"] + EventSvc["Event Service
• 이벤트 생성/수정/삭제
• 플로우 오케스트레이션
• AI 작업 요청
• 배포 작업 요청
• Redis → DB 저장"] + PartSvc["Participation
Service
• 참여 접수
• 참여자 목록
• 당첨자 추첨"] + AnalSvc["Analytics Service
• 실시간 대시보드
• 성과 분석
• 채널별 통계
[Circuit Breaker]"] + end + + %% Async Services + subgraph "Async Services" + AISvc["AI Service
• 트렌드 분석
• 이벤트 추천
• Redis 저장
[Circuit Breaker]
[Timeout: 5분]"] + ContentSvc["Content Service
• Redis 데이터 읽기
• SNS 이미지 생성
• Redis 저장
[Circuit Breaker]
[Timeout: 5분]"] + DistSvc["Distribution
Service
• 다중 채널 배포
[Circuit Breaker]
[Retry: 3회]
[Bulkhead]"] + end + + %% Kafka (Event Bus + Job Queue) + Kafka["Kafka
━━━━━━━━━━

• EventCreated
• ParticipantRegistered
• DistributionCompleted
━━━━━━━━━━

• ai 이벤트 생성"] + + %% External System + External["외부시스템
[Circuit Breaker]
━━━━━━━━━━
• AI API
• 이미지 생성 API
• 배포 채널 APIs
(비동기)"] + + %% Redis + Redis["Redis Cache
━━━━━━━━━━
• AI 결과
• 이미지 URL
• 이벤트 데이터"] + + %% Event Publishing + EventSvc ==>|"EventCreated
발행"| Kafka + PartSvc ==>|"ParticipantRegistered
발행"| Kafka + DistSvc ==>|"DistributionCompleted
발행"| Kafka + + %% Job Publishing (비동기 작업 요청) + EventSvc -->|"ai 이벤트 생성 발행"| Kafka + + %% Event Subscription + Kafka -.->|"EventCreated
구독"| AnalSvc + Kafka -.->|"ParticipantRegistered
구독"| AnalSvc + Kafka -.->|"DistributionCompleted
구독"| AnalSvc + + %% Job Subscription + Kafka -.->|"ai 이벤트 생성 구독"| AISvc + + %% Service to Service (동기 호출) + EventSvc -->|"다중 채널 배포
[Circuit Breaker]"| DistSvc + EventSvc -->|"회원정보 조회"| UserSvc + + %% Redis Interactions + AISvc -->|"AI 결과 저장"| Redis + ContentSvc -->|"AI 데이터 읽기"| Redis + ContentSvc -->|"이미지 URL 저장"| Redis + EventSvc -->|"Redis → DB 저장"| Redis + + %% Services to External (Resilience 패턴) + AISvc -->|"트렌드 분석/추천"| External + ContentSvc -->|"이미지 생성"| External + DistSvc -->|"다중 채널 배포
(비동기)"| External + AnalSvc -->|"채널별 통계
[Fallback: Cache]"| External + + %% Styling + classDef service fill:#4ECDC4,stroke:#14B8A6,stroke-width:3px + classDef async fill:#8B5CF6,stroke:#7C3AED,stroke-width:3px,color:#fff + classDef kafka fill:#F59E0B,stroke:#D97706,stroke-width:3px + classDef external fill:#E5E7EB,stroke:#9CA3AF,stroke-width:2px + classDef cache fill:#EF4444,stroke:#DC2626,stroke-width:3px + + class UserSvc,EventSvc,PartSvc,AnalSvc service + class AISvc,ContentSvc,DistSvc async + class Kafka kafka + class External external + class Redis cache diff --git a/design/backend/sequence/inner/README.md b/design/backend/sequence/inner/README.md new file mode 100644 index 0000000..ac0f380 --- /dev/null +++ b/design/backend/sequence/inner/README.md @@ -0,0 +1,393 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 내부 시퀀스 설계서 + +## 문서 정보 +- **작성일**: 2025-10-22 +- **버전**: 1.0 +- **작성자**: System Architect +- **관련 문서**: + - [유저스토리](../../userstory.md) + - [외부 시퀀스 설계서](../outer/) + - [논리 아키텍처](../../logical/logical-architecture.md) + +--- + +## 목차 +1. [개요](#1-개요) +2. [서비스별 시나리오 목록](#2-서비스별-시나리오-목록) +3. [설계 원칙](#3-설계-원칙) +4. [주요 패턴](#4-주요-패턴) +5. [파일 구조](#5-파일-구조) +6. [PlantUML 다이어그램 확인 방법](#6-plantuml-다이어그램-확인-방법) + +--- + +## 1. 개요 + +본 문서는 KT AI 기반 소상공인 이벤트 자동 생성 서비스의 **7개 마이크로서비스**에 대한 **26개 내부 시퀀스 다이어그램**을 포함합니다. + +### 1.1 설계 범위 + +각 마이크로서비스 내부의 처리 흐름을 상세히 표현: +- **API 레이어**: Controller +- **비즈니스 레이어**: Service, Validator, Domain Logic +- **데이터 레이어**: Repository, Cache Manager +- **인프라 레이어**: Kafka, Redis, Database, External APIs + +### 1.2 설계 대상 서비스 + +| 서비스 | 시나리오 수 | 주요 책임 | +|--------|------------|----------| +| **User** | 4 | 사용자 인증, 프로필 관리 | +| **Event** | 10 | 이벤트 생명주기 관리, 오케스트레이션 | +| **Participation** | 3 | 참여자 관리, 당첨자 추첨 | +| **Analytics** | 5 | 실시간 성과 분석, 대시보드 | +| **AI** | 1 | AI 트렌드 분석 및 이벤트 추천 | +| **Content** | 1 | SNS 이미지 생성 | +| **Distribution** | 2 | 다중 채널 배포 | +| **총계** | **26** | - | + +--- + +## 2. 서비스별 시나리오 목록 + +### 2.1 User 서비스 (4개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 회원가입 | `user-회원가입.puml` | UFR-USER-010 | 사업자번호 검증(Circuit Breaker), 트랜잭션, JWT 발급 | +| 로그인 | `user-로그인.puml` | UFR-USER-020 | 비밀번호 검증(bcrypt), JWT 발급, 세션 저장 | +| 프로필수정 | `user-프로필수정.puml` | UFR-USER-030 | 기본/매장 정보 수정, 비밀번호 변경, 트랜잭션 | +| 로그아웃 | `user-로그아웃.puml` | UFR-USER-040 | JWT 검증, 세션 삭제, Blacklist 추가 | + +**주요 특징**: +- **Resilience 패턴**: Circuit Breaker (국세청 API), Retry, Timeout, Fallback +- **보안**: bcrypt 해싱, AES-256 암호화, JWT 관리 +- **캐싱**: 사업자번호 검증 결과 (TTL 7일), 세션 정보 (TTL 7일) + +--- + +### 2.2 Event 서비스 (10개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 목적선택 | `event-목적선택.puml` | UFR-EVENT-020 | 이벤트 목적 선택 및 저장, EventCreated 발행 | +| AI추천요청 | `event-AI추천요청.puml` | UFR-EVENT-030 | Kafka ai-job 발행, Job ID 반환 (202 Accepted) | +| 추천결과조회 | `event-추천결과조회.puml` | UFR-EVENT-030 | Redis Job 상태 폴링 조회 | +| 이미지생성요청 | `event-이미지생성요청.puml` | UFR-CONT-010 | Kafka image-job 발행, Job ID 반환 (202 Accepted) | +| 이미지결과조회 | `event-이미지결과조회.puml` | UFR-CONT-010 | Redis Job 상태 폴링 조회 | +| 콘텐츠선택 | `event-콘텐츠선택.puml` | UFR-CONT-020 | 선택한 콘텐츠 저장 | +| 최종승인및배포 | `event-최종승인및배포.puml` | UFR-EVENT-050 | Distribution Service 동기 호출, 상태 변경 | +| 상세조회 | `event-상세조회.puml` | UFR-EVENT-060 | 이벤트 상세 조회 (캐싱) | +| 목록조회 | `event-목록조회.puml` | UFR-EVENT-070 | 이벤트 목록 조회 (필터/검색/페이지네이션) | +| 대시보드조회 | `event-대시보드조회.puml` | UFR-EVENT-010 | 대시보드 이벤트 목록 (병렬 쿼리) | + +**주요 특징**: +- **Kafka 통합**: Event Topics (EventCreated), Job Topics (ai-job, image-job) +- **비동기 처리**: Job 발행 → 폴링 방식 결과 조회 +- **동기 호출**: Distribution Service REST API 직접 호출 +- **캐싱 전략**: 목적(30분), 상세(5분), 목록/대시보드(1분) + +--- + +### 2.3 Participation 서비스 (3개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 이벤트참여 | `participation-이벤트참여.puml` | UFR-PART-010 | 중복 체크, ParticipantRegistered 발행 | +| 참여자목록조회 | `participation-참여자목록조회.puml` | UFR-PART-020 | 필터/검색, 페이지네이션, 전화번호 마스킹 | +| 당첨자추첨 | `participation-당첨자추첨.puml` | UFR-PART-030 | Fisher-Yates Shuffle, WinnerSelected 발행 | + +**주요 특징**: +- **중복 방지**: Redis Cache + DB 2단계 체크 +- **추첨 알고리즘**: 난수 기반 공정성, 가산점 시스템, Fisher-Yates Shuffle +- **Kafka Event**: ParticipantRegistered, WinnerSelected → Analytics Service 구독 +- **보안**: 전화번호 마스킹 (010-****-1234) + +--- + +### 2.4 Analytics 서비스 (5개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 대시보드조회-캐시히트 | `analytics-대시보드조회-캐시히트.puml` | UFR-ANAL-010 | Redis 캐시 HIT (0.5초) | +| 대시보드조회-캐시미스 | `analytics-대시보드조회-캐시미스.puml` | UFR-ANAL-010 | 외부 API 병렬 호출, ROI 계산 (3초) | +| 이벤트생성구독 | `analytics-이벤트생성구독.puml` | - | EventCreated 구독, 통계 초기화 | +| 참여자등록구독 | `analytics-참여자등록구독.puml` | - | ParticipantRegistered 구독, 실시간 통계 | +| 배포완료구독 | `analytics-배포완료구독.puml` | - | DistributionCompleted 구독, 배포 통계 | + +**주요 특징**: +- **Cache-Aside 패턴**: Redis 캐싱 (TTL 5분, 히트율 95%) +- **외부 API 병렬 호출**: 우리동네TV, 지니TV, SNS APIs (Circuit Breaker, Timeout, Fallback) +- **Kafka 구독**: 3개 Event Topics 실시간 처리 +- **멱등성 보장**: Redis Set으로 중복 이벤트 방지 + +--- + +### 2.5 AI 서비스 (1개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 트렌드분석및추천 | `ai-트렌드분석및추천.puml` | UFR-AI-010 | Kafka ai-job 구독, 트렌드 분석, 3가지 추천 병렬 생성 | + +**주요 특징**: +- **Kafka Job 구독**: ai-job 토픽 Consumer +- **외부 AI API**: Claude/GPT-4 호출 (Circuit Breaker, Timeout 30초) +- **캐싱 전략**: 트렌드 분석 결과 (TTL 1시간), 추천 결과 (TTL 24시간) +- **3가지 옵션 병렬 생성**: 저비용/중비용/고비용 추천안 + +--- + +### 2.6 Content 서비스 (1개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 이미지생성 | `content-이미지생성.puml` | UFR-CONT-010 | Kafka image-job 구독, 3가지 스타일 병렬 생성 | + +**주요 특징**: +- **Kafka Job 구독**: image-job 토픽 Consumer +- **외부 이미지 API**: Stable Diffusion/DALL-E 병렬 호출 (Circuit Breaker, Timeout 20초) +- **3가지 스타일 병렬**: 심플/화려한/트렌디 (par 블록) +- **CDN 업로드**: 이미지 URL 캐싱 (TTL 7일) +- **Fallback 2단계**: Stable Diffusion 실패 → DALL-E → 기본 템플릿 + +--- + +### 2.7 Distribution 서비스 (2개) + +| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 | +|----------|--------|-----------|---------------| +| 다중채널배포 | `distribution-다중채널배포.puml` | UFR-DIST-010 | REST API 동기 호출, 채널별 병렬 배포, DistributionCompleted 발행 | +| 배포상태조회 | `distribution-배포상태조회.puml` | UFR-DIST-020 | 배포 상태 모니터링, 재시도 기능 | + +**주요 특징**: +- **동기 호출**: Event Service → Distribution Service REST API +- **채널별 병렬 배포**: 우리동네TV, 링고비즈, 지니TV, SNS APIs (par 블록) +- **Resilience 패턴**: Circuit Breaker, Retry (3회), Bulkhead (채널별 독립) +- **독립 처리**: 하나 실패해도 다른 채널 계속 +- **Kafka Event**: DistributionCompleted → Analytics Service 구독 + +--- + +## 3. 설계 원칙 + +### 3.1 공통설계원칙 준수 + +✅ **PlantUML 표준** +- `!theme mono` 테마 적용 +- 명확한 타이틀 및 참여자 타입 표시 +- 외부 시스템/인프라 `<>` 표시 + +✅ **레이어 아키텍처** +``` +Controller (API Layer) + ↓ +Service (Business Layer) + ↓ +Repository (Data Layer) + ↓ +External Systems (Redis, DB, Kafka, APIs) +``` + +✅ **동기/비동기 구분** +- 실선 화살표 (`→`): 동기 호출 +- 점선 화살표 (`-->`): 비동기 호출 (Kafka) +- `activate`/`deactivate`: 생명선 활성화 + +### 3.2 내부시퀀스설계 가이드 준수 + +✅ **유저스토리 기반 설계** +- 20개 유저스토리와 정확히 매칭 +- 불필요한 추가 설계 배제 + +✅ **외부 시퀀스와 일치** +- 외부 시퀀스 다이어그램과 플로우 일치 +- 서비스 간 통신 방식 동일 + +✅ **모든 레이어 표시** +- API, 비즈니스, 데이터, 인프라 레이어 명시 +- 캐시, DB, 외부 API 접근 표시 + +--- + +## 4. 주요 패턴 + +### 4.1 Resilience 패턴 + +#### Circuit Breaker +- **적용 대상**: 모든 외부 API 호출 +- **설정**: 실패율 50% 초과 시 Open, 30초 후 Half-Open +- **효과**: 빠른 실패로 리소스 보호 + +#### Retry Pattern +- **적용 대상**: 일시적 장애가 예상되는 외부 API +- **설정**: 최대 3회, 지수 백오프 (1초, 2초, 4초) +- **효과**: 일시적 장애 자동 복구 + +#### Timeout Pattern +- **적용 대상**: 모든 외부 API 호출 +- **설정**: 국세청 5초, AI 30초, 이미지 20초, 배포 10초 +- **효과**: 리소스 점유 방지 + +#### Fallback Pattern +- **적용 대상**: 외부 API 장애 시 +- **전략**: 캐시된 이전 데이터, 기본값, 검증 스킵 +- **효과**: 서비스 지속성 보장 (Graceful Degradation) + +#### Bulkhead Pattern +- **적용 대상**: Distribution Service 다중 채널 배포 +- **설정**: 채널별 독립 스레드 풀 +- **효과**: 채널 장애 격리, 장애 전파 차단 + +### 4.2 캐싱 전략 (Cache-Aside) + +| 서비스 | 캐시 키 패턴 | TTL | 히트율 목표 | 효과 | +|--------|-------------|-----|-----------|------| +| User | `user:business:{사업자번호}` | 7일 | 90% | 5초 → 0.1초 (98% 개선) | +| AI | `ai:recommendation:{업종}:{지역}:{목적}` | 24시간 | 80% | 10초 → 0.1초 (99% 개선) | +| Content | `content:image:{이벤트ID}:{스타일}` | 7일 | 80% | 5초 → 0.1초 (98% 개선) | +| Analytics | `analytics:dashboard:{이벤트ID}` | 5분 | 95% | 3초 → 0.5초 (83% 개선) | +| Event | `event:detail:{eventId}` | 5분 | 85% | 1초 → 0.2초 (80% 개선) | +| Participation | `participation:list:{eventId}:{filter}` | 5분 | 90% | 2초 → 0.3초 (85% 개선) | + +### 4.3 Event-Driven 패턴 + +#### Kafka Event Topics (도메인 이벤트) +- **EventCreated**: 이벤트 생성 시 → Analytics Service 구독 +- **ParticipantRegistered**: 참여자 등록 시 → Analytics Service 구독 +- **WinnerSelected**: 당첨자 선정 시 → (추후 확장) +- **DistributionCompleted**: 배포 완료 시 → Analytics Service 구독 + +#### Kafka Job Topics (비동기 작업) +- **ai-job**: AI 추천 요청 → AI Service 구독 +- **image-job**: 이미지 생성 요청 → Content Service 구독 + +#### 멱등성 보장 +- Redis Set으로 이벤트 ID 중복 체크 +- 동일 이벤트 중복 처리 시 무시 + +--- + +## 5. 파일 구조 + +``` +design/backend/sequence/inner/ +├── README.md (본 문서) +├── user-회원가입.puml +├── user-로그인.puml +├── user-프로필수정.puml +├── user-로그아웃.puml +├── event-목적선택.puml +├── event-AI추천요청.puml +├── event-추천결과조회.puml +├── event-이미지생성요청.puml +├── event-이미지결과조회.puml +├── event-콘텐츠선택.puml +├── event-최종승인및배포.puml +├── event-상세조회.puml +├── event-목록조회.puml +├── event-대시보드조회.puml +├── participation-이벤트참여.puml +├── participation-참여자목록조회.puml +├── participation-당첨자추첨.puml +├── analytics-대시보드조회-캐시히트.puml +├── analytics-대시보드조회-캐시미스.puml +├── analytics-이벤트생성구독.puml +├── analytics-참여자등록구독.puml +├── analytics-배포완료구독.puml +├── ai-트렌드분석및추천.puml +├── content-이미지생성.puml +├── distribution-다중채널배포.puml +└── distribution-배포상태조회.puml +``` + +**총 26개 파일, 약 114KB** + +--- + +## 6. PlantUML 다이어그램 확인 방법 + +### 6.1 온라인 확인 + +#### PlantUML Web Server +1. https://www.plantuml.com/plantuml/uml 접속 +2. 각 `.puml` 파일 내용 복사 +3. 에디터에 붙여넣기 +4. 다이어그램 시각적 확인 +5. PNG/SVG/PDF 다운로드 가능 + +#### PlantUML Editor (추천) +1. https://plantuml-editor.kkeisuke.com/ 접속 +2. 실시간 미리보기 제공 +3. 편집 및 다운로드 지원 + +### 6.2 로컬 확인 (Docker) + +#### Docker로 PlantUML 검증 +```bash +# Docker 실행 필요 +docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:jetty + +# 각 파일 문법 검사 +cat "user-회원가입.puml" | docker exec -i plantuml java -jar /app/plantuml.jar -syntax +``` + +### 6.3 IDE 플러그인 + +#### IntelliJ IDEA +- **PlantUML Integration** 플러그인 설치 +- `.puml` 파일 우클릭 → "Show PlantUML Diagram" + +#### VS Code +- **PlantUML** 확장 설치 +- `Alt+D`: 미리보기 열기 + +--- + +## 부록 + +### A. 파일 크기 및 통계 + +| 서비스 | 시나리오 수 | 총 크기 | 평균 크기 | +|--------|------------|---------|----------| +| User | 4 | 21.2KB | 5.3KB | +| Event | 10 | 20.2KB | 2.0KB | +| Participation | 3 | 15.4KB | 5.1KB | +| Analytics | 5 | 20.8KB | 4.2KB | +| AI | 1 | 12KB | 12KB | +| Content | 1 | 8.5KB | 8.5KB | +| Distribution | 2 | 17.5KB | 8.8KB | +| **총계** | **26** | **115.6KB** | **4.4KB** | + +### B. 주요 기술 스택 + +#### Backend +- **Framework**: Spring Boot +- **ORM**: JPA/Hibernate +- **Security**: Spring Security + JWT +- **Cache**: Redis +- **Database**: PostgreSQL +- **Message Queue**: Apache Kafka + +#### Resilience +- **Circuit Breaker**: Resilience4j +- **Retry**: Resilience4j RetryRegistry +- **Timeout**: Resilience4j TimeLimiterRegistry + +#### Utilities +- **Password**: bcrypt (Spring Security) +- **JWT**: jjwt library +- **Encryption**: AES-256 (javax.crypto) + +### C. 참고 문서 +- [유저스토리](../../userstory.md) +- [외부 시퀀스 설계서](../outer/) +- [논리 아키텍처](../../logical/logical-architecture.md) +- [공통설계원칙](../../../../claude/common-principles.md) +- [내부시퀀스설계 가이드](../../../../claude/sequence-inner-design.md) + +--- + +**문서 버전**: 1.0 +**최종 수정일**: 2025-10-22 +**작성자**: System Architect (박영자) +**내부 시퀀스 설계 완료**: ✅ 26개 시나리오 모두 작성 완료 diff --git a/design/backend/sequence/inner/ai-트렌드분석및추천.puml b/design/backend/sequence/inner/ai-트렌드분석및추천.puml new file mode 100644 index 0000000..4cd369a --- /dev/null +++ b/design/backend/sequence/inner/ai-트렌드분석및추천.puml @@ -0,0 +1,343 @@ +@startuml ai-트렌드분석및추천 +!theme mono + +title AI Service - 트렌드 분석 및 이벤트 추천 (내부 시퀀스) + +actor Client +participant "Kafka Consumer" as Consumer <> +participant "JobMessageHandler" as Handler <> +participant "AIRecommendationService" as Service <> +participant "TrendAnalysisEngine" as TrendEngine <> +participant "RecommendationEngine" as RecommendEngine <> +participant "CacheManager" as Cache <> +participant "CircuitBreakerManager" as CB <> +participant "ExternalAIClient" as AIClient <> +participant "JobStateManager" as JobState <> +participant "Redis" as Redis <> +participant "External AI API" as ExternalAPI <> +participant "Kafka Producer" as Producer <> + +note over Consumer: Kafka ai-job Topic 구독\nConsumer Group: ai-service-group + +== 1. Job 메시지 수신 == +Consumer -> Handler: onMessage(jobMessage)\n{jobId, eventDraftId, 목적, 업종, 지역, 매장정보} +activate Handler + +Handler -> Handler: 메시지 유효성 검증 +note right +검증 항목: +- jobId 존재 여부 +- eventDraftId 유효성 +- 필수 파라미터 (목적, 업종, 지역) +end note + +alt 유효하지 않은 메시지 + Handler -> Producer: DLQ 발행 (Dead Letter Queue)\n{jobId, error: INVALID_MESSAGE} + Handler --> Consumer: ACK (메시지 처리 완료) + note over Handler: 잘못된 메시지는 DLQ로 이동\n수동 검토 필요 +else 유효한 메시지 + Handler -> JobState: updateJobStatus(jobId, PROCESSING) + JobState -> Redis: SET job:{jobId}:status = PROCESSING + Redis --> JobState: OK + JobState --> Handler: 상태 업데이트 완료 + + Handler -> Service: generateRecommendations(\neventDraftId, 목적, 업종, 지역, 매장정보) + activate Service + + == 2. 트렌드 분석 == + Service -> TrendEngine: analyzeTrends(업종, 지역, 목적) + activate TrendEngine + + TrendEngine -> Cache: getCachedTrend(업종, 지역) + Cache -> Redis: GET trend:{업종}:{지역} + Redis --> Cache: 캐시 결과 + + alt 캐시 히트 + Cache --> TrendEngine: 캐시된 트렌드 데이터 + note right + 캐시 키: trend:{업종}:{지역} + TTL: 1시간 + 데이터: { + industry_trends, + regional_characteristics, + seasonal_patterns + } + end note + + else 캐시 미스 + note right of TrendEngine + **트렌드 분석 입력 데이터** + - 업종 정보 + - 지역 정보 + - 현재 시즌 (계절, 월) + - 이벤트 목적 + + **외부 AI API 호출** + - 과거 이벤트 데이터 사용 안 함 + - 실시간 시장 트렌드 분석 + - 업종별/지역별 일반적 특성 + end note + + TrendEngine -> CB: executeWithCircuitBreaker(\nAI API 트렌드 분석 호출) + activate CB + + CB -> CB: Circuit Breaker 상태 확인 + note right + **Circuit Breaker 설정** + - Failure Rate Threshold: 50% + - Timeout: 5분 (300초) + - Half-Open Wait Duration: 1분 (60초) + - Permitted Calls in Half-Open: 3 + - Sliding Window Size: 10 + end note + + alt Circuit CLOSED (정상) + CB -> AIClient: callAIAPI(\nmethod: "trendAnalysis",\nprompt: 트렌드 분석 프롬프트,\ntimeout: 5분) + activate AIClient + + AIClient -> AIClient: 프롬프트 구성 + note right of AIClient + **AI 프롬프트 구성** + "당신은 마케팅 트렌드 분석 전문가입니다. + + **입력 정보** + - 업종: {업종} + - 지역: {지역} + - 현재 시즌: {계절/월} + - 이벤트 목적: {목적} + + **분석 요청사항** + 1. 업종별 일반적 트렌드 + (업종 특성 기반 효과적인 이벤트 유형) + + 2. 지역별 특성 + (지역 고객 특성, 선호도) + + 3. 시즌별 추천 + (현재 시기에 적합한 이벤트)" + end note + + AIClient -> ExternalAPI: AI API 호출\nPOST /api/v1/analyze\nAuthorization: Bearer {API_KEY}\nTimeout: 5분\nPayload: {업종, 지역, 시즌, 목적} + activate ExternalAPI + + ExternalAPI --> AIClient: 200 OK\n{"industryTrend": "...",\n"regionalCharacteristics": "...",\n"seasonalRecommendation": "..."} + deactivate ExternalAPI + + AIClient -> AIClient: 응답 검증 및 파싱 + AIClient --> CB: 분석 결과 + deactivate AIClient + + CB -> CB: 성공 기록 (Circuit Breaker) + CB --> TrendEngine: 트렌드 분석 결과 + deactivate CB + + TrendEngine -> Cache: cacheTrend(\nkey: trend:{업종}:{지역},\ndata: 분석결과,\nTTL: 1시간) + Cache -> Redis: SETEX trend:{업종}:{지역} 3600 {분석결과} + Redis --> Cache: OK + Cache --> TrendEngine: 캐싱 완료 + + else Circuit OPEN (장애) + CB --> TrendEngine: CircuitBreakerOpenException + TrendEngine -> TrendEngine: Fallback 실행\n(기본 트렌드 데이터 사용) + note right + Fallback 전략: + - 이전 캐시 데이터 반환 + - 또는 기본 트렌드 템플릿 사용 + - 클라이언트에 안내 메시지 포함 + end note + + else Circuit HALF-OPEN (복구 시도) + CB -> AIClient: 제한된 요청 허용 (3개) + AIClient -> ExternalAPI: POST /api/v1/analyze + ExternalAPI --> AIClient: 200 OK + AIClient --> CB: 성공 + CB -> CB: 연속 성공 시 CLOSED로 전환 + CB --> TrendEngine: 트렌드 분석 결과 + + else Timeout (5분 초과) + CB --> TrendEngine: TimeoutException + note right of TrendEngine + **Timeout 처리** + - 5분 초과 시 즉시 실패 + - Fallback: 기본 트렌드 사용 + - 사용자에게 안내 메시지 제공 + end note + TrendEngine -> TrendEngine: Fallback 실행\n(기본 트렌드 템플릿 사용) + end + end + + TrendEngine --> Service: 트렌드 분석 완료\n{업종트렌드, 지역특성, 시즌특성} + deactivate TrendEngine + + == 3. 이벤트 추천 생성 (3가지 옵션) == + Service -> RecommendEngine: generateRecommendations(\n목적, 트렌드, 매장정보) + activate RecommendEngine + + RecommendEngine -> RecommendEngine: 추천 컨텍스트 구성 + note right + 추천 입력: + - 이벤트 목적 (신규 고객 유치 등) + - 트렌드 분석 결과 + - 매장 정보 (업종, 위치, 크기) + - 예산 범위 (저/중/고) + end note + + group parallel + RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 1: 저비용) + activate CB + CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 저비용 추천 프롬프트,\ntimeout: 5분) + activate AIClient + + AIClient -> AIClient: 프롬프트 구성 + note right + 옵션 1 프롬프트: + "저비용, 높은 참여율 중심 이벤트 기획 + 목적: {목적} + 트렌드: {트렌드} + 매장: {매장정보} + + 출력 형식: + - 이벤트 제목 + - 추천 경품 (예산: 저) + - 참여 방법 (난이도: 낮음) + - 예상 참여자 수 + - 예상 비용 + - 예상 ROI" + end note + + AIClient -> ExternalAPI: POST /api/v1/recommend\n(저비용 옵션) + ExternalAPI --> AIClient: 200 OK\n{추천안 1} + AIClient --> CB: 추천안 1 + deactivate AIClient + CB --> RecommendEngine: 옵션 1 완료 + deactivate CB + + RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 2: 중비용) + activate CB + CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 중비용 추천 프롬프트,\ntimeout: 5분) + activate AIClient + + AIClient -> AIClient: 프롬프트 구성 + note right + 옵션 2 프롬프트: + "중비용, 균형잡힌 ROI 이벤트 기획 + 목적: {목적} + 트렌드: {트렌드} + 매장: {매장정보} + + 출력 형식: + - 이벤트 제목 + - 추천 경품 (예산: 중) + - 참여 방법 (난이도: 중간) + - 예상 참여자 수 + - 예상 비용 + - 예상 ROI" + end note + + AIClient -> ExternalAPI: POST /api/v1/recommend\n(중비용 옵션) + ExternalAPI --> AIClient: 200 OK\n{추천안 2} + AIClient --> CB: 추천안 2 + deactivate AIClient + CB --> RecommendEngine: 옵션 2 완료 + deactivate CB + + RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 3: 고비용) + activate CB + CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 고비용 추천 프롬프트,\ntimeout: 5분) + activate AIClient + + AIClient -> AIClient: 프롬프트 구성 + note right + 옵션 3 프롬프트: + "고비용, 높은 매출 증대 이벤트 기획 + 목적: {목적} + 트렌드: {트렌드} + 매장: {매장정보} + + 출력 형식: + - 이벤트 제목 + - 추천 경품 (예산: 고) + - 참여 방법 (난이도: 높음) + - 예상 참여자 수 + - 예상 비용 + - 예상 ROI" + end note + + AIClient -> ExternalAPI: POST /api/v1/recommend\n(고비용 옵션) + ExternalAPI --> AIClient: 200 OK\n{추천안 3} + AIClient --> CB: 추천안 3 + deactivate AIClient + CB --> RecommendEngine: 옵션 3 완료 + deactivate CB + end + + RecommendEngine -> RecommendEngine: 3가지 추천안 통합 및 검증 + note right + 검증 항목: + - 필수 필드 존재 여부 + - 예상 성과 계산 (ROI) + - 추천안 차별화 확인 + - 홍보 문구 생성 (각 5개) + - SNS 해시태그 자동 생성 + end note + + RecommendEngine --> Service: 3가지 추천안 생성 완료 + deactivate RecommendEngine + + == 4. 결과 저장 및 Job 상태 업데이트 == + Service -> Cache: cacheRecommendations(\nkey: ai:recommendation:{eventDraftId},\ndata: {트렌드+추천안},\nTTL: 24시간) + Cache -> Redis: SETEX ai:recommendation:{eventDraftId} 86400 {결과} + Redis --> Cache: OK + Cache --> Service: 캐싱 완료 + + Service -> JobState: updateJobStatus(\njobId,\nstatus: COMPLETED,\nresult: {트렌드, 추천안}) + JobState -> Redis: HSET job:{jobId} status COMPLETED result {JSON} + Redis --> JobState: OK + JobState --> Service: 상태 업데이트 완료 + + Service --> Handler: 추천 생성 완료\n{트렌드분석, 3가지추천안} + deactivate Service + + Handler --> Consumer: ACK (메시지 처리 완료) + deactivate Handler + + note over Consumer: Job 처리 완료\nRedis에 저장된 결과를\n클라이언트는 폴링으로 조회 +end + +== 예외 처리 == +note over Handler, Producer +**AI API 장애 시** +- Circuit Breaker Open +- Fallback: 기본 트렌드 템플릿 사용 +- Job 상태: COMPLETED (안내 메시지 포함) +- 사용자에게 "AI 분석이 제한적으로 제공됩니다" 안내 + +**Timeout (5분 초과)** +- Circuit Breaker로 즉시 실패 +- Retry 없음 (비동기 Job) +- Job 상태: FAILED +- 사용자에게 재시도 요청 안내 + +**Kafka 메시지 처리 실패** +- DLQ(Dead Letter Queue)로 이동 +- 수동 검토 및 재처리 +- 에러 로그 기록 + +**Redis 장애** +- 캐싱 스킵 +- Job 상태는 메모리에 임시 저장 +- 성능 저하 가능 (매 요청마다 AI API 호출) + +**성능 목표** +- 평균 응답 시간: 2분 이내 +- P95 응답 시간: 4분 이내 +- Circuit Breaker Timeout: 5분 +- Redis 캐시 TTL: 24시간 + +**데이터 처리 원칙** +- 과거 이벤트 데이터 사용 안 함 +- 외부 AI API로 실시간 트렌드 분석 +- 업종/지역 기반 일반적 마케팅 트렌드 활용 +end note + +@enduml diff --git a/design/backend/sequence/inner/analytics-대시보드조회.puml b/design/backend/sequence/inner/analytics-대시보드조회.puml new file mode 100644 index 0000000..785eaa7 --- /dev/null +++ b/design/backend/sequence/inner/analytics-대시보드조회.puml @@ -0,0 +1,342 @@ +@startuml analytics-대시보드조회 +!theme mono + +title Analytics Service - 대시보드 조회 내부 시퀀스\n(UFR-ANAL-010: 실시간 성과분석 대시보드 조회) + +participant "AnalyticsController" as Controller +participant "AnalyticsService" as Service +participant "CacheService" as Cache +participant "AnalyticsRepository" as Repository +participant "ExternalChannelService" as ChannelService +participant "ROICalculator" as Calculator +participant "CircuitBreaker" as CB +participant "Redis<>" as Redis +database "Analytics DB<>" as DB + +-> Controller: GET /api/events/{id}/analytics\n+ Authorization: Bearer {token} +activate Controller + +Controller -> Service: getDashboardData(eventId, userId) +activate Service + +note right of Service + **입력 검증** + - eventId: UUID 형식 검증 + - userId: JWT에서 추출 + - 권한 확인: 매장 소유자 여부 +end note + +Service -> Cache: get("analytics:dashboard:{eventId}") +activate Cache + +note right of Cache + **Cache-Aside 패턴** + - Redis GET 호출 + - Cache Key 구조: + analytics:dashboard:{eventId} + - TTL: 3600초 (1시간) +end note + +Cache -> Redis: GET analytics:dashboard:{eventId} +activate Redis + +alt Cache HIT + + Redis --> Cache: **Cache HIT**\n캐시된 데이터 반환\n{\n totalParticipants: 1234,\n totalViews: 17200,\n roi: 250,\n channelStats: [...],\n lastUpdated: "2025-10-22T10:30:00Z"\n} + deactivate Redis + + Cache --> Service: Dashboard 데이터 (JSON) + deactivate Cache + + note right of Service + **응답 데이터 구조** + - 4개 요약 카드 + * 총 참여자 수, 달성률 + * 총 노출 수, 증감률 + * 예상 ROI, 업종 평균 대비 + * 매출 증가율 + - 채널별 성과 + - 시간대별 참여 추이 + - 참여자 프로필 분석 + - 비교 분석 (업종 평균, 이전 이벤트) + end note + + Service --> Controller: DashboardResponse\n(200 OK) + deactivate Service + + Controller --> : 200 OK\nDashboard Data (JSON) + deactivate Controller + + note over Controller, Redis + **Cache HIT 시나리오 성능** + - 응답 시간: 약 0.5초 + - Redis 조회 시간: 0.01초 + - 직렬화/역직렬화: 0.05초 + - HTTP 오버헤드: 0.44초 + - 예상 히트율: 95% + end note + +else Cache MISS + + Redis --> Cache: **Cache MISS** (null) + deactivate Redis + + Cache --> Service: null (캐시 미스) + deactivate Cache + + note right of Service + **Cache MISS 처리** + - 데이터 통합 작업 시작 + - 로컬 DB 조회 + 외부 API 병렬 호출 + end note + + ||| + == 1. Analytics DB 조회 (로컬 데이터) == + + Service -> Repository: getEventStats(eventId) + activate Repository + + Repository -> DB: 이벤트 통계 조회\n(이벤트ID로 통계 데이터 조회) + activate DB + + DB --> Repository: EventStatsEntity\n- totalParticipants\n- estimatedROI\n- salesGrowthRate + deactivate DB + + Repository --> Service: EventStats + deactivate Repository + + note right of Service + **로컬 데이터 확보** + - 총 참여자 수 + - 예상 ROI (DB 캐시) + - 매출 증가율 (POS 연동) + end note + + ||| + == 2. 외부 채널 API 병렬 호출 (Circuit Breaker 적용) == + + note right of Service + **병렬 처리 시작** + - CompletableFuture 4개 생성 + - 우리동네TV, 지니TV, 링고비즈, SNS APIs 동시 호출 + - Circuit Breaker 적용 (채널별 독립) + end note + + par 외부 API 병렬 호출 + + Service -> ChannelService: getWooriTVStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("wooriTV", () -> callAPI()) + activate CB + + note right of CB + **Circuit Breaker** + - State: CLOSED (정상) + - Failure Rate: 50% 초과 시 OPEN + - Timeout: 10초 + end note + + CB -> CB: 외부 API 호출\nGET /stats/{eventId} + + alt Circuit Breaker CLOSED (정상) + CB --> ChannelService: ChannelStats\n- views: 5000\n- clicks: 1200 + deactivate CB + + ChannelService --> Service: WooriTVStats + deactivate ChannelService + else Circuit Breaker OPEN (장애) + CB -> CB: **Fallback 실행**\n캐시된 이전 데이터 반환 + note right of CB + Fallback 전략: + - Redis에서 이전 통계 조회 + - 없으면 기본값 (0) 반환 + - 알림: "일부 채널 데이터 로딩 실패" + end note + CB --> ChannelService: Fallback 데이터 + deactivate CB + ChannelService --> Service: WooriTVStats (Fallback) + deactivate ChannelService + end + + else + + Service -> ChannelService: getGenieTVStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("genieTV", () -> callAPI()) + activate CB + + CB -> CB: 외부 API 호출\nGET /campaign/{id}/stats + + alt 정상 응답 + CB --> ChannelService: ChannelStats\n- adViews: 10000\n- clicks: 500 + deactivate CB + ChannelService --> Service: GenieTVStats + deactivate ChannelService + else Timeout (10초 초과) + CB -> CB: **Timeout 처리**\n기본값 반환 + note right of CB + Timeout 발생: + - 리소스 점유 방지 + - Fallback으로 기본값 (0) 설정 + - 알림: "지니TV 데이터 로딩 지연" + end note + CB --> ChannelService: 기본값 (0) + deactivate CB + ChannelService --> Service: GenieTVStats (기본값) + deactivate ChannelService + end + + else + + Service -> ChannelService: getRingoBizStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("ringoBiz", () -> callAPI()) + activate CB + + note right of CB + **Circuit Breaker** + - State: CLOSED (정상) + - Failure Rate: 50% 초과 시 OPEN + - Timeout: 10초 + end note + + CB -> CB: 외부 API 호출\nGET /voice-stats/{eventId} + + alt 정상 응답 + CB --> ChannelService: ChannelStats\n- calls: 3000\n- completed: 2500\n- avgDuration: 45초 + deactivate CB + ChannelService --> Service: RingoBizStats + deactivate ChannelService + else Timeout 또는 장애 + CB -> CB: **Fallback 실행**\n기본값 반환 + note right of CB + 링고비즈 API 장애: + - 기본값 (0) 반환 + - 알림: "링고비즈 데이터 로딩 실패" + end note + CB --> ChannelService: 기본값 (0) + deactivate CB + ChannelService --> Service: RingoBizStats (기본값) + deactivate ChannelService + end + + else + + Service -> ChannelService: getSNSStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("SNS", () -> callAPIs()) + activate CB + + note right of CB + **SNS APIs 통합 호출** + - Instagram API + - Naver Blog API + - Kakao Channel API + - 3개 API 병렬 호출 + end note + + CB -> CB: 외부 APIs 호출\n(Instagram, Naver, Kakao) + + alt 정상 응답 + CB --> ChannelService: SNSStats\n- Instagram: likes 300, comments 50\n- Naver: views 2000\n- Kakao: shares 100 + deactivate CB + ChannelService --> Service: SNSStats + deactivate ChannelService + else 장애 또는 Timeout + CB -> CB: **Fallback 실행**\n기본값 반환 + note right of CB + SNS API 장애: + - 기본값 (0) 반환 + - 알림: "SNS 데이터 로딩 실패" + end note + CB --> ChannelService: 기본값 (0) + deactivate CB + ChannelService --> Service: SNSStats (기본값) + deactivate ChannelService + end + + end + + ||| + == 3. 데이터 통합 및 ROI 계산 == + + Service -> Service: mergeChannelStats(\n wooriTV, genieTV, ringoBiz, sns\n) + + note right of Service + **데이터 통합** + - 총 노출 수 = 외부 채널 노출 합계 + - 총 참여자 수 = Analytics DB + - 채널별 전환율 = 참여자 수 / 노출 수 + - 링고비즈: 통화 완료 수 포함 + end note + + Service -> Calculator: calculateROI(\n eventStats, channelStats\n) + activate Calculator + + note right of Calculator + **ROI 계산 로직** + 총 비용 = 경품 비용 + 플랫폼 비용 + 예상 수익 = 매출 증가액 + 신규 고객 LTV + ROI = (수익 - 비용) / 비용 × 100 + end note + + Calculator --> Service: ROIData\n- roi: 250%\n- totalCost: 100만원\n- totalRevenue: 350만원\n- breakEvenPoint: 달성 + deactivate Calculator + + Service -> Service: buildDashboardData(\n eventStats, channelStats, roiData\n) + + note right of Service + **대시보드 데이터 구조 생성** + - 4개 요약 카드 + - 채널별 성과 차트 데이터 + - 시간대별 참여 추이 + - 참여자 프로필 분석 + - 비교 분석 (업종 평균, 이전 이벤트) + end note + + ||| + == 4. Redis 캐싱 및 응답 == + + Service -> Cache: set(\n "analytics:dashboard:{eventId}",\n dashboardData,\n TTL=3600\n) + activate Cache + + Cache -> Redis: 캐시 저장\nSET analytics:dashboard:{eventId}\nvalue={통합 데이터}\nEX 3600 (1시간) + activate Redis + + Redis --> Cache: OK (저장 완료) + deactivate Redis + + Cache --> Service: OK (캐싱 완료) + deactivate Cache + + note right of Service + **캐싱 완료** + - TTL: 3600초 (1시간) + - 다음 조회 시 Cache HIT + - 예상 크기: 5KB + - 갱신 주기: 1시간마다 새 데이터 조회 + end note + + Service --> Controller: DashboardResponse\n(200 OK) + deactivate Service + + Controller --> : 200 OK\nDashboard Data (JSON) + deactivate Controller + + note over Controller, DB + **Cache MISS 시나리오 성능** + - 응답 시간: 약 3초 + - Analytics DB 조회: 0.1초 + - 외부 API 병렬 호출: 2초 (병렬 처리) + - ROI 계산: 0.05초 + - Redis 캐싱: 0.01초 + - 직렬화/HTTP: 0.84초 + end note + +end + +@enduml diff --git a/design/backend/sequence/inner/analytics-배포완료구독.puml b/design/backend/sequence/inner/analytics-배포완료구독.puml new file mode 100644 index 0000000..11eb5fb --- /dev/null +++ b/design/backend/sequence/inner/analytics-배포완료구독.puml @@ -0,0 +1,168 @@ +@startuml analytics-배포완료구독 +!theme mono + +title Analytics Service - DistributionCompleted 이벤트 구독 처리 내부 시퀀스\n(Kafka Event 구독) + +participant "Kafka Consumer" as Consumer +participant "DistributionCompletedListener" as Listener +participant "AnalyticsService" as Service +participant "AnalyticsRepository" as Repository +participant "CacheService" as Cache +participant "Redis" as Redis +database "Analytics DB" as DB + +note over Consumer + **Kafka Consumer 설정** + - Topic: DistributionCompleted + - Consumer Group: analytics-service + - Partition Key: eventId + - At-Least-Once Delivery 보장 +end note + +Kafka -> Consumer: DistributionCompleted 이벤트 수신\n{\n eventId: "uuid",\n distributedChannels: [\n {\n channel: "우리동네TV",\n status: "SUCCESS",\n expectedViews: 5000\n },\n {\n channel: "지니TV",\n status: "SUCCESS",\n expectedViews: 10000\n },\n {\n channel: "Instagram",\n status: "SUCCESS",\n expectedViews: 2000\n }\n ],\n completedAt: "2025-10-22T12:00:00Z"\n} +activate Consumer + +Consumer -> Listener: onDistributionCompleted(event) +activate Listener + +note right of Listener + **멱등성 체크** + - Redis Set에 이벤트 ID 존재 여부 확인 + - 중복 처리 방지 + - Key: distribution_completed:{eventId} +end note + +Listener -> Redis: SISMEMBER distribution_completed {eventId} +activate Redis + +alt 이벤트 미처리 (멱등성 보장) + Redis --> Listener: false (미처리) + deactivate Redis + + Listener -> Service: updateDistributionStats(event) + activate Service + + note right of Service + **배포 채널 통계 저장** + - 채널별 배포 상태 기록 + - 예상 노출 수 집계 + - 배포 완료 시각 기록 + end note + + Service -> Service: parseChannelStats(event) + + note right of Service + **채널 데이터 파싱** + - distributedChannels 배열 순회 + - 각 채널별 통계 추출 + - 총 예상 노출 수 계산 + end note + + loop 각 채널별로 + Service -> Repository: saveChannelStats(\n eventId, channel, stats\n) + activate Repository + + Repository -> DB: 채널별 통계 저장\n(이벤트ID, 채널명, 상태,\n예상노출수, 배포일시 저장,\n중복 시 업데이트) + activate DB + + DB --> Repository: 1 row inserted/updated + deactivate DB + + Repository --> Service: ChannelStatsEntity + deactivate Repository + end + + note right of Service + **배포 통계 저장 완료** + - 채널별 배포 상태 기록 + - 예상 노출 수 저장 + - 향후 외부 API 조회 시 기준 데이터로 활용 + end note + + Service -> Repository: updateTotalViews(eventId, totalViews) + activate Repository + + Repository -> DB: 총 노출 수 업데이트\n(총 예상 노출 수를 설정하고,\n수정일시를 현재 시각으로 업데이트) + activate DB + + DB --> Repository: 1 row updated + deactivate DB + + Repository --> Service: UpdateResult (success) + deactivate Repository + + note right of Service + **이벤트 통계 업데이트** + - 총 예상 노출 수 업데이트 + - 다음 대시보드 조회 시 반영 + end note + + Service -> Cache: delete("analytics:dashboard:{eventId}") + activate Cache + + note right of Cache + **캐시 무효화** + - 기존 캐시 삭제 + - 다음 조회 시 최신 배포 통계 반영 + - 채널별 성과 차트 갱신 + end note + + Cache -> Redis: DEL analytics:dashboard:{eventId} + activate Redis + + Redis --> Cache: OK + deactivate Redis + + Cache --> Service: OK + deactivate Cache + + Service -> Redis: SADD distribution_completed {eventId} + activate Redis + + note right of Redis + **멱등성 처리 완료 기록** + - Redis Set에 eventId 추가 + - TTL 설정 (7일) + end note + + Redis --> Service: OK + deactivate Redis + + Service --> Listener: 배포 통계 업데이트 완료 + deactivate Service + + Listener -> Consumer: ACK (처리 완료) + deactivate Listener + +else 이벤트 이미 처리됨 (중복) + Redis --> Listener: true (이미 처리) + deactivate Redis + + note right of Listener + **중복 이벤트 스킵** + - At-Least-Once Delivery로 인한 중복 + - 멱등성 보장으로 중복 처리 방지 + end note + + Listener -> Consumer: ACK (스킵) + deactivate Listener +end + +Consumer --> Kafka: Commit Offset +deactivate Consumer + +note over Consumer, DB + **처리 시간** + - 이벤트 수신 → 통계 업데이트 완료: 약 0.3초 + - 채널별 DB INSERT (3개): 0.15초 + - event_stats UPDATE: 0.05초 + - Redis 캐시 무효화: 0.01초 + - 멱등성 체크: 0.01초 + + **배포 통계 효과** + - 배포 완료 즉시 통계 반영 + - 채널별 성과 추적 가능 + - 다음 대시보드 조회 시 최신 배포 정보 제공 +end note + +@enduml diff --git a/design/backend/sequence/inner/analytics-이벤트생성구독.puml b/design/backend/sequence/inner/analytics-이벤트생성구독.puml new file mode 100644 index 0000000..cad097b --- /dev/null +++ b/design/backend/sequence/inner/analytics-이벤트생성구독.puml @@ -0,0 +1,134 @@ +@startuml analytics-이벤트생성구독 +!theme mono + +title Analytics Service - EventCreated 이벤트 구독 처리 내부 시퀀스\n(Kafka Event 구독) + +participant "Kafka Consumer" as Consumer +participant "EventCreatedListener" as Listener +participant "AnalyticsService" as Service +participant "AnalyticsRepository" as Repository +participant "CacheService" as Cache +participant "Redis" as Redis +database "Analytics DB" as DB + +note over Consumer + **Kafka Consumer 설정** + - Topic: EventCreated + - Consumer Group: analytics-service + - Partition Key: eventId + - At-Least-Once Delivery 보장 +end note + +Kafka -> Consumer: EventCreated 이벤트 수신\n{\n eventId: "uuid",\n storeId: "uuid",\n title: "이벤트 제목",\n objective: "신규 고객 유치",\n createdAt: "2025-10-22T10:00:00Z"\n} +activate Consumer + +Consumer -> Listener: onEventCreated(event) +activate Listener + +note right of Listener + **멱등성 체크** + - Redis Set에 이벤트 ID 존재 여부 확인 + - 중복 처리 방지 +end note + +Listener -> Redis: SISMEMBER processed_events {eventId} +activate Redis + +alt 이벤트 미처리 (멱등성 보장) + Redis --> Listener: false (미처리) + deactivate Redis + + Listener -> Service: initializeEventStats(event) + activate Service + + note right of Service + **이벤트 통계 초기화** + - 이벤트 기본 정보 저장 + - 통계 초기값 설정 + * 총 참여자 수: 0 + * 총 노출 수: 0 + * 예상 ROI: 계산 전 + * 매출 증가율: 0% + end note + + Service -> Repository: save(eventStatsEntity) + activate Repository + + Repository -> DB: 이벤트 통계 초기화\n(이벤트ID, 매장ID, 제목, 목적,\n참여자수/노출수/ROI/매출증가율을\n0으로 초기화하여 저장) + activate DB + + DB --> Repository: 1 row inserted + deactivate DB + + Repository --> Service: EventStatsEntity + deactivate Repository + + note right of Service + **초기화 완료** + - 이벤트 통계 DB 생성 + - 향후 ParticipantRegistered 이벤트 수신 시 + 실시간 증가 + end note + + Service -> Cache: delete("analytics:dashboard:{eventId}") + activate Cache + + note right of Cache + **캐시 무효화** + - 기존 캐시 삭제 + - 다음 조회 시 최신 데이터 갱신 + end note + + Cache -> Redis: DEL analytics:dashboard:{eventId} + activate Redis + + Redis --> Cache: OK + deactivate Redis + + Cache --> Service: OK + deactivate Cache + + Service -> Redis: SADD processed_events {eventId} + activate Redis + + note right of Redis + **멱등성 처리 완료 기록** + - Redis Set에 eventId 추가 + - TTL 설정 (7일) + end note + + Redis --> Service: OK + deactivate Redis + + Service --> Listener: EventStats 초기화 완료 + deactivate Service + + Listener -> Consumer: ACK (처리 완료) + deactivate Listener + +else 이벤트 이미 처리됨 (중복) + Redis --> Listener: true (이미 처리) + deactivate Redis + + note right of Listener + **중복 이벤트 스킵** + - At-Least-Once Delivery로 인한 중복 + - 멱등성 보장으로 중복 처리 방지 + end note + + Listener -> Consumer: ACK (스킵) + deactivate Listener +end + +Consumer --> Kafka: Commit Offset +deactivate Consumer + +note over Consumer, DB + **처리 시간** + - 이벤트 수신 → 초기화 완료: 약 0.2초 + - DB INSERT: 0.05초 + - Redis 캐시 무효화: 0.01초 + - 멱등성 체크: 0.01초 +end note + +@enduml diff --git a/design/backend/sequence/inner/analytics-참여자등록구독.puml b/design/backend/sequence/inner/analytics-참여자등록구독.puml new file mode 100644 index 0000000..aa2d680 --- /dev/null +++ b/design/backend/sequence/inner/analytics-참여자등록구독.puml @@ -0,0 +1,135 @@ +@startuml analytics-참여자등록구독 +!theme mono + +title Analytics Service - ParticipantRegistered 이벤트 구독 처리 내부 시퀀스\n(Kafka Event 구독) + +participant "Kafka Consumer" as Consumer +participant "ParticipantRegisteredListener" as Listener +participant "AnalyticsService" as Service +participant "AnalyticsRepository" as Repository +participant "CacheService" as Cache +participant "Redis" as Redis +database "Analytics DB" as DB + +note over Consumer + **Kafka Consumer 설정** + - Topic: ParticipantRegistered + - Consumer Group: analytics-service + - Partition Key: eventId + - At-Least-Once Delivery 보장 +end note + +Kafka -> Consumer: ParticipantRegistered 이벤트 수신\n{\n participantId: "uuid",\n eventId: "uuid",\n phoneNumber: "010-1234-5678",\n registeredAt: "2025-10-22T11:30:00Z"\n} +activate Consumer + +Consumer -> Listener: onParticipantRegistered(event) +activate Listener + +note right of Listener + **멱등성 체크** + - Redis Set에 participantId 존재 여부 확인 + - 중복 처리 방지 +end note + +Listener -> Redis: SISMEMBER processed_participants {participantId} +activate Redis + +alt 이벤트 미처리 (멱등성 보장) + Redis --> Listener: false (미처리) + deactivate Redis + + Listener -> Service: updateParticipantCount(eventId) + activate Service + + note right of Service + **참여자 수 실시간 증가** + - DB UPDATE로 참여자 수 증가 + - 캐시 무효화로 다음 조회 시 최신 데이터 반영 + end note + + Service -> Repository: incrementParticipantCount(eventId) + activate Repository + + Repository -> DB: 참여자 수 증가\n(참여자 수를 1 증가시키고,\n수정일시를 현재 시각으로 업데이트) + activate DB + + DB --> Repository: 1 row updated + deactivate DB + + Repository --> Service: UpdateResult (success) + deactivate Repository + + note right of Service + **실시간 통계 업데이트 완료** + - 참여자 수 +1 + - 다음 대시보드 조회 시 최신 통계 반영 + end note + + Service -> Cache: delete("analytics:dashboard:{eventId}") + activate Cache + + note right of Cache + **캐시 무효화** + - 기존 캐시 삭제 + - 다음 조회 시 최신 참여자 수 반영 + - Cache MISS 시 DB 조회로 최신 데이터 확보 + end note + + Cache -> Redis: DEL analytics:dashboard:{eventId} + activate Redis + + Redis --> Cache: OK + deactivate Redis + + Cache --> Service: OK + deactivate Cache + + Service -> Redis: SADD processed_participants {participantId} + activate Redis + + note right of Redis + **멱등성 처리 완료 기록** + - Redis Set에 participantId 추가 + - TTL 설정 (7일) + end note + + Redis --> Service: OK + deactivate Redis + + Service --> Listener: 참여자 수 업데이트 완료 + deactivate Service + + Listener -> Consumer: ACK (처리 완료) + deactivate Listener + +else 이벤트 이미 처리됨 (중복) + Redis --> Listener: true (이미 처리) + deactivate Redis + + note right of Listener + **중복 이벤트 스킵** + - At-Least-Once Delivery로 인한 중복 + - 멱등성 보장으로 중복 처리 방지 + end note + + Listener -> Consumer: ACK (스킵) + deactivate Listener +end + +Consumer --> Kafka: Commit Offset +deactivate Consumer + +note over Consumer, DB + **처리 시간** + - 이벤트 수신 → 통계 업데이트 완료: 약 0.15초 + - DB UPDATE: 0.05초 + - Redis 캐시 무효화: 0.01초 + - 멱등성 체크: 0.01초 + + **실시간 업데이트 효과** + - 참여자 등록 즉시 통계 반영 + - 다음 대시보드 조회 시 최신 데이터 제공 + - Cache-Aside 패턴으로 성능 유지 +end note + +@enduml diff --git a/design/backend/sequence/inner/content-이미지결과조회.puml b/design/backend/sequence/inner/content-이미지결과조회.puml new file mode 100644 index 0000000..c9265e9 --- /dev/null +++ b/design/backend/sequence/inner/content-이미지결과조회.puml @@ -0,0 +1,140 @@ +@startuml event-이미지결과조회 +!theme mono + +title Content Service - 이미지 생성 결과 폴링 조회 + +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "JobManager" as JobMgr <> +participant "Redis Cache" as Cache <> + +note over Controller, Cache +**폴링 방식 Job 상태 조회** +- 최대 30초 동안 폴링 (2초 간격) +- Job 상태: PENDING → PROCESSING → COMPLETED +- 이미지 URL: Redis에 저장 (TTL: 7일) +end note + +Client -> Gateway: GET /api/content/jobs/{jobId}/status +activate Gateway + +Gateway -> Controller: GET /api/content/jobs/{jobId}/status +activate Controller + +Controller -> Service: getJobStatus(jobId) +activate Service + +Service -> JobMgr: getJobStatus(jobId) +activate JobMgr + +JobMgr -> Cache: Job 상태 조회\nKey: job:{jobId} +activate Cache + +alt Job 데이터 존재 + Cache --> JobMgr: Job 데이터\n{status, eventDraftId,\ntype, createdAt} + deactivate Cache + + alt status = COMPLETED + JobMgr -> Cache: 이미지 URL 조회\nKey: content:image:{eventDraftId} + activate Cache + Cache --> JobMgr: 이미지 URL\n{simple, fancy, trendy} + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nimageUrls: {...}} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: COMPLETED, imageUrls} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"imageUrls": {\n "simple": "https://cdn.../simple.png",\n "fancy": "https://cdn.../fancy.png",\n "trendy": "https://cdn.../trendy.png"\n}} + deactivate Controller + + Gateway --> Client: 200 OK\n이미지 URL 반환 + deactivate Gateway + + note right of Client + **프론트엔드 처리** + - 3가지 스타일 카드 표시 + - 사용자가 스타일 선택 + - 이미지 편집 가능 + end note + + else status = PROCESSING 또는 PENDING + JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: PROCESSING} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "이미지 생성 중입니다"} + deactivate Controller + + Gateway --> Client: 200 OK\n진행 중 상태 + deactivate Gateway + + note right of Client + **폴링 재시도** + - 2초 후 재요청 + - 최대 30초 (15회) + end note + + else status = FAILED + JobMgr -> Cache: 에러 정보 조회\nKey: job:{jobId}:error + activate Cache + Cache --> JobMgr: 에러 메시지 + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: FAILED, error} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "이미지 생성 실패",\n"message": "다시 시도해주세요"} + deactivate Controller + + Gateway --> Client: 200 OK\n실패 상태 + deactivate Gateway + + note right of Client + **실패 처리** + - 에러 메시지 표시 + - "다시 생성" 버튼 제공 + end note + end + +else Job 데이터 없음 + Cache --> JobMgr: null (캐시 미스) + deactivate Cache + + JobMgr --> Service: throw NotFoundException\n("Job을 찾을 수 없습니다") + deactivate JobMgr + + Service --> Controller: NotFoundException + deactivate Service + + Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job을 찾을 수 없습니다"} + deactivate Controller + + Gateway --> Client: 404 Not Found + deactivate Gateway +end + +note over Controller, Cache +**폴링 전략** +- 간격: 2초 +- 최대 시간: 30초 (15회) +- Timeout 시: 사용자에게 알림 + "다시 생성" 옵션 + +**Redis 캐시** +- Job 상태: TTL 1시간 +- 이미지 URL: TTL 7일 + +**성능 목표** +- 평균 이미지 생성 시간: 20초 이내 +- P95 이미지 생성 시간: 40초 이내 +end note + +@enduml diff --git a/design/backend/sequence/inner/content-이미지생성.puml b/design/backend/sequence/inner/content-이미지생성.puml new file mode 100644 index 0000000..f00284d --- /dev/null +++ b/design/backend/sequence/inner/content-이미지생성.puml @@ -0,0 +1,255 @@ +@startuml content-이미지생성 +!theme mono + +title Content Service - 이미지 생성 내부 시퀀스 (UFR-CONT-010) + +actor Client +participant "Kafka\nimage-job\nConsumer" as Consumer +participant "JobHandler" as Handler +participant "CacheManager" as Cache +participant "ImageGenerator" as Generator +participant "ImageStyleFactory" as Factory +participant "StableDiffusion\nAPI Client" as SDClient +participant "DALL-E\nAPI Client" as DALLEClient +participant "Circuit Breaker" as CB +participant "BlobStorage\nUploader" as BlobStorage +participant "JobStatusManager" as JobStatus +database "Redis Cache" as Redis + +note over Consumer: Kafka 구독\nimage-job 토픽 + +== Kafka Job 수신 == +Consumer -> Handler: Job Message 수신\n{jobId, eventDraftId, eventInfo} +activate Handler + +Handler -> Cache: 캐시 조회\nkey: content:image:{eventDraftId} +activate Cache +Cache -> Redis: GET content:image:{eventDraftId} +Redis --> Cache: 캐시 데이터 또는 NULL +Cache --> Handler: 캐시 결과 +deactivate Cache + +alt 캐시 HIT (기존 이미지 존재) + Handler -> JobStatus: Job 상태 업데이트\nstatus: COMPLETED (캐시) + activate JobStatus + JobStatus -> Redis: SET job:{jobId}\n{status: COMPLETED, imageUrls: [...]} + JobStatus --> Handler: 업데이트 완료 + deactivate JobStatus + Handler --> Consumer: 처리 완료 (캐시) +else 캐시 MISS (새로운 이미지 생성) + Handler -> JobStatus: Job 상태 업데이트\nstatus: PROCESSING + activate JobStatus + JobStatus -> Redis: SET job:{jobId}\n{status: PROCESSING} + JobStatus --> Handler: 업데이트 완료 + deactivate JobStatus + + Handler -> Generator: 3가지 스타일 이미지 생성 요청\n{eventInfo} + activate Generator + + == 3가지 스타일 병렬 생성 (par 블록) == + group parallel + Generator -> Factory: 심플 프롬프트 생성\n{eventInfo, style: SIMPLE} + activate Factory + Factory --> Generator: 심플 프롬프트 + deactivate Factory + + Generator -> CB: Circuit Breaker 체크 + activate CB + CB --> Generator: State: CLOSED (정상) + deactivate CB + + Generator -> SDClient: Stable Diffusion API 호출\n{prompt, style: SIMPLE}\nTimeout: 20초 + activate SDClient + + note over SDClient: Circuit Breaker 적용\nRetry: 최대 3회\nTimeout: 20초 + + alt API 성공 + SDClient --> Generator: 심플 이미지 데이터 + deactivate SDClient + Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: SIMPLE}\nRetry: 3회, Timeout: 30초 + activate BlobStorage + note right of BlobStorage: SAS Token 생성\n(유효기간 7일) + BlobStorage --> Generator: Blob SAS URL (심플) + deactivate BlobStorage + else API 실패 (Timeout/Error) + SDClient --> Generator: 실패 응답 + deactivate SDClient + Generator -> CB: 실패 기록 + activate CB + CB -> CB: 실패율 계산 + alt 실패율 > 50% + CB -> CB: Circuit State: OPEN + end + CB --> Generator: Circuit State + deactivate CB + + Generator -> DALLEClient: Fallback - DALL-E API 호출\n{prompt, style: SIMPLE}\nTimeout: 20초 + activate DALLEClient + alt Fallback 성공 + DALLEClient --> Generator: 심플 이미지 데이터 + deactivate DALLEClient + Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: SIMPLE}\nRetry: 3회, Timeout: 30초 + activate BlobStorage + note right of BlobStorage: SAS Token 생성\n(유효기간 7일) + BlobStorage --> Generator: Blob SAS URL (심플) + deactivate BlobStorage + else Fallback 실패 + DALLEClient --> Generator: 실패 응답 + deactivate DALLEClient + Generator -> Generator: 기본 템플릿 사용\n(심플) + end + end + + Generator -> Factory: 화려한 프롬프트 생성\n{eventInfo, style: FANCY} + activate Factory + Factory --> Generator: 화려한 프롬프트 + deactivate Factory + + Generator -> CB: Circuit Breaker 체크 + activate CB + CB --> Generator: State: CLOSED/OPEN + deactivate CB + + alt Circuit CLOSED + Generator -> SDClient: Stable Diffusion API 호출\n{prompt, style: FANCY}\nTimeout: 20초 + activate SDClient + + alt API 성공 + SDClient --> Generator: 화려한 이미지 데이터 + deactivate SDClient + Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: FANCY}\nRetry: 3회, Timeout: 30초 + activate BlobStorage + note right of BlobStorage: SAS Token 생성\n(유효기간 7일) + BlobStorage --> Generator: Blob SAS URL (화려한) + deactivate BlobStorage + else API 실패 + SDClient --> Generator: 실패 응답 + deactivate SDClient + Generator -> DALLEClient: Fallback - DALL-E API 호출 + activate DALLEClient + alt Fallback 성공 + DALLEClient --> Generator: 화려한 이미지 데이터 + deactivate DALLEClient + Generator -> BlobStorage: Blob 업로드\n{imageData, eventId, style: FANCY}\nRetry: 3회, Timeout: 30초 + activate BlobStorage + note right of BlobStorage: SAS Token 생성\n(유효기간 7일) + BlobStorage --> Generator: Blob SAS URL (화려한) + deactivate BlobStorage + else Fallback 실패 + DALLEClient --> Generator: 실패 응답 + deactivate DALLEClient + Generator -> Generator: 기본 템플릿 사용\n(화려한) + end + end + else Circuit OPEN + Generator -> Generator: Circuit Open\n즉시 기본 템플릿 사용 + end + + Generator -> Factory: 트렌디 프롬프트 생성\n{eventInfo, style: TRENDY} + activate Factory + Factory --> Generator: 트렌디 프롬프트 + deactivate Factory + + Generator -> CB: Circuit Breaker 체크 + activate CB + CB --> Generator: State: CLOSED/OPEN + deactivate CB + + alt Circuit CLOSED + Generator -> SDClient: Stable Diffusion API 호출\n{prompt, style: TRENDY}\nTimeout: 20초 + activate SDClient + + alt API 성공 + SDClient --> Generator: 트렌디 이미지 데이터 + deactivate SDClient + Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: TRENDY}\nRetry: 3회, Timeout: 30초 + activate BlobStorage + note right of BlobStorage: SAS Token 생성\n(유효기간 7일) + BlobStorage --> Generator: Blob SAS URL (트렌디) + deactivate BlobStorage + else API 실패 + SDClient --> Generator: 실패 응답 + deactivate SDClient + Generator -> DALLEClient: Fallback - DALL-E API 호출 + activate DALLEClient + alt Fallback 성공 + DALLEClient --> Generator: 트렌디 이미지 데이터 + deactivate DALLEClient + Generator -> BlobStorage: Blob 업로드\n{imageData, eventId, style: TRENDY}\nRetry: 3회, Timeout: 30초 + activate BlobStorage + note right of BlobStorage: SAS Token 생성\n(유효기간 7일) + BlobStorage --> Generator: Blob SAS URL (트렌디) + deactivate BlobStorage + else Fallback 실패 + DALLEClient --> Generator: 실패 응답 + deactivate DALLEClient + Generator -> Generator: 기본 템플릿 사용\n(트렌디) + end + end + else Circuit OPEN + Generator -> Generator: Circuit Open\n즉시 기본 템플릿 사용 + end + end + + Generator --> Handler: 3가지 이미지 URL 반환\n{simple, fancy, trendy} + deactivate Generator + + == 결과 캐싱 및 Job 완료 == + Handler -> Cache: Blob SAS URL 캐싱\nkey: content:image:{eventDraftId}\nTTL: 7일 + activate Cache + Cache -> Redis: SET content:image:{eventDraftId}\n{simple: SAS_URL, fancy: SAS_URL, trendy: SAS_URL}\nTTL: 604800 (7일) + Redis --> Cache: 저장 완료 + Cache --> Handler: 캐싱 완료 + deactivate Cache + + Handler -> JobStatus: Job 상태 업데이트\nstatus: COMPLETED + activate JobStatus + JobStatus -> Redis: SET job:{jobId}\n{status: COMPLETED, imageUrls: [...]} + JobStatus --> Handler: 업데이트 완료 + deactivate JobStatus + + Handler --> Consumer: 처리 완료 + note over Handler + Blob SAS URL은 Redis에만 저장됨 + Event Service는 폴링을 통해 + Redis에서 결과 조회 + SAS Token 유효기간: 7일 + end note +end + +deactivate Handler + +note over Consumer, Redis +**Resilience 패턴 적용** +- Circuit Breaker: 실패율 50% 초과 시 Open (AI API용) +- AI API Timeout: 20초 +- Fallback: Stable Diffusion 실패 시 DALL-E, 모두 실패 시 기본 템플릿 +- Blob Storage Retry: 최대 3회 (Exponential Backoff: 1s, 2s, 4s) +- Blob Storage Timeout: 30초 (대용량 이미지 고려) +- Cache-Aside: Redis 캐싱 (TTL 7일) + +**처리 시간** +- 캐시 HIT: 0.1초 +- 캐시 MISS: 5.2초 이내 (병렬 처리) + └─ AI 생성: 3-5초 + Blob 업로드: 0.15-0.3초 + +**병렬 처리** +- 3가지 스타일 동시 생성 (par 블록) +- 독립적인 스레드 풀 사용 + +**Blob Storage 업로드** +- Azure Blob Storage (Korea Central) +- SAS Token 기반 접근 제어 (읽기 전용) +- SAS Token 유효기간: 7일 (Redis TTL과 동기화) +- Public Access 비활성화 (보안 강화) +- Container: event-images +- URL 형식: https://{account}.blob.core.windows.net/event-images/{id}-{style}.png?{sas_token} + +**보안** +- Storage Account Public Access 비활성화 +- SAS Token 기반 URL 생성 (읽기 전용 권한) +- Firewall 규칙: K8s Cluster IP만 허용 +- HTTPS 강제 (TLS 1.2 이상) +end note + +@enduml diff --git a/design/backend/sequence/inner/content-이미지생성요청.puml b/design/backend/sequence/inner/content-이미지생성요청.puml new file mode 100644 index 0000000..1d38c6a --- /dev/null +++ b/design/backend/sequence/inner/content-이미지생성요청.puml @@ -0,0 +1,90 @@ +@startuml event-이미지생성요청 +!theme mono + +title Content Service - 이미지 생성 요청 (UFR-CONT-010) + +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "JobManager" as JobMgr <> +participant "Redis Cache" as Cache <> + +note over Controller, Cache +**UFR-CONT-010: SNS 이미지 생성 요청** +- Kafka 사용 안 함 (내부 Job 관리) +- 백그라운드 워커가 비동기 처리 +- Redis에서 AI 추천 데이터 읽기 +- 3가지 스타일 이미지 생성 (심플, 화려한, 트렌디) +end note + +Client -> Gateway: POST /api/content/images/{eventDraftId}/generate +activate Gateway + +Gateway -> Controller: POST /api/content/images/{eventDraftId}/generate +activate Controller + +Controller -> Controller: 요청 검증\n(eventDraftId 유효성) + +Controller -> Service: generateImages(eventDraftId) +activate Service + +== 1단계: Redis에서 AI 추천 데이터 확인 == + +Service -> Cache: AI 추천 데이터 조회\nKey: ai:event:{eventDraftId} +activate Cache +Cache --> Service: AI 추천 결과\n{선택된 추천안, 이벤트 정보} +deactivate Cache + +alt AI 추천 데이터 없음 + Service --> Controller: throw NotFoundException\n("AI 추천을 먼저 선택해주세요") + Controller --> Gateway: 404 Not Found\n{"code": "CONTENT_001",\n"message": "AI 추천을 먼저 선택해주세요"} + deactivate Service + deactivate Controller + Gateway --> Client: 404 Not Found + deactivate Gateway + +else AI 추천 데이터 존재 + + == 2단계: Job 생성 == + + Service -> JobMgr: createJob(eventDraftId, imageGeneration) + activate JobMgr + + JobMgr -> JobMgr: Job ID 생성 (UUID) + + JobMgr -> Cache: Job 상태 저장\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: IMAGE_GEN,\ncreatedAt}\nTTL: 1시간 + activate Cache + Cache --> JobMgr: 저장 완료 + deactivate Cache + + JobMgr --> Service: Job 생성 완료\n{jobId, status: PENDING} + deactivate JobMgr + + == 3단계: 응답 반환 == + + Service --> Controller: JobResponse\n{jobId, status: PENDING} + deactivate Service + + Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "이미지 생성 중입니다"} + deactivate Controller + + Gateway --> Client: 202 Accepted\n이미지 생성 시작 + deactivate Gateway + + note over Service, Cache + **백그라운드 워커 처리** + - Redis 폴링 또는 스케줄러가 Job 감지 + - content-이미지생성.puml 참조 + - 외부 이미지 생성 API 호출 (병렬) + - Redis에 이미지 URL 저장 + + **상세 내용** + - 3가지 스타일 병렬 생성 (심플, 화려한, 트렌디) + - Circuit Breaker 적용 (Timeout: 5분) + - 결과: Redis Key: content:image:{eventDraftId} + - TTL: 7일 + end note +end + +@enduml diff --git a/design/backend/sequence/inner/distribution-다중채널배포.puml b/design/backend/sequence/inner/distribution-다중채널배포.puml new file mode 100644 index 0000000..6f5eea9 --- /dev/null +++ b/design/backend/sequence/inner/distribution-다중채널배포.puml @@ -0,0 +1,141 @@ +@startuml distribution-다중채널배포-sprint2 +!theme mono + +title Distribution Service - 다중 채널 배포 Sprint 2 (UFR-DIST-010) + +participant "Event Service" as EventSvc +participant "Distribution\nREST API" as API +participant "Distribution\nController" as Controller +participant "Distribution\nService" as Service +database "Distribution DB" as DB +queue "Kafka" as Kafka + +== REST API 동기 호출 수신 == +EventSvc -> API: POST /api/distribution/distribute\n{eventId, channels[], contentUrls} +activate API + +API -> Controller: distributeToChannels(request) +activate Controller + +Controller -> Service: executeDistribution(distributionRequest) +activate Service + +Service -> DB: 배포 이력 초기화\n(이벤트ID, 상태를 PENDING으로 저장) +DB --> Service: 배포 이력 ID + +note over Service: 배포 시작 상태로 변경 +Service -> DB: 배포 이력 상태 업데이트\n(상태를 IN_PROGRESS로 변경) + +== 다중 채널 배포 로그 기록 (Sprint 2: Mock 처리) == + +note over Service: Sprint 2: 실제 외부 API 호출 없이\n배포 결과만 기록 + +par 우리동네TV 배포 + alt 채널 선택됨 + Service -> Service: 우리동네TV 배포 처리\n(Mock: 즉시 성공 반환) + activate Service + + note over Service: 배포 요청 검증\n- eventId 유효성\n- contentUrls 존재 여부 + + Service -> DB: 배포 채널 로그 저장\n(채널: 우리동네TV,\n상태: 성공, 배포ID,\n예상노출수 저장) + + note over Service: Mock 결과:\n성공 (distributionId 생성) + + deactivate Service + end + + alt 링고비즈 선택됨 + Service -> Service: 링고비즈 배포 처리\n(Mock: 즉시 성공 반환) + activate Service + + note over Service: 배포 요청 검증\n- phoneNumber 형식\n- audioUrl 존재 여부 + + Service -> DB: 배포 채널 로그 저장\n(채널: 링고비즈,\n상태: 성공,\n업데이트 시각 저장) + + note over Service: Mock 결과:\n성공 (timestamp 기록) + + deactivate Service + end + + alt 지니TV 선택됨 + Service -> Service: 지니TV 배포 처리\n(Mock: 즉시 성공 반환) + activate Service + + note over Service: 배포 요청 검증\n- region 유효성\n- schedule 형식\n- budget 범위 + + Service -> DB: 배포 채널 로그 저장\n(채널: 지니TV,\n상태: 성공, 광고ID,\n노출 스케줄 저장) + + note over Service: Mock 결과:\n성공 (adId 생성) + + deactivate Service + end + + alt Instagram 선택됨 + Service -> Service: Instagram 배포 처리\n(Mock: 즉시 성공 반환) + activate Service + + note over Service: 배포 요청 검증\n- imageUrl 형식\n- caption 길이\n- hashtags 유효성 + + Service -> DB: 배포 채널 로그 저장\n(채널: Instagram,\n상태: 성공,\n포스트 URL/ID 저장) + + note over Service: Mock 결과:\n성공 (postUrl, postId 생성) + + deactivate Service + end + + alt Naver Blog 선택됨 + Service -> Service: Naver Blog 배포 처리\n(Mock: 즉시 성공 반환) + activate Service + + note over Service: 배포 요청 검증\n- imageUrl 형식\n- content 길이 + + Service -> DB: 배포 채널 로그 저장\n(채널: NaverBlog,\n상태: 성공,\n포스트 URL 저장) + + note over Service: Mock 결과:\n성공 (postUrl 생성) + + deactivate Service + end + + alt Kakao Channel 선택됨 + Service -> Service: Kakao Channel 배포 처리\n(Mock: 즉시 성공 반환) + activate Service + + note over Service: 배포 요청 검증\n- imageUrl 형식\n- message 길이 + + Service -> DB: 배포 채널 로그 저장\n(채널: KakaoChannel,\n상태: 성공,\n메시지 ID 저장) + + note over Service: Mock 결과:\n성공 (messageId 생성) + + deactivate Service + end +end + +note over Service: 모든 채널 배포 완료\n(즉시 처리 - 외부 API 호출 없음) + +== 배포 결과 집계 및 저장 == +Service -> Service: 채널별 배포 결과 집계\n성공: [선택된 모든 채널] + +note over Service: Sprint 2에서는\n모든 채널 배포가 성공으로 처리됨 + +Service -> DB: 배포 이력 상태 업데이트\n(상태를 COMPLETED로,\n완료일시를 현재 시각으로 설정) + +== Kafka 이벤트 발행 == +Service -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt} +note over Kafka: Analytics Service 구독\n실시간 통계 업데이트 + +== REST API 동기 응답 == +Service --> Controller: 배포 완료 응답\n{status: COMPLETED, successChannels: [all]} +deactivate Service + +Controller --> API: DistributionResponse\n{eventId, status: COMPLETED, results: [all success]} +deactivate Controller + +API --> EventSvc: 200 OK\n{distributionId, status: COMPLETED, results[]} +deactivate API + +note over EventSvc: 배포 완료 응답 수신\n이벤트 상태 업데이트\nAPPROVED → ACTIVE + +== Sprint 2 제약사항 == +note over Service: **Sprint 2 구현 범위**\n- 외부 API 호출 없음 (Mock 처리)\n- 모든 배포 요청은 성공으로 처리\n- 배포 로그만 DB에 기록\n- Circuit Breaker, Retry 미구현\n- 실패 처리 시나리오 미구현\n\n**Sprint 3 이후 구현 예정**\n- 실제 외부 채널 API 연동\n- Circuit Breaker 패턴 적용\n- Retry 로직 구현\n- 실패 처리 및 알림 + +@enduml diff --git a/design/backend/sequence/inner/event-AI추천요청.puml b/design/backend/sequence/inner/event-AI추천요청.puml new file mode 100644 index 0000000..f3baef0 --- /dev/null +++ b/design/backend/sequence/inner/event-AI추천요청.puml @@ -0,0 +1,126 @@ +@startuml event-AI추천요청 +!theme mono + +title Event Service - AI 추천 요청 (Kafka Job 발행) (UFR-EVENT-030) + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "JobService" as JobSvc <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> +participant "Kafka Producer" as Kafka <> + +note over Controller, Kafka +**UFR-EVENT-030: AI 이벤트 추천 요청** +- Kafka 비동기 Job 발행 +- AI Service가 Kafka 구독하여 처리 +- 트렌드 분석 + 3가지 추천안 생성 +- 처리 시간: 평균 2분 이내 +end note + +Client -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{"objective": "신규 고객 유치",\n"industry": "음식점",\n"region": "서울 강남구"} +activate Gateway + +Gateway -> Controller: POST /api/events/{eventDraftId}/ai-recommendations +activate Controller + +Controller -> Controller: 요청 검증\n(필수 필드, 목적 유효성) + +Controller -> Service: requestAIRecommendation(eventDraftId, userId) +activate Service + +== 1단계: 이벤트 초안 조회 및 검증 == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: 이벤트 초안 조회\n(초안ID로 이벤트 목적,\n매장 정보 조회) +activate DB +DB --> Repo: EventDraft 엔티티\n{목적, 매장명, 업종, 주소} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: 소유권 검증\nvalidateOwnership(userId, eventDraft) + +alt 소유권 없음 + Service --> Controller: throw ForbiddenException\n("권한이 없습니다") + Controller --> Gateway: 403 Forbidden\n{"code": "EVENT_003",\n"message": "권한이 없습니다"} + deactivate Service + deactivate Controller + Gateway --> Client: 403 Forbidden + deactivate Gateway + +else 소유권 확인 + + == 2단계: Kafka Job 생성 == + + Service -> JobSvc: createAIJob(eventDraft) + activate JobSvc + + JobSvc -> JobSvc: Job ID 생성 (UUID) + + JobSvc -> Cache: Job 상태 저장\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: AI_RECOMMEND,\ncreatedAt}\nTTL: 1시간 + activate Cache + Cache --> JobSvc: 저장 완료 + deactivate Cache + + == 3단계: Kafka 이벤트 발행 == + + JobSvc -> Kafka: 이벤트 발행\nTopic: ai-job-topic\nPayload: {jobId, eventDraftId,\nobjective, industry,\nregion, storeInfo} + activate Kafka + note right of Kafka + **Kafka Topic** + - Topic: ai-job-topic + - Consumer: AI Service + - Consumer Group: ai-service-group + + **Payload** + { + "jobId": "UUID", + "eventDraftId": "UUID", + "objective": "신규 고객 유치", + "industry": "음식점", + "region": "서울 강남구", + "storeInfo": {...} + } + end note + Kafka --> JobSvc: ACK (발행 확인) + deactivate Kafka + + JobSvc --> Service: JobResponse\n{jobId, status: PENDING} + deactivate JobSvc + + == 4단계: 응답 반환 == + + Service --> Controller: JobResponse\n{jobId, status: PENDING} + deactivate Service + + Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "AI가 분석 중입니다"} + deactivate Controller + + Gateway --> Client: 202 Accepted\nAI 분석 시작 + deactivate Gateway + + note over Service, Kafka + **AI Service 비동기 처리** + - Kafka 구독: ai-job-topic + - 트렌드 분석 (업종, 지역 기반) + - 3가지 추천안 생성 (저/중/고 비용) + - 결과: Redis에 저장 (TTL: 24시간) + - 상세: ai-트렌드분석및추천.puml 참조 + + **처리 시간** + - 평균: 2분 이내 + - P95: 4분 이내 + - Timeout: 5분 + + **결과 조회** + - 폴링 방식: GET /api/jobs/{jobId}/status + - 간격: 2초, 최대 30초 + end note +end + +@enduml diff --git a/design/backend/sequence/inner/event-대시보드조회.puml b/design/backend/sequence/inner/event-대시보드조회.puml new file mode 100644 index 0000000..34187cd --- /dev/null +++ b/design/backend/sequence/inner/event-대시보드조회.puml @@ -0,0 +1,73 @@ +@startuml event-대시보드조회 +!theme mono + +title Event Service - 대시보드 이벤트 목록 (UFR-EVENT-010) + +actor Client +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller: GET /api/events/dashboard +Controller -> Service: getDashboard(userId) +activate Service + +Service -> Cache: get("dashboard:" + userId) +activate Cache + +alt 캐시 히트 + Cache --> Service: Dashboard data + Service --> Controller: DashboardResponse + +else 캐시 미스 + Cache --> Service: null + deactivate Cache + + group parallel + Service -> Repo: findTopByStatusAndUserId(ACTIVE, userId, limit=5) + activate Repo + Repo -> DB: 진행중 이벤트 목록 조회\n(사용자ID로 ACTIVE 상태 이벤트 조회,\n참여자 수 함께 조회,\n생성일 내림차순, 최대 5개) + activate DB + DB --> Repo: Active events + deactivate DB + Repo --> Service: List (active) + deactivate Repo + + Service -> Repo: findTopByStatusAndUserId(APPROVED, userId, limit=5) + activate Repo + Repo -> DB: 예정 이벤트 목록 조회\n(사용자ID로 APPROVED 상태 이벤트 조회,\n승인일 내림차순, 최대 5개) + activate DB + DB --> Repo: Approved events + deactivate DB + Repo --> Service: List (approved) + deactivate Repo + + Service -> Repo: findTopByStatusAndUserId(COMPLETED, userId, limit=5) + activate Repo + Repo -> DB: 종료 이벤트 목록 조회\n(사용자ID로 COMPLETED 상태 이벤트 조회,\n참여자 수 함께 조회,\n종료일 내림차순, 최대 5개) + activate DB + DB --> Repo: Completed events + deactivate DB + Repo --> Service: List (completed) + deactivate Repo + end + + Service -> Service: buildDashboardResponse(active, approved, completed) + note right: 대시보드 데이터 구성:\n- 진행중: 5개\n- 예정: 5개\n- 종료: 5개\n각 카드에 기본 통계 포함 + + Service -> Cache: set("dashboard:" + userId,\ndashboard, TTL=1분) + activate Cache + Cache --> Service: OK + deactivate Cache +end + +Service --> Controller: DashboardResponse\n{active: [...], approved: [...],\ncompleted: [...]} +deactivate Service + +Controller --> Client: 200 OK\n{active: [\n {eventId, title, period, status,\n participantCount, viewCount, ...}\n],\napproved: [...],\ncompleted: [...]} + +note over Controller, DB: 대시보드 카드 정보:\n- 이벤트명\n- 이벤트 기간\n- 진행 상태 뱃지\n- 간단한 통계\n (참여자 수, 조회수 등)\n\n섹션당 최대 5개 표시\n(최신 순) + +@enduml diff --git a/design/backend/sequence/inner/event-목록조회.puml b/design/backend/sequence/inner/event-목록조회.puml new file mode 100644 index 0000000..7be0f71 --- /dev/null +++ b/design/backend/sequence/inner/event-목록조회.puml @@ -0,0 +1,64 @@ +@startuml event-목록조회 +!theme mono + +title Event Service - 이벤트 목록 조회 (필터/검색) (UFR-EVENT-070) + +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller: GET /api/events?status={상태}&keyword={검색어}\n&page={페이지}&size={크기} +Controller -> Service: 이벤트 목록 조회(사용자ID, 필터, 페이징) +activate Service + +Service -> Cache: 캐시 조회("events:" + 사용자ID + ":" + 필터 + ":" + 페이지) +activate Cache + +alt 캐시 히트 + Cache --> Service: 이벤트 목록 데이터 + Service --> Controller: 이벤트 목록 응답 + +else 캐시 미스 + Cache --> Service: null + deactivate Cache + + Service -> Repo: 사용자별 필터링 이벤트 조회(사용자ID, 필터, 페이징) + activate Repo + + alt 필터 있음 (상태별) + Repo -> DB: 사용자별 특정 상태 이벤트 조회\n(참여자 수 포함, 생성일 기준 내림차순,\n페이징 적용) + else 검색 있음 (키워드) + Repo -> DB: 사용자별 이벤트 키워드 검색\n(제목/설명에서 검색, 참여자 수 포함,\n생성일 기준 내림차순, 페이징 적용) + else 필터 없음 (전체) + Repo -> DB: 사용자별 전체 이벤트 목록 조회\n(참여자 수 포함, 생성일 기준 내림차순,\n페이징 적용) + end + + activate DB + note right: 인덱스 활용:\n- 사용자ID\n- 상태\n- 생성일시 + DB --> Repo: 이벤트 목록 및 참여자 수 + deactivate DB + + Repo -> DB: 전체 이벤트 개수 조회\n(필터 조건 포함, 페이징용) + activate DB + DB --> Repo: 전체 개수 + deactivate DB + + Repo --> Service: 페이징된 이벤트 결과 + deactivate Repo + + Service -> Cache: 캐시 저장("events:" + 사용자ID + ":" + 필터 + ":" + 페이지,\n페이징결과, TTL=1분) + activate Cache + Cache --> Service: OK + deactivate Cache +end + +Service --> Controller: 이벤트 목록 응답\n{이벤트목록: [...], 전체개수,\n전체페이지수, 현재페이지} +deactivate Service + +Controller --> Client: 200 OK\n{이벤트목록: [\n {이벤트ID, 제목, 기간, 상태,\n 참여자수, ROI, 생성일시},\n ...\n],\n전체개수, 전체페이지수, 현재페이지} + +note over Controller, DB: 필터 옵션:\n- 상태: 임시저장, 진행중, 완료\n- 기간: 최근 1개월/3개월/6개월/1년\n- 정렬: 최신순, 참여자 많은 순,\n ROI 높은 순\n\n페이지네이션:\n- 기본 20개/페이지\n- 페이지 번호 기반 + +@enduml diff --git a/design/backend/sequence/inner/event-목적선택.puml b/design/backend/sequence/inner/event-목적선택.puml new file mode 100644 index 0000000..2c65eaa --- /dev/null +++ b/design/backend/sequence/inner/event-목적선택.puml @@ -0,0 +1,110 @@ +@startuml event-목적선택 +!theme mono + +title Event Service - 이벤트 목적 선택 및 저장 (UFR-EVENT-020) + +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> +participant "Kafka Producer" as Kafka <> +actor Client + +note over Controller, DB +**UFR-EVENT-020: 이벤트 목적 선택 및 저장** +- 목적 선택: 신규 고객 유치, 재방문 유도, 매출 증대, 인지도 향상 +- Redis 캐시 사용 (TTL: 30분) +- Kafka 이벤트 발행 (EventDraftCreated) +- 사용자 및 매장 정보는 User Service에서 조회 후 전달됨 +end note + +Client -> Controller: POST /api/events/purposes\n{"userId": 123,\n"objective": "신규 고객 유치",\n"storeName": "맛있는집",\n"industry": "음식점",\n"address": "서울시 강남구"} +activate Controller + +Controller -> Controller: 입력값 검증\n(필수 필드, 목적 유효성 확인) + +Controller -> Service: createEventDraft(userId, objective, storeInfo) +activate Service + +== 1단계: Redis 캐시 확인 == + +Service -> Cache: 캐시 조회\nKey: draft:event:{userId}\n(기존 작성 중인 이벤트 확인) +activate Cache +Cache --> Service: null (캐시 미스) +deactivate Cache + +== 2단계: 목적 유효성 검증 == + +Service -> Service: 목적 유효성 검증\n- 신규 고객 유치\n- 재방문 유도\n- 매출 증대\n- 인지도 향상 + +Service -> Service: 매장 정보 유효성 검증\n(매장명, 업종, 주소) + +== 3단계: 이벤트 초안 저장 == + +Service -> Repo: save(eventDraft) +activate Repo +Repo -> DB: 이벤트 초안 저장\n(사용자ID, 목적, 매장명,\n업종, 주소, 상태=DRAFT,\n생성일시)\n저장 후 이벤트초안ID 반환 +activate DB +DB --> Repo: 생성된 이벤트초안ID +deactivate DB +Repo --> Service: EventDraft 엔티티\n(eventDraftId 포함) +deactivate Repo + +== 4단계: Redis 캐시 저장 == + +Service -> Cache: 캐시 저장\nKey: draft:event:{eventDraftId}\nValue: {목적, 매장정보, 상태}\nTTL: 24시간 +activate Cache +Cache --> Service: 저장 완료 +deactivate Cache + +== 5단계: Kafka 이벤트 발행 == + +Service -> Kafka: 이벤트 발행\nTopic: event-topic\nEvent: EventDraftCreated\nPayload: {eventDraftId,\nuserId, objective,\ncreatedAt} +activate Kafka +note right of Kafka +**Kafka Event Topic** +- Topic: event-topic +- Event: EventDraftCreated +- 목적 선택 시 발행 + +**구독자** +- Analytics Service (선택적) + +**참고** +- EventCreated는 + 최종 승인 시 발행 +end note +Kafka --> Service: ACK (발행 확인) +deactivate Kafka + +== 6단계: 응답 반환 == + +Service -> Service: 응답 DTO 생성 +Service --> Controller: EventDraftResponse\n{eventDraftId, objective,\nstoreName, status=DRAFT} +deactivate Service + +Controller --> Client: 200 OK\n{"eventDraftId": "draft-123",\n"objective": "신규 고객 유치",\n"storeName": "맛있는집",\n"status": "DRAFT"} +deactivate Controller + +note over Controller, Kafka +**캐시 전략** +- Key: draft:event:{eventDraftId} +- TTL: 24시간 +- 캐시 히트 시: DB 조회 생략, 즉시 반환 + +**이벤트 발행 전략** +- EventDraftCreated: 목적 선택 시 발행 (Analytics Service 선택적 구독) +- EventCreated: 최종 승인 시 발행 (통계 초기화 시작) + +**성능 목표** +- 평균 응답 시간: 0.3초 이내 +- P95 응답 시간: 0.5초 이내 +- Redis 캐시 조회: 0.05초 이내 + +**에러 코드** +- EVENT_001: 유효하지 않은 목적 +- EVENT_002: 매장 정보 누락 +end note + +@enduml diff --git a/design/backend/sequence/inner/event-상세조회.puml b/design/backend/sequence/inner/event-상세조회.puml new file mode 100644 index 0000000..4e94e8e --- /dev/null +++ b/design/backend/sequence/inner/event-상세조회.puml @@ -0,0 +1,54 @@ +@startuml event-상세조회 +!theme mono + +title Event Service - 이벤트 상세 조회 (UFR-EVENT-060) + +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller: GET /api/events/{id} +Controller -> Service: getEventDetail(eventId, userId) +activate Service + +Service -> Cache: get("event:" + eventId) +activate Cache + +alt 캐시 히트 + Cache --> Service: Event data + Service -> Service: validateAccess(userId, event) + note right: 사용자 권한 검증 + Service --> Controller: EventDetailResponse + +else 캐시 미스 + Cache --> Service: null + deactivate Cache + + Service -> Repo: findById(eventId) + activate Repo + Repo -> DB: 이벤트 상세 정보 조회\n(이벤트ID로 이벤트 정보,\n경품 정보, 배포 이력을\nJOIN하여 함께 조회) + activate DB + note right: JOIN으로\n경품 정보 및\n배포 이력 조회 + DB --> Repo: Event with prizes and distributions + deactivate DB + Repo --> Service: Event entity (with relations) + deactivate Repo + + Service -> Service: validateAccess(userId, event) + + Service -> Cache: set("event:" + eventId, event, TTL=5분) + activate Cache + Cache --> Service: OK + deactivate Cache +end + +Service --> Controller: EventDetailResponse\n{eventId, title, objective,\nprizes, period, status,\nchannels, distributionStatus,\ncreatedAt, publishedAt} +deactivate Service + +Controller --> Client: 200 OK\n{event: {...},\nprizes: [...],\ndistributionStatus: {...}} + +note over Controller, DB: 상세 정보 포함:\n- 기본 정보 (제목, 목적, 기간, 상태)\n- 경품 정보\n- 참여 방법\n- 배포 채널 현황\n- 실시간 통계 (Analytics Service)\n\nAnalytics 통계는\n별도 API 호출 + +@enduml diff --git a/design/backend/sequence/inner/event-최종승인및배포.puml b/design/backend/sequence/inner/event-최종승인및배포.puml new file mode 100644 index 0000000..725af86 --- /dev/null +++ b/design/backend/sequence/inner/event-최종승인및배포.puml @@ -0,0 +1,157 @@ +@startuml event-최종승인및배포 +!theme mono + +title Event Service - 최종 승인 및 Distribution Service 동기 호출 (UFR-EVENT-050) + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> +participant "Distribution Service" as DistSvc <> +participant "Kafka Producer" as Kafka <> + +note over Controller, Kafka +**UFR-EVENT-050: 이벤트 최종 승인 및 배포** +- 이벤트 준비 상태 검증 +- 이벤트 승인 및 Kafka 이벤트 발행 +- Distribution Service 동기 호출 (다중 채널 배포) +- 이벤트 상태를 ACTIVE로 변경 +end note + +Client -> Gateway: POST /api/events/{eventDraftId}/publish\n{"userId": 123,\n"selectedChannels": [\n "우리동네TV",\n "지니TV",\n "Instagram"\n]} +activate Gateway + +Gateway -> Controller: POST /api/events/{eventDraftId}/publish +activate Controller + +Controller -> Controller: 요청 검증\n(필수 필드, 채널 유효성) + +Controller -> Service: publishEvent(eventDraftId, userId, selectedChannels) +activate Service + +== 1단계: 이벤트 초안 조회 및 검증 == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: 이벤트 초안 조회\n(초안ID로 조회) +activate DB +DB --> Repo: EventDraft 엔티티\n{목적, 추천안, 콘텐츠, 상태} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: validateOwnership(userId, eventDraft) +note right +소유권 검증: +- 사용자ID와 초안 소유자 일치 확인 +- 권한 없으면 403 Forbidden +end note + +Service -> Service: validatePublishReady() +note right +발행 준비 검증: +- 목적 선택 완료 +- AI 추천 선택 완료 +- 콘텐츠 선택 완료 +- 배포 채널 최소 1개 선택 +end note + +== 2단계: 이벤트 승인 == + +Service -> Repo: updateStatus(eventDraftId, APPROVED) +activate Repo +Repo -> DB: 이벤트 초안 상태 업데이트\n(상태를 APPROVED로,\n승인일시를 현재 시각으로 저장) +activate DB +DB --> Repo: 업데이트 완료 +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +== 3단계: Kafka 이벤트 발행 == + +Service -> Kafka: 이벤트 발행\nTopic: event-topic\nPayload: {eventId, userId, title,\nobjective, createdAt} +activate Kafka +note right +Kafka Event Topic: +- Topic: event-topic +- Consumer: Analytics Service +- Event Type: EventCreated +end note +Kafka --> Service: ACK (발행 확인) +deactivate Kafka + +== 4단계: Distribution Service 동기 호출 == + +Service -> DistSvc: POST /api/distribution/distribute\n{"eventId": 123,\n"channels": [...],\n"content": {...}} +activate DistSvc +note right +동기 호출 (Circuit Breaker 적용): +- Timeout: 70초 +- 다중 채널 병렬 배포 +- Failure Rate: 50% 초과 시 OPEN +end note + +DistSvc -> DistSvc: distributeToChannels(eventId, channels) +note right +다중 채널 병렬 배포: +- 우리동네TV +- 링고비즈 (음성 안내) +- 지니TV +- Instagram +- Naver Blog +- Kakao Channel +end note + +DistSvc --> Service: DistributionResponse\n{"distributionId": "dist-123",\n"channelResults": [\n {"channel": "우리동네TV", "status": "SUCCESS"},\n {"channel": "지니TV", "status": "SUCCESS"},\n {"channel": "Instagram", "status": "SUCCESS"}\n]} +deactivate DistSvc + +== 5단계: 이벤트 활성화 == + +Service -> Repo: updateStatus(eventDraftId, ACTIVE) +activate Repo +Repo -> DB: 이벤트 상태 업데이트\n(상태를 ACTIVE로,\n배포일시를 현재 시각으로 저장) +activate DB +DB --> Repo: 업데이트 완료 +deactivate DB +Repo --> Service: Event entity +deactivate Repo + +== 6단계: 캐시 무효화 == + +Service -> Cache: 캐시 삭제\nKey: draft:event:{eventDraftId} +activate Cache +note right +Redis 캐시 무효화: +- 이벤트 초안 캐시 삭제 +- 활성화 완료 후 불필요 +end note +Cache --> Service: 삭제 완료 +deactivate Cache + +== 7단계: 응답 반환 == + +Service --> Controller: PublishResponse\n{eventId, status: ACTIVE,\ndistributionResults} +deactivate Service + +Controller --> Gateway: 200 OK\n{"eventId": 123,\n"status": "ACTIVE",\n"distributionResults": [...]} +deactivate Controller + +Gateway --> Client: 200 OK\n이벤트 배포 완료 +deactivate Gateway + +note over Client, Kafka +**배포 완료 후 처리** +- Distribution Service는 배포 완료 후 Kafka에\n DistributionCompleted 이벤트 발행 +- Analytics Service가 구독하여 초기 통계 생성 +- 이벤트 상태: ACTIVE (참여자 접수 시작) + +**성능 목표** +- 응답 시간: 60초 이내 (Distribution Service 포함) +- Distribution Service 타임아웃: 70초 +- 채널별 배포: 병렬 처리로 최적화 +end note + +@enduml diff --git a/design/backend/sequence/inner/event-추천결과조회.puml b/design/backend/sequence/inner/event-추천결과조회.puml new file mode 100644 index 0000000..c69425a --- /dev/null +++ b/design/backend/sequence/inner/event-추천결과조회.puml @@ -0,0 +1,140 @@ +@startuml event-추천결과조회 +!theme mono + +title Event Service - AI 추천 결과 폴링 조회 + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "JobManager" as JobMgr <> +participant "Redis Cache" as Cache <> + +note over Controller, Cache +**폴링 방식 Job 상태 조회** +- 최대 30초 동안 폴링 (2초 간격) +- Job 상태: PENDING → PROCESSING → COMPLETED +- AI 추천 결과: Redis에 저장 (TTL: 24시간) +end note + +Client -> Gateway: GET /api/events/jobs/{jobId}/status +activate Gateway + +Gateway -> Controller: GET /api/events/jobs/{jobId}/status +activate Controller + +Controller -> Service: getJobStatus(jobId) +activate Service + +Service -> JobMgr: getJobStatus(jobId) +activate JobMgr + +JobMgr -> Cache: Job 상태 조회\nKey: job:{jobId} +activate Cache + +alt Job 데이터 존재 + Cache --> JobMgr: Job 데이터\n{status, eventDraftId,\ntype, createdAt} + deactivate Cache + + alt status = COMPLETED + JobMgr -> Cache: AI 추천 결과 조회\nKey: ai:recommendation:{eventDraftId} + activate Cache + Cache --> JobMgr: AI 추천 결과\n{트렌드분석, 3가지추천안} + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nrecommendations: {...}} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: COMPLETED, recommendations} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"recommendations": [\n {"title": "저비용 추천안",\n "prize": "커피쿠폰",\n "method": "QR코드 스캔",\n "cost": "50만원",\n "roi": "150%"},\n {"title": "중비용 추천안",\n "prize": "상품권",\n "method": "SNS 공유",\n "cost": "100만원",\n "roi": "200%"},\n {"title": "고비용 추천안",\n "prize": "경품 추첨",\n "method": "설문 참여",\n "cost": "200만원",\n "roi": "300%"}\n]} + deactivate Controller + + Gateway --> Client: 200 OK\nAI 추천 결과 반환 + deactivate Gateway + + note right of Client + **프론트엔드 처리** + - 3가지 추천안 카드 표시 + - 사용자가 추천안 선택 + - 트렌드 분석 정보 표시 + end note + + else status = PROCESSING 또는 PENDING + JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: PROCESSING} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "AI가 분석 중입니다"} + deactivate Controller + + Gateway --> Client: 200 OK\n진행 중 상태 + deactivate Gateway + + note right of Client + **폴링 재시도** + - 2초 후 재요청 + - 최대 30초 (15회) + end note + + else status = FAILED + JobMgr -> Cache: 에러 정보 조회\nKey: job:{jobId}:error + activate Cache + Cache --> JobMgr: 에러 메시지 + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: FAILED, error} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "AI 분석 실패",\n"message": "다시 시도해주세요"} + deactivate Controller + + Gateway --> Client: 200 OK\n실패 상태 + deactivate Gateway + + note right of Client + **실패 처리** + - 에러 메시지 표시 + - "다시 분석" 버튼 제공 + end note + end + +else Job 데이터 없음 + Cache --> JobMgr: null (캐시 미스) + deactivate Cache + + JobMgr --> Service: throw NotFoundException\n("Job을 찾을 수 없습니다") + deactivate JobMgr + + Service --> Controller: NotFoundException + deactivate Service + + Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job을 찾을 수 없습니다"} + deactivate Controller + + Gateway --> Client: 404 Not Found + deactivate Gateway +end + +note over Controller, Cache +**폴링 전략** +- 간격: 2초 +- 최대 시간: 30초 (15회) +- Timeout 시: 사용자에게 알림 + "다시 분석" 옵션 + +**Redis 캐시** +- Job 상태: TTL 1시간 +- AI 추천 결과: TTL 24시간 + +**성능 목표** +- 평균 AI 분석 시간: 2분 이내 +- P95 AI 분석 시간: 4분 이내 +end note + +@enduml diff --git a/design/backend/sequence/inner/event-추천안선택.puml b/design/backend/sequence/inner/event-추천안선택.puml new file mode 100644 index 0000000..295bdea --- /dev/null +++ b/design/backend/sequence/inner/event-추천안선택.puml @@ -0,0 +1,116 @@ +@startuml event-추천안선택 +!theme mono + +title Event Service - 선택한 AI 추천안 저장 (UFR-EVENT-040) + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller, Cache +**UFR-EVENT-040: AI 추천안 선택 및 저장** +- 사용자가 3가지 추천안 중 하나를 선택 +- 선택된 추천안을 이벤트 초안에 적용 +- Redis 캐시에서 AI 추천 결과 삭제 +end note + +Client -> Gateway: PUT /api/events/drafts/{eventDraftId}/recommendation\n{"userId": 123,\n"selectedIndex": 1,\n"recommendation": {...}} +activate Gateway + +Gateway -> Controller: PUT /api/events/drafts/{eventDraftId}/recommendation +activate Controller + +Controller -> Controller: 요청 검증\n(필수 필드, 추천안 유효성) + +Controller -> Service: updateEventRecommendation(eventDraftId, userId,\nselectedRecommendation) +activate Service + +== 1단계: 이벤트 초안 조회 및 검증 == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: 이벤트 초안 조회\n(초안ID로 조회) +activate DB +DB --> Repo: EventDraft 엔티티\n{목적, 매장정보, 상태} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: validateOwnership(userId, eventDraft) +note right +소유권 검증: +- 사용자ID와 초안 소유자 일치 확인 +- 권한 없으면 403 Forbidden +end note + +Service -> Service: validateRecommendation(selectedRecommendation) +note right +추천안 유효성 검증: +- 필수 필드 존재 여부 +- 비용/ROI 값 타당성 +end note + +== 2단계: 추천안 적용 == + +Service -> Service: applyRecommendation(eventDraft, selectedRecommendation) +note right +추천안 적용: +- 이벤트 제목 +- 경품 정보 +- 참여 방법 +- 예상 비용 +- 예상 ROI +- 홍보 문구 +end note + +== 3단계: DB 저장 == + +Service -> Repo: update(eventDraft) +activate Repo +Repo -> DB: 이벤트 초안 업데이트\n(선택된 추천안 정보 저장:\n제목, 경품, 참여방법,\n예상비용, 예상ROI,\n수정일시) +activate DB +DB --> Repo: 업데이트 완료 +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +== 4단계: 캐시 무효화 == + +Service -> Cache: 캐시 삭제\nKey: ai:recommendation:{eventDraftId} +activate Cache +note right +Redis 캐시 무효화: +- AI 추천 결과 삭제 +- 선택 완료 후 불필요 +end note +Cache --> Service: 삭제 완료 +deactivate Cache + +== 5단계: 응답 반환 == + +Service --> Controller: EventRecommendationResponse\n{eventDraftId, selectedRecommendation} +deactivate Service + +Controller --> Gateway: 200 OK\n{"eventDraftId": 123,\n"status": "추천안 선택 완료",\n"selectedRecommendation": {...}} +deactivate Controller + +Gateway --> Client: 200 OK\n추천안 선택 완료 +deactivate Gateway + +note over Client, Cache +**저장 내용** +- 최종 선택된 추천안만 Event DB에 저장 +- Redis에 저장된 3가지 추천안은 선택 후 삭제 +- 다음 단계: 콘텐츠 생성 (이미지 선택) + +**성능 목표** +- 응답 시간: 0.5초 이내 +- DB 업데이트: 0.1초 +- 캐시 삭제: 0.01초 +end note + +@enduml diff --git a/design/backend/sequence/inner/event-콘텐츠선택.puml b/design/backend/sequence/inner/event-콘텐츠선택.puml new file mode 100644 index 0000000..464d55a --- /dev/null +++ b/design/backend/sequence/inner/event-콘텐츠선택.puml @@ -0,0 +1,118 @@ +@startuml event-콘텐츠선택 +!theme mono + +title Content Service - 선택한 콘텐츠 저장 (UFR-CONT-020) + +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller, Cache +**UFR-CONT-020: 콘텐츠 선택 및 편집 저장** +- 사용자가 3가지 이미지 스타일 중 하나 선택 +- 선택된 이미지에 텍스트/색상 편집 적용 +- 편집된 콘텐츠를 이벤트 초안에 저장 +end note + +Client -> Gateway: PUT /api/content/{eventDraftId}/select\n{"userId": 123,\n"selectedImageUrl": "https://cdn.../fancy.png",\n"editedContent": {...}} +activate Gateway + +Gateway -> Controller: PUT /api/content/{eventDraftId}/select +activate Controller + +Controller -> Controller: 요청 검증\n(필수 필드, URL 유효성) + +Controller -> Service: updateEventContent(eventDraftId, userId,\nselectedImageUrl, editedContent) +activate Service + +== 1단계: 이벤트 초안 조회 및 검증 == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: 이벤트 초안 조회\n(초안ID로 조회) +activate DB +DB --> Repo: EventDraft 엔티티\n{목적, 추천안, 상태} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: validateOwnership(userId, eventDraft) +note right +소유권 검증: +- 사용자ID와 초안 소유자 일치 확인 +- 권한 없으면 403 Forbidden +end note + +Service -> Service: validateImageUrl(selectedImageUrl) +note right +이미지 URL 검증: +- URL 형식 유효성 +- CDN 경로 확인 +- 이미지 존재 여부 +end note + +== 2단계: 콘텐츠 편집 적용 == + +Service -> Service: applyContentEdits(eventDraft, editedContent) +note right +편집 내용 적용: +- 제목 텍스트 +- 경품 정보 텍스트 +- 참여 안내 텍스트 +- 배경색 +- 텍스트 색상 +- 강조 색상 +end note + +== 3단계: DB 저장 == + +Service -> Repo: update(eventDraft) +activate Repo +Repo -> DB: 이벤트 초안 업데이트\n(선택된 이미지 URL,\n편집된 제목/텍스트,\n배경색/텍스트색,\n수정일시 저장) +activate DB +DB --> Repo: 업데이트 완료 +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +== 4단계: 캐시 무효화 == + +Service -> Cache: 캐시 삭제\nKey: content:image:{eventDraftId} +activate Cache +note right +Redis 캐시 무효화: +- 이미지 URL 캐시 삭제 +- 선택 완료 후 불필요 +end note +Cache --> Service: 삭제 완료 +deactivate Cache + +== 5단계: 응답 반환 == + +Service --> Controller: EventContentResponse\n{eventDraftId, selectedImageUrl,\neditedContent} +deactivate Service + +Controller --> Gateway: 200 OK\n{"eventDraftId": 123,\n"status": "콘텐츠 선택 완료",\n"selectedImageUrl": "...",\n"editedContent": {...}} +deactivate Controller + +Gateway --> Client: 200 OK\n콘텐츠 선택 완료 +deactivate Gateway + +note over Client, Cache +**저장 내용** +- 선택된 이미지 URL +- 편집된 텍스트 (제목, 경품 정보, 참여 안내) +- 편집된 색상 (배경색, 텍스트색, 강조색) +- 다음 단계: 최종 승인 및 배포 + +**성능 목표** +- 응답 시간: 0.5초 이내 +- DB 업데이트: 0.1초 +- 캐시 삭제: 0.01초 +end note + +@enduml diff --git a/design/backend/sequence/inner/participation-당첨자추첨.puml b/design/backend/sequence/inner/participation-당첨자추첨.puml new file mode 100644 index 0000000..63b9381 --- /dev/null +++ b/design/backend/sequence/inner/participation-당첨자추첨.puml @@ -0,0 +1,164 @@ +@startuml participation-당첨자추첨 +!theme mono + +title Participation Service - 당첨자 추첨 내부 시퀀스 + +actor "사장님" as Owner +participant "API Gateway" as Gateway +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "LotteryAlgorithm" as Lottery +participant "ParticipantRepository" as Repo +participant "DrawLogRepository" as LogRepo +database "Participation DB" as DB + +== UFR-PART-030: 당첨자 추첨 == + +Owner -> Gateway: POST /api/v1/events/{eventId}/draw-winners\n{winnerCount, visitBonus, algorithm} +activate Gateway + +Gateway -> Gateway: JWT 토큰 검증\n- 토큰 유효성 확인\n- 사장님 권한 확인 + +alt JWT 검증 실패 + Gateway --> Owner: 401 Unauthorized + deactivate Gateway +else JWT 검증 성공 + + Gateway -> Controller: POST /participations/draw-winners\n{eventId, winnerCount, visitBonus} + activate Controller + + Controller -> Controller: 요청 데이터 유효성 검증\n- eventId 필수\n- winnerCount > 0\n- winnerCount <= 참여자 수 + + alt 유효성 검증 실패 + Controller --> Gateway: 400 Bad Request + Gateway --> Owner: 400 Bad Request + deactivate Controller + deactivate Gateway + else 유효성 검증 성공 + + Controller -> Service: drawWinners(eventId, winnerCount, visitBonus) + activate Service + + Service -> Service: 이벤트 상태 확인\n- 이벤트 종료 여부\n- 이미 추첨 완료 여부 + + Service -> LogRepo: findByEventId(eventId) + activate LogRepo + LogRepo -> DB: 추첨 로그 조회\n(이벤트ID로 조회) + activate DB + DB --> LogRepo: 추첨 로그 조회 + deactivate DB + LogRepo --> Service: Optional + deactivate LogRepo + + alt 이미 추첨 완료 + Service --> Controller: AlreadyDrawnException + Controller --> Gateway: 409 Conflict\n{message: "이미 추첨이 완료된 이벤트입니다"} + Gateway --> Owner: 409 Conflict + deactivate Service + deactivate Controller + deactivate Gateway + else 추첨 가능 상태 + + Service -> Repo: findAllByEventIdAndIsWinner(eventId, false) + activate Repo + Repo -> DB: 미당첨 참여자 목록 조회\n(이벤트ID로 당첨되지 않은\n참여자 전체 조회,\n참여일시 오름차순 정렬) + activate DB + DB --> Repo: 전체 참여자 목록 + deactivate DB + Repo --> Service: List + deactivate Repo + + alt 참여자 수 부족 + Service --> Controller: InsufficientParticipantsException + Controller --> Gateway: 400 Bad Request\n{message: "참여자 수가 부족합니다"} + Gateway --> Owner: 400 Bad Request + deactivate Service + deactivate Controller + deactivate Gateway + else 추첨 진행 + + Service -> Lottery: executeLottery(participants, winnerCount, visitBonus) + activate Lottery + + note right of Lottery + 추첨 알고리즘: + 시간 복잡도: O(n log n) + 공간 복잡도: O(n) + + 1. 난수 생성 (Crypto.randomBytes) + 2. 매장 방문 가산점 적용 (옵션) + - 방문 고객: 가중치 2배 + - 비방문 고객: 가중치 1배 + 3. Fisher-Yates Shuffle + - 가중치 기반 확률 분포 + - 무작위 섞기 + 4. 상위 N명 선정 + end note + + Lottery -> Lottery: Step 1: 난수 시드 생성\n- Crypto.randomBytes(32)\n- 예측 불가능한 난수 보장 + + Lottery -> Lottery: Step 2: 가산점 적용\n- visitBonus = true일 경우\n- 매장 방문 경로 참여자 가중치 증가 + + Lottery -> Lottery: Step 3: Fisher-Yates Shuffle\n- 가중치 기반 확률 분포\n- O(n) 시간 복잡도 + + Lottery -> Lottery: Step 4: 당첨자 선정\n- 상위 winnerCount명 추출 + + Lottery --> Service: List 당첨자 목록 + deactivate Lottery + + Service -> Service: DB 트랜잭션 시작 + + alt DB 저장 실패 시 + note right of Service + 트랜잭션 롤백 처리: + - 당첨자 업데이트 취소 + - 추첨 로그 저장 취소 + - 재시도 가능 상태 유지 + end note + end + + Service -> Repo: updateWinners(winnerIds) + activate Repo + Repo -> DB: 당첨자 정보 업데이트\n(당첨 여부를 true로,\n당첨 일시를 현재 시각으로 설정,\n대상: 선정된 참여자ID 목록) + activate DB + DB --> Repo: 업데이트 완료 + deactivate DB + Repo --> Service: 업데이트 건수 + deactivate Repo + + Service -> Service: DrawLog 엔티티 생성\n- drawLogId (UUID)\n- eventId\n- drawMethod: "RANDOM"\n- algorithm: "FISHER_YATES_SHUFFLE"\n- visitBonusApplied\n- winnerCount\n- drawnAt (현재시각) + + Service -> LogRepo: save(drawLog) + activate LogRepo + LogRepo -> DB: 추첨 로그 저장\n(추첨로그ID, 이벤트ID,\n추첨방법, 알고리즘,\n가산점적용여부, 당첨인원,\n추첨일시 저장) + activate DB + note right of DB + 추첨 로그 저장: + - 추첨 일시 기록 + - 알고리즘 버전 기록 + - 가산점 적용 여부 + - 감사 추적 목적 + end note + DB --> LogRepo: 로그 저장 완료 + deactivate DB + LogRepo --> Service: DrawLog 엔티티 + deactivate LogRepo + + Service -> Service: DB 트랜잭션 커밋 + + Service --> Controller: DrawWinnersResponse\n{당첨자목록, 추첨로그ID} + deactivate Service + + Controller --> Gateway: 200 OK\n{winners[], drawLogId, message} + deactivate Controller + + Gateway --> Owner: 200 OK + deactivate Gateway + + Owner -> Owner: 당첨자 목록 화면 표시\n- 당첨자 정보 테이블\n- 재추첨 버튼\n- 추첨 완료 메시지 + end + end + end +end + +@enduml diff --git a/design/backend/sequence/inner/participation-이벤트참여.puml b/design/backend/sequence/inner/participation-이벤트참여.puml new file mode 100644 index 0000000..0f144b7 --- /dev/null +++ b/design/backend/sequence/inner/participation-이벤트참여.puml @@ -0,0 +1,129 @@ +@startuml participation-이벤트참여 +!theme mono + +title Participation Service - 이벤트 참여 내부 시퀀스 + +actor "고객" as Customer +participant "API Gateway" as Gateway +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "ParticipantRepository" as Repo +database "Participation DB" as DB +participant "KafkaProducer" as Kafka +database "Redis Cache<>" as Cache + +== UFR-PART-010: 이벤트 참여 == + +Customer -> Gateway: POST /api/v1/participations\n{name, phone, eventId, entryPath, consent} +activate Gateway + +note right of Gateway + 비회원 참여 가능 + JWT 검증 불필요 +end note + +Gateway -> Controller: POST /participations/register\n{name, phone, eventId, entryPath, consent} +activate Controller + +Controller -> Controller: 요청 데이터 유효성 검증\n- 이름 2자 이상\n- 전화번호 형식 (정규식)\n- 개인정보 동의 필수 + +alt 유효성 검증 실패 + Controller --> Gateway: 400 Bad Request\n{message: "유효성 오류"} + Gateway --> Customer: 400 Bad Request + deactivate Controller + deactivate Gateway +else 유효성 검증 성공 + + Controller -> Service: registerParticipant(request) + activate Service + + Service -> Cache: GET duplicate_check:{eventId}:{phone} + activate Cache + Cache --> Service: 캐시 확인 결과 + deactivate Cache + + alt 캐시 HIT: 중복 참여 + Service --> Controller: DuplicateParticipationException + Controller --> Gateway: 409 Conflict\n{message: "이미 참여하신 이벤트입니다"} + Gateway --> Customer: 409 Conflict + deactivate Service + deactivate Controller + deactivate Gateway + else 캐시 MISS: DB 조회 + + Service -> Repo: findByEventIdAndPhoneNumber(eventId, phone) + activate Repo + Repo -> DB: 참여자 중복 확인\n(이벤트ID, 전화번호로 조회) + activate DB + DB --> Repo: 조회 결과 + deactivate DB + Repo --> Service: Optional + deactivate Repo + + alt DB에 중복 참여 존재 + Service -> Cache: SET duplicate_check:{eventId}:{phone} = true\nTTL: 7일 + activate Cache + Cache --> Service: 캐시 저장 완료 + deactivate Cache + + Service --> Controller: DuplicateParticipationException + Controller --> Gateway: 409 Conflict\n{message: "이미 참여하신 이벤트입니다"} + Gateway --> Customer: 409 Conflict + deactivate Service + deactivate Controller + deactivate Gateway + else 신규 참여: 저장 진행 + + Service -> Service: 응모 번호 생성\n- UUID 기반\n- 형식: EVT-{timestamp}-{random} + + Service -> Service: Participant 엔티티 생성\n- participantId (UUID)\n- eventId\n- name, phoneNumber\n- entryPath\n- applicationNumber (응모번호)\n- participatedAt (현재시각) + + Service -> Repo: save(participant) + activate Repo + Repo -> DB: 참여자 정보 저장\n(참여자ID, 이벤트ID, 이름, 전화번호,\n참여경로, 응모번호, 참여일시,\n마케팅동의여부) + activate DB + DB --> Repo: 저장 완료 + deactivate DB + Repo --> Service: Participant 엔티티 반환 + deactivate Repo + + note right of Service + 참여자 등록 완료 후 + 캐싱 및 이벤트 발행 + end note + + Service -> Cache: SET duplicate_check:{eventId}:{phone} = true\nTTL: 7일 + activate Cache + Cache --> Service: 캐시 저장 완료 + deactivate Cache + + Service -> Kafka: Publish Event\n"ParticipantRegistered"\nTopic: participant-events + activate Kafka + note right of Kafka + Event Payload: + { + "participantId": "UUID", + "eventId": "UUID", + "phoneNumber": "010-1234-5678", + "entryPath": "SNS", + "registeredAt": "2025-10-22T10:30:00Z" + } + end note + Kafka --> Service: 이벤트 발행 완료 + deactivate Kafka + + Service -> Service: 당첨 발표일 계산\n- 이벤트 종료일 + 3일 + + Service --> Controller: ParticipationResponse\n{응모번호, 당첨발표일, 참여완료메시지} + deactivate Service + + Controller --> Gateway: 201 Created\n{applicationNumber, drawDate, message} + deactivate Controller + + Gateway --> Customer: 201 Created\n참여 완료 화면 표시 + deactivate Gateway + end + end +end + +@enduml diff --git a/design/backend/sequence/inner/participation-참여자목록조회.puml b/design/backend/sequence/inner/participation-참여자목록조회.puml new file mode 100644 index 0000000..82e6d75 --- /dev/null +++ b/design/backend/sequence/inner/participation-참여자목록조회.puml @@ -0,0 +1,120 @@ +@startuml participation-참여자목록조회 +!theme mono + +title Participation Service - 참여자 목록 조회 내부 시퀀스 + +actor "사장님" as Owner +participant "API Gateway" as Gateway +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "ParticipantRepository" as Repo +database "Participation DB" as DB +database "Redis Cache<>" as Cache + +== UFR-PART-020: 참여자 목록 조회 == + +Owner -> Gateway: GET /api/v1/events/{eventId}/participants\n?entryPath={경로}&isWinner={당첨여부}\n&name={이름}&phone={전화번호}\n&page={페이지}&size={크기} +activate Gateway + +Gateway -> Gateway: JWT 토큰 검증\n- 토큰 유효성 확인\n- 사장님 권한 확인 + +alt JWT 검증 실패 + Gateway --> Owner: 401 Unauthorized + deactivate Gateway +else JWT 검증 성공 + + Gateway -> Controller: GET /participants\n{eventId, filters, pagination} + activate Controller + + Controller -> Controller: 요청 파라미터 유효성 검증\n- eventId 필수\n- page >= 0\n- size: 10~100 + + alt 유효성 검증 실패 + Controller --> Gateway: 400 Bad Request + Gateway --> Owner: 400 Bad Request + deactivate Controller + deactivate Gateway + else 유효성 검증 성공 + + Controller -> Service: getParticipantList(eventId, filters, pageable) + activate Service + + Service -> Service: 캐시 키 생성\n- participant_list:{eventId}:{filters}:{page} + + Service -> Cache: GET participant_list:{key} + activate Cache + Cache --> Service: 캐시 조회 결과 + deactivate Cache + + alt 캐시 HIT + Service --> Controller: ParticipantListResponse\n(캐시된 데이터) + note right of Service + 캐시된 데이터 반환 + - TTL: 10분 + - 실시간 정확도 vs 성능 트레이드오프 + end note + Controller --> Gateway: 200 OK\n{participants, totalElements, totalPages} + Gateway --> Owner: 200 OK\n참여자 목록 표시 + deactivate Service + deactivate Controller + deactivate Gateway + else 캐시 MISS: DB 조회 + + Service -> Service: 동적 쿼리 생성\n- 참여 경로 필터\n- 당첨 여부 필터\n- 이름/전화번호 검색 + + Service -> Repo: findParticipants(eventId, filters, pageable) + activate Repo + + Repo -> DB: 참여자 목록 조회\n(이벤트ID, 참여경로, 당첨여부,\n이름/전화번호 검색조건으로 필터링하여\n참여일시 내림차순으로 페이징 조회) + activate DB + + note right of DB + 동적 쿼리 조건: + - entryPath 필터 (선택) + - isWinner 필터 (선택) + - name/phone 검색 (선택) + - 페이지네이션 (필수) + + 필요 인덱스: + idx_participants_event_filters + (event_id, entry_path, is_winner, participated_at DESC) + end note + + DB --> Repo: 참여자 목록 결과셋 + deactivate DB + + Repo -> DB: 전체 참여자 수 조회\n(동일한 필터 조건 적용) + activate DB + DB --> Repo: 전체 건수 + deactivate DB + + Repo --> Service: Page + deactivate Repo + + Service -> Service: DTO 변환\n- 전화번호 마스킹 (010-****-1234)\n- 응모번호 형식화\n- 당첨 여부 라벨 변환 + + Service -> Cache: SET participant_list:{key} = data\nTTL: 10분 + activate Cache + note right of Cache + 캐시 저장: + - TTL: 10분 + - 실시간 참여 반영과 성능 균형 + - 이벤트 참여 빈도 고려 + end note + Cache --> Service: 캐시 저장 완료 + deactivate Cache + + Service --> Controller: ParticipantListResponse\n{participants[], totalElements, totalPages, currentPage} + deactivate Service + + Controller --> Gateway: 200 OK\n{data, pagination} + deactivate Controller + + Gateway --> Owner: 200 OK + deactivate Gateway + + Owner -> Owner: 참여자 목록 화면 표시\n- 테이블 형태\n- 페이지네이션\n- 필터/검색 UI + end + end +end + +@enduml diff --git a/design/backend/sequence/inner/user-로그아웃.puml b/design/backend/sequence/inner/user-로그아웃.puml new file mode 100644 index 0000000..88f1d24 --- /dev/null +++ b/design/backend/sequence/inner/user-로그아웃.puml @@ -0,0 +1,155 @@ +@startuml user-로그아웃 +!theme mono + +title User Service - 로그아웃 내부 시퀀스 (UFR-USER-040) + +actor Client +participant "UserController" as Controller <> +participant "AuthenticationService" as AuthService <> +participant "JwtTokenProvider" as JwtProvider <> +participant "Redis\nCache" as Redis <> + +note over Controller, Redis +**UFR-USER-040: 로그아웃** +- JWT 토큰 검증 +- Redis 세션 삭제 +- 클라이언트 측 토큰 삭제 (프론트엔드 처리) +end note + +Client -> Controller: POST /api/users/logout\nAuthorization: Bearer {JWT} +activate Controller + +Controller -> Controller: @AuthenticationPrincipal\n(JWT에서 userId 추출) + +Controller -> Controller: JWT 토큰 추출\n(Authorization 헤더에서) + +Controller -> AuthService: logout(token, userId) +activate AuthService + +== 1단계: JWT 토큰 검증 == + +AuthService -> JwtProvider: validateToken(token) +activate JwtProvider +JwtProvider -> JwtProvider: JWT 서명 검증\n(만료 시간 확인) +JwtProvider --> AuthService: boolean (유효 여부) +deactivate JwtProvider + +alt JWT 토큰 무효 + AuthService --> Controller: throw InvalidTokenException\n("유효하지 않은 토큰입니다") + Controller --> Client: 401 Unauthorized\n{"code": "AUTH_002",\n"error": "유효하지 않은 토큰입니다"} + deactivate AuthService + deactivate Controller + +else JWT 토큰 유효 + + == 2단계: Redis 세션 삭제 == + + AuthService -> Redis: 세션 삭제\n(캐시키: user:session:{token}) + activate Redis + Redis --> AuthService: 삭제된 키 개수 (0 또는 1) + deactivate Redis + + alt 세션 없음 (이미 로그아웃됨) + note right of AuthService + **멱등성 보장** + - 세션이 없어도 로그아웃 성공으로 처리 + - 중복 로그아웃 요청에 안전 + end note + else 세션 있음 (정상 로그아웃) + note right of AuthService + **세션 삭제 완료** + - Redis에서 세션 정보 제거 + - JWT 토큰 무효화 (Blacklist 방식) + end note + end + + == 3단계: JWT 토큰 Blacklist 추가 (선택적) == + + note right of AuthService + **JWT Blacklist 전략** + - 만료되지 않은 JWT 토큰을 강제로 무효화 + - Redis에 토큰을 Blacklist에 추가 (TTL: 남은 만료 시간) + - API Gateway에서 Blacklist 확인 + + **API Gateway 연계 시나리오** + 1. 로그아웃: AuthService가 Blacklist에 토큰 추가 + 2. 후속 API 요청: API Gateway가 Blacklist 확인 + - Redis GET jwt:blacklist:{token} + - 존재하면: 401 Unauthorized 즉시 반환 + - 존재하지 않으면: 백엔드 서비스로 라우팅 + 3. 만료 시간 도달: Redis TTL 만료로 자동 삭제 + end note + + AuthService -> JwtProvider: getRemainingExpiration(token) + activate JwtProvider + JwtProvider -> JwtProvider: JWT Claims에서\nexp(만료 시간) 추출\n(현재 시간과 비교) + JwtProvider --> AuthService: remainingSeconds + deactivate JwtProvider + + alt 남은 만료 시간 > 0 + AuthService -> Redis: 블랙리스트에 토큰 추가\n(캐시키: jwt:blacklist:{token},\n값: "revoked", TTL: 남은초) + activate Redis + Redis --> AuthService: Blacklist 추가 완료 + deactivate Redis + end + + == 4단계: 로그아웃 이벤트 발행 (선택적) == + + note right of AuthService + **로그아웃 로깅 및 이벤트** + - 감사 로그 기록: userId, timestamp, IP + - 이벤트 발행: LOGOUT_SUCCESS + - 분석 데이터 수집: 세션 지속 시간, 활동 통계 + end note + + AuthService -> AuthService: 로그아웃 성공 로그 기록\n(userId, timestamp, sessionDuration) + + AuthService ->> AuthService: publishEvent(LOGOUT_SUCCESS) + note right of AuthService + **이벤트 활용** + - 비동기 이벤트 처리 + - 분석 시스템 연동 + - 감사 로그 저장소 전송 + end note + + == 5단계: 응답 반환 == + + AuthService --> Controller: LogoutResponse\n(success: true) + deactivate AuthService + + Controller --> Client: 200 OK\n{"success": true,\n"message": "안전하게 로그아웃되었습니다"} + deactivate Controller +end + +note over Controller, Redis +**보안 처리** +- JWT 토큰 Blacklist: 만료 전 토큰 강제 무효화 +- 멱등성 보장: 중복 로그아웃 요청에 안전 +- 세션 완전 삭제: Redis에서 세션 정보 제거 +- 감사 로그: userId, timestamp, IP, sessionDuration 기록 + +**API Gateway 연계** +- Blacklist 확인: GET jwt:blacklist:{token} +- 존재 시: 401 Unauthorized 즉시 반환 +- TTL 자동 관리: 만료 시간 도달 시 자동 삭제 + +**클라이언트 측 처리** +- 프론트엔드: LocalStorage 또는 Cookie에서 JWT 토큰 삭제 +- 로그인 화면으로 리다이렉트 +- 모든 인증 헤더 제거 + +**성능 목표** +- Redis 삭제 연산: O(1) 시간 복잡도 +- 평균 응답 시간: 0.1초 이내 +- P95 응답 시간: 0.2초 이내 + +**이벤트 처리** +- LOGOUT_SUCCESS 이벤트 발행 (비동기) +- 감사 로그 저장소 전송 +- 분석 데이터 수집 + +**에러 코드** +- AUTH_002: JWT 토큰 무효 +end note + +@enduml diff --git a/design/backend/sequence/inner/user-로그인.puml b/design/backend/sequence/inner/user-로그인.puml new file mode 100644 index 0000000..5afba33 --- /dev/null +++ b/design/backend/sequence/inner/user-로그인.puml @@ -0,0 +1,147 @@ +@startuml user-로그인 +!theme mono + +title User Service - 로그인 내부 시퀀스 (UFR-USER-020) + +actor Client +participant "UserController" as Controller <> +participant "UserService" as Service <> +participant "AuthenticationService" as AuthService <> +participant "UserRepository" as UserRepo <> +participant "PasswordEncoder" as PwdEncoder <> +participant "JwtTokenProvider" as JwtProvider <> +participant "Redis\nCache" as Redis <> +participant "User DB\n(PostgreSQL)" as UserDB <> + +note over Controller, UserDB +**UFR-USER-020: 로그인** +- 입력: 이메일, 비밀번호 +- 비밀번호 검증 (bcrypt compare) +- JWT 토큰 발급 +- 세션 저장 (Redis) +- 최종 로그인 시각 업데이트 +end note + +Client -> Controller: POST /api/users/login\n{"email": "user@example.com",\n"password": "password123"} +activate Controller + +Controller -> Controller: 입력값 검증\n(필수 필드, 이메일 형식 확인) + +Controller -> AuthService: authenticate(email, password) +activate AuthService + +== 1단계: 사용자 조회 == + +AuthService -> Service: findByEmail(email) +activate Service +Service -> UserRepo: findByEmail(email) +activate UserRepo +UserRepo -> UserDB: 이메일로 사용자 조회\n(사용자ID, 비밀번호해시, 역할,\n이름, 전화번호 조회) +activate UserDB +UserDB --> UserRepo: 사용자 정보 반환 또는 없음 +deactivate UserDB +UserRepo --> Service: Optional +deactivate UserRepo +Service --> AuthService: Optional +deactivate Service + +alt 사용자 없음 + AuthService --> Controller: throw AuthenticationFailedException\n("이메일 또는 비밀번호를 확인해주세요") + Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "이메일 또는 비밀번호를\n확인해주세요"} + deactivate AuthService + deactivate Controller + +else 사용자 존재 + + == 2단계: 비밀번호 검증 == + + AuthService -> PwdEncoder: matches(rawPassword, passwordHash) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt compare\n(입력 비밀번호 vs 저장된 해시) + PwdEncoder --> AuthService: boolean (일치 여부) + deactivate PwdEncoder + + alt 비밀번호 불일치 + AuthService --> Controller: throw AuthenticationFailedException\n("이메일 또는 비밀번호를 확인해주세요") + Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "이메일 또는 비밀번호를\n확인해주세요"} + deactivate AuthService + deactivate Controller + + else 비밀번호 일치 + + == 3단계: JWT 토큰 생성 == + + AuthService -> JwtProvider: generateToken(userId, role) + activate JwtProvider + JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일) + JwtProvider --> AuthService: JWT 토큰 + deactivate JwtProvider + + == 4단계: 세션 저장 == + + AuthService -> Redis: 세션 정보 저장\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7일 + activate Redis + Redis --> AuthService: 저장 완료 + deactivate Redis + + == 5단계: 최종 로그인 시각 업데이트 (비동기) == + + AuthService ->> Service: updateLastLoginAt(userId) + activate Service + note right of Service + **비동기 처리** + - @Async 어노테이션 사용 + - 로그인 응답 지연 방지 + - 별도 스레드풀에서 실행 + end note + Service ->> UserRepo: updateLastLoginAt(userId) + activate UserRepo + UserRepo ->> UserDB: 사용자 최종 로그인 시각 갱신\n(현재 시각으로 업데이트) + activate UserDB + UserDB -->> UserRepo: 업데이트 완료 + deactivate UserDB + UserRepo -->> Service: void + deactivate UserRepo + Service -->> AuthService: void (비동기 완료) + deactivate Service + + note over AuthService, Service + **비동기 화살표 설명** + - `->>`: 비동기 호출 (호출 후 즉시 반환) + - `-->>`: 비동기 응답 (별도 스레드에서 완료) + end note + + == 6단계: 응답 반환 == + + AuthService -> AuthService: 로그인 응답 DTO 생성 + AuthService --> Controller: LoginResponse\n{token, userId, userName, role, email} + deactivate AuthService + + Controller --> Client: 200 OK\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"} + deactivate Controller + end +end + +note over Controller, UserDB +**보안 처리** +- 비밀번호: bcrypt compare (원본 노출 안 됨) +- 에러 메시지: 이메일/비밀번호 구분 없이 동일 메시지 반환 (Timing Attack 방어) +- JWT 토큰: 7일 만료, 서버 세션과 동기화 + +**보안 강화 (향후 구현)** +- Rate Limiting: IP당 5분에 5회 로그인 실패 시 임시 차단 (15분) +- Account Lockout: 동일 계정 10회 실패 시 계정 잠금 (관리자 해제) +- MFA: 2단계 인증 추가 (SMS/TOTP) +- Anomaly Detection: 비정상 로그인 패턴 감지 (지역, 디바이스 변경) + +**성능 최적화** +- 최종 로그인 시각 업데이트: 비동기 처리 (@Async) +- 평균 응답 시간: 0.5초 이내 +- P95 응답 시간: 1.0초 이내 +- Redis 세션 조회: 0.1초 이내 + +**에러 코드** +- AUTH_001: 인증 실패 (이메일 또는 비밀번호 불일치) +end note + +@enduml diff --git a/design/backend/sequence/inner/user-프로필수정.puml b/design/backend/sequence/inner/user-프로필수정.puml new file mode 100644 index 0000000..1bbaebf --- /dev/null +++ b/design/backend/sequence/inner/user-프로필수정.puml @@ -0,0 +1,233 @@ +@startuml user-프로필수정 +!theme mono + +title User Service - 프로필 수정 내부 시퀀스 (UFR-USER-030) + +actor Client +participant "UserController" as Controller <> +participant "UserService" as Service <> +participant "UserRepository" as UserRepo <> +participant "StoreRepository" as StoreRepo <> +participant "PasswordEncoder" as PwdEncoder <> +participant "Redis\nCache" as Redis <> +participant "User DB\n(PostgreSQL)" as UserDB <> + +note over Controller, UserDB +**UFR-USER-030: 프로필 수정** +- 기본 정보: 이름, 전화번호, 이메일 +- 매장 정보: 매장명, 업종, 주소, 영업시간 +- 비밀번호 변경 (현재 비밀번호 확인 필수) +- 전화번호 변경 시 재인증 필요 (향후 구현) +end note + +Client -> Controller: PUT /api/users/profile\nAuthorization: Bearer {JWT}\n(UpdateProfileRequest DTO) +activate Controller + +Controller -> Controller: @AuthenticationPrincipal\n(JWT에서 userId 추출) + +Controller -> Controller: @Valid 어노테이션 검증\n(이메일 형식, 필드 길이 등) + +Controller -> Service: updateProfile(userId, UpdateProfileRequest) +activate Service + +== 1단계: 기존 사용자 정보 조회 == + +Service -> UserRepo: findById(userId) +activate UserRepo +UserRepo -> UserDB: 사용자ID로 사용자 조회\n(사용자 정보 조회) +activate UserDB +UserDB --> UserRepo: 사용자 정보 +deactivate UserDB +UserRepo --> Service: User 엔티티 +deactivate UserRepo + +alt 사용자 없음 + Service --> Controller: throw UserNotFoundException\n("사용자를 찾을 수 없습니다") + Controller --> Client: 404 Not Found\n{"code": "USER_003",\n"error": "사용자를 찾을 수 없습니다"} + deactivate Service + deactivate Controller + +else 사용자 존재 + + == 2단계: 비밀번호 변경 요청 처리 == + + alt 비밀번호 변경 요청 O + Service -> Service: 현재 비밀번호 검증 필요 확인 + + Service -> PwdEncoder: matches(currentPassword,\nuser.getPasswordHash()) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt compare + PwdEncoder --> Service: boolean (일치 여부) + deactivate PwdEncoder + + alt 현재 비밀번호 불일치 + Service --> Controller: throw InvalidPasswordException\n("현재 비밀번호가 일치하지 않습니다") + Controller --> Client: 400 Bad Request\n{"code": "USER_004",\n"error": "현재 비밀번호가\n일치하지 않습니다"} + deactivate Service + deactivate Controller + + else 현재 비밀번호 일치 + Service -> Service: 새 비밀번호 유효성 검증\n(8자 이상, 영문/숫자/특수문자) + + Service -> PwdEncoder: encode(newPassword) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt 해싱\n(Cost Factor 10) + PwdEncoder --> Service: newPasswordHash + deactivate PwdEncoder + + Service -> Service: user.setPasswordHash(newPasswordHash) + end + end + + == 3단계: 엔티티 수정 준비 (메모리상 변경) == + + note right of Service + **JPA Dirty Checking** + - 트랜잭션 시작 전 엔티티 수정 (메모리상) + - 트랜잭션 커밋 시 변경 감지하여 UPDATE 자동 실행 + - 변경된 필드만 UPDATE 쿼리에 포함 + end note + + alt 이름 변경 + Service -> Service: user.setName(newName) + end + + alt 전화번호 변경 + Service -> Service: user.setPhoneNumber(newPhoneNumber) + note right of Service + **향후 구현: 재인증 필요** + - SMS 인증 또는 이메일 인증 + - 인증 완료 후에만 변경 반영 + end note + end + + alt 이메일 변경 + Service -> Service: user.setEmail(newEmail) + end + + == 4단계: 매장 정보 수정 준비 (메모리상 변경) == + + Service -> StoreRepo: findByUserId(userId) + activate StoreRepo + StoreRepo -> UserDB: 사용자ID로 매장 조회\n(매장 정보 조회) + activate UserDB + UserDB --> StoreRepo: 매장 정보 + deactivate UserDB + StoreRepo --> Service: Store 엔티티 + deactivate StoreRepo + + alt 매장명 변경 + Service -> Service: store.setStoreName(newStoreName) + end + + alt 업종 변경 + Service -> Service: store.setIndustry(newIndustry) + end + + alt 주소 변경 + Service -> Service: store.setAddress(newAddress) + end + + alt 영업시간 변경 + Service -> Service: store.setBusinessHours(newBusinessHours) + end + + == 5단계: 데이터베이스 트랜잭션 == + + Service -> UserDB: 트랜잭션 시작 + activate UserDB + + note right of Service + **Optimistic Locking** + - @Version 필드로 동시 수정 감지 + - 다른 트랜잭션이 먼저 수정한 경우 + - OptimisticLockException 발생 + end note + + Service -> UserRepo: save(user) + activate UserRepo + UserRepo -> UserDB: 사용자 정보 업데이트\n(이름, 전화번호, 이메일,\n비밀번호해시, 수정일시,\n버전 증가)\nOptimistic Lock 적용 + UserDB --> UserRepo: 업데이트 완료 (1 row affected) + UserRepo --> Service: User 엔티티 + deactivate UserRepo + + alt 동시성 충돌 (version 불일치) + UserRepo --> Service: throw OptimisticLockException + Service --> Controller: throw ConcurrentModificationException\n("다른 사용자가 수정 중입니다") + Controller --> Client: 409 Conflict\n{"code": "USER_005",\n"error": "다른 세션에서 프로필을\n수정했습니다.\n새로고침 후 다시 시도하세요"} + Service -> UserDB: 트랜잭션 롤백 + deactivate UserDB + deactivate Service + deactivate Controller + else 정상 업데이트 + + Service -> StoreRepo: save(store) + activate StoreRepo + StoreRepo -> UserDB: 매장 정보 업데이트\n(매장명, 업종, 주소,\n영업시간, 수정일시,\n버전 증가)\nOptimistic Lock 적용 + UserDB --> StoreRepo: 업데이트 완료 (1 row affected) + StoreRepo --> Service: Store 엔티티 + deactivate StoreRepo + + Service -> UserDB: 트랜잭션 커밋 + UserDB --> Service: 트랜잭션 커밋 완료 + deactivate UserDB + + == 6단계: 캐시 무효화 (선택적) == + + note right of Service + **캐시 무효화 전략** + - 세션 정보는 변경 없음 (JWT 유지) + - 프로필 캐시가 있다면 무효화 + end note + + alt 프로필 캐시 사용 중 + Service -> Redis: 프로필 캐시 삭제\n(캐시키: user:profile:{userId}) + activate Redis + Redis --> Service: 캐시 삭제 완료 + deactivate Redis + end + + == 7단계: 응답 반환 == + + Service -> Service: 응답 DTO 생성\n(UpdateProfileResponse) + Service --> Controller: UpdateProfileResponse\n(userId, userName, email,\nstoreId, storeName) + deactivate Service + + Controller --> Client: 200 OK\n{"userId": 123,\n"userName": "홍길동",\n"email": "hong@example.com",\n"storeId": 456,\n"storeName": "맛있는집"} + deactivate Controller + end +end + +note over Controller, UserDB +**Transaction Rollback 처리** +- 트랜잭션 실패 시 자동 Rollback +- User/Store UPDATE 중 하나라도 실패 시 전체 롤백 +- OptimisticLockException 발생 시 409 Conflict 반환 + +**동시성 제어** +- Optimistic Locking: @Version 필드로 동시 수정 감지 +- 충돌 감지 시: 409 Conflict 반환 (사용자에게 재시도 안내) +- Lost Update 방지: version 필드 자동 증가 + +**보안 처리** +- 비밀번호 변경: 현재 비밀번호 확인 필수 +- JWT 인증: Controller에서 @AuthenticationPrincipal로 userId 추출 +- 권한 검증: 본인만 수정 가능 + +**성능 목표** +- 평균 응답 시간: 0.3초 이내 +- P95 응답 시간: 0.5초 이내 +- 트랜잭션 격리 수준: READ_COMMITTED + +**향후 개선사항** +- 전화번호 변경: SMS/이메일 재인증 구현 +- 이메일 변경: 이메일 인증 구현 +- 변경 이력 추적: Audit Log 기록 + +**에러 코드** +- USER_003: 사용자 없음 +- USER_004: 현재 비밀번호 불일치 +- USER_005: 동시성 충돌 (다른 세션에서 수정) +end note + +@enduml diff --git a/design/backend/sequence/inner/user-회원가입.puml b/design/backend/sequence/inner/user-회원가입.puml new file mode 100644 index 0000000..dfd6c71 --- /dev/null +++ b/design/backend/sequence/inner/user-회원가입.puml @@ -0,0 +1,149 @@ +@startuml user-회원가입 +!theme mono + +title User Service - 회원가입 내부 시퀀스 (UFR-USER-010) + +participant "UserController" as Controller <> +participant "UserService" as Service <> +participant "UserRepository" as UserRepo <> +participant "StoreRepository" as StoreRepo <> +participant "PasswordEncoder" as PwdEncoder <> +participant "JwtTokenProvider" as JwtProvider <> +participant "Redis\nCache" as Redis <> +participant "User DB\n(PostgreSQL)" as UserDB <> +actor Client + +note over Controller, UserDB +**UFR-USER-010: 회원가입** +- 기본 정보: 이름, 전화번호, 이메일, 비밀번호 +- 매장 정보: 매장명, 업종, 주소, 영업시간, 사업자번호 +- 이메일/전화번호 중복 검사 +- 트랜잭션 처리 +- JWT 토큰 발급 +end note + +Client -> Controller: POST /api/users/register\n{"name": "홍길동",\n"phoneNumber": "01012345678",\n"email": "hong@example.com",\n"password": "password123"} +activate Controller + +Controller -> Controller: 입력값 검증\n(이메일 형식, 비밀번호 8자 이상 등) + +Controller -> Service: register(RegisterRequest) +activate Service + +== 1단계: 이메일 중복 확인 == + +Service -> UserRepo: findByEmail(email) +activate UserRepo +UserRepo -> UserDB: 이메일로 사용자 조회\n(중복 가입 확인) +activate UserDB +UserDB --> UserRepo: 조회 결과 반환 또는 없음 +deactivate UserDB +UserRepo --> Service: Optional +deactivate UserRepo + +alt 이메일 중복 존재 + Service --> Controller: throw DuplicateEmailException\n("이미 가입된 이메일입니다") + Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"message": "이미 가입된 이메일입니다"} + deactivate Service + deactivate Controller + +else 이메일 신규 + + == 2단계: 전화번호 중복 확인 == + + Service -> UserRepo: findByPhoneNumber(phoneNumber) + activate UserRepo + UserRepo -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인) + activate UserDB + UserDB --> UserRepo: 조회 결과 반환 또는 없음 + deactivate UserDB + UserRepo --> Service: Optional + deactivate UserRepo + + alt 전화번호 중복 존재 + Service --> Controller: throw DuplicatePhoneException\n("이미 가입된 전화번호입니다") + Controller --> Client: 400 Bad Request\n{"code": "USER_002",\n"message": "이미 가입된 전화번호입니다"} + deactivate Service + deactivate Controller + + else 신규 사용자 + + == 3단계: 비밀번호 해싱 == + + Service -> PwdEncoder: encode(rawPassword) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt 해싱\n(Cost Factor 10) + PwdEncoder --> Service: passwordHash + deactivate PwdEncoder + + == 4단계: 데이터베이스 트랜잭션 == + + Service -> UserDB: 트랜잭션 시작 + activate UserDB + + Service -> UserRepo: save(User) + activate UserRepo + UserRepo -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시)\n저장 후 사용자ID 반환 + UserDB --> UserRepo: 생성된 사용자ID + UserRepo --> Service: User 엔티티\n(userId 포함) + deactivate UserRepo + + Service -> StoreRepo: save(Store) + activate StoreRepo + StoreRepo -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 사업자번호, 영업시간)\n저장 후 매장ID 반환 + UserDB --> StoreRepo: 생성된 매장ID + StoreRepo --> Service: Store 엔티티\n(storeId 포함) + deactivate StoreRepo + + Service -> UserDB: 트랜잭션 커밋 + UserDB --> Service: 커밋 완료 + deactivate UserDB + + == 5단계: JWT 토큰 생성 == + + Service -> JwtProvider: generateToken(userId, role) + activate JwtProvider + JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일) + JwtProvider --> Service: JWT 토큰 + deactivate JwtProvider + + == 6단계: 세션 저장 == + + Service -> Redis: 세션 정보 저장\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7일 + activate Redis + Redis --> Service: 저장 완료 + deactivate Redis + + == 7단계: 응답 반환 == + + Service -> Service: 회원가입 응답 DTO 생성 + Service --> Controller: RegisterResponse\n{token, userId, userName, storeId, storeName} + deactivate Service + + Controller --> Client: 201 Created\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "홍길동",\n"storeId": 456,\n"storeName": "맛있는집"} + deactivate Controller + end + end +end + +note over Controller, UserDB +**Transaction Rollback 처리** +- 트랜잭션 실패 시 자동 Rollback +- User/Store INSERT 중 하나라도 실패 시 전체 롤백 +- 예외: DataAccessException, ConstraintViolationException + +**보안 처리** +- 비밀번호: bcrypt 해싱 (Cost Factor 10) +- JWT 토큰: 7일 만료, 서버 세션과 동기화 + +**성능 목표** +- 평균 응답 시간: 1.0초 이내 +- P95 응답 시간: 1.5초 이내 +- 트랜잭션 처리: 0.5초 이내 + +**에러 코드** +- USER_001: 이메일 중복 +- USER_002: 전화번호 중복 +end note + +@enduml diff --git a/design/backend/sequence/outer/README.md b/design/backend/sequence/outer/README.md new file mode 100644 index 0000000..99a1be5 --- /dev/null +++ b/design/backend/sequence/outer/README.md @@ -0,0 +1,304 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 외부 시퀀스 설계 + +## 문서 정보 +- **작성일**: 2025-10-22 +- **작성자**: System Architect +- **버전**: 1.0 +- **관련 문서**: + - [유저스토리](../../../userstory.md) + - [논리 아키텍처](../../logical/logical-architecture.md) + - [UI/UX 설계서](../../../uiux/uiux.md) + +--- + +## 개요 + +본 문서는 KT AI 기반 소상공인 이벤트 자동 생성 서비스의 **외부 시퀀스 설계**를 정의합니다. +외부 시퀀스는 서비스 간의 상호작용과 데이터 흐름을 표현하며, 유저스토리와 논리 아키텍처를 기반으로 설계되었습니다. + +### 설계 원칙 +1. **유저스토리 기반**: 20개 유저스토리와 정확히 매칭 +2. **Event-Driven 아키텍처**: Kafka를 통한 비동기 이벤트 발행/구독 +3. **Resilience 패턴**: Circuit Breaker, Retry, Timeout, Fallback 적용 +4. **Cache-Aside 패턴**: Redis 캐싱을 통한 성능 최적화 +5. **서비스 독립성**: 느슨한 결합과 장애 격리 + +--- + +## 외부 시퀀스 플로우 목록 + +총 **4개의 주요 비즈니스 플로우**로 구성되어 있습니다: + +### 1. 사용자 인증 플로우 +**파일**: `사용자인증플로우.puml` + +**포함된 유저스토리**: +- UFR-USER-010: 회원가입 +- UFR-USER-020: 로그인 +- UFR-USER-040: 로그아웃 + +**주요 참여자**: +- Frontend (Web/Mobile) +- API Gateway +- User Service +- Redis Cache +- User DB (PostgreSQL) +- 국세청 API (외부) + +**핵심 기능**: +- JWT 기반 인증 +- 사업자번호 검증 (Circuit Breaker 적용) +- Redis 캐싱 (사업자번호 검증 결과, TTL 7일) +- 비밀번호 해싱 (bcrypt) +- 사업자번호 암호화 (AES-256) + +**Resilience 패턴**: +- Circuit Breaker: 국세청 API (실패율 50% 초과 시 Open) +- Retry: 최대 3회 재시도 (지수 백오프: 1초, 2초, 4초) +- Timeout: 5초 +- Fallback: 사업자번호 검증 스킵 (수동 확인 안내) + +--- + +### 2. 이벤트 생성 플로우 +**파일**: `이벤트생성플로우.puml` + +**포함된 유저스토리**: +- UFR-EVENT-020: 이벤트 목적 선택 +- UFR-EVENT-030: AI 이벤트 추천 +- UFR-CONT-010: SNS 이미지 생성 +- UFR-EVENT-050: 최종 승인 및 배포 + +**주요 참여자**: +- Frontend +- API Gateway +- Event Service +- AI Service (Kafka 구독) +- Content Service (Kafka 구독) +- Distribution Service (동기 호출) +- Kafka (Event Topics + Job Topics) +- Redis Cache +- Event DB +- 외부 API (AI API, 이미지 생성 API, 배포 채널 APIs) + +**핵심 기능**: +1. **이벤트 목적 선택** (동기) + - Event DB에 목적 저장 + - EventCreated 이벤트 발행 + +2. **AI 이벤트 추천** (비동기) + - Kafka ai-job 토픽 발행 + - AI Service 구독 및 처리 + - Polling 패턴으로 Job 상태 확인 (최대 30초) + - Redis 캐싱 (TTL 24시간) + +3. **SNS 이미지 생성** (비동기) + - Kafka image-job 토픽 발행 + - Content Service 구독 및 처리 + - Polling 패턴으로 Job 상태 확인 (최대 20초) + - CDN 업로드 및 Redis 캐싱 (TTL 7일) + +4. **최종 승인 및 배포** (동기) + - Distribution Service REST API 직접 호출 + - 다중 채널 병렬 배포 (1분 이내) + - DistributionCompleted 이벤트 발행 + +**Resilience 패턴**: +- Circuit Breaker: 모든 외부 API 호출 시 적용 +- Retry: 최대 3회 재시도 (지수 백오프) +- Timeout: AI API 30초, 이미지 API 20초, 배포 API 10초 +- Bulkhead: 채널별 스레드 풀 격리 +- Fallback: AI 추천 시 캐시된 이전 결과, 이미지 생성 시 기본 템플릿 + +--- + +### 3. 고객 참여 플로우 +**파일**: `고객참여플로우.puml` + +**포함된 유저스토리**: +- UFR-PART-010: 이벤트 참여 +- UFR-PART-030: 당첨자 추첨 + +**주요 참여자**: +- Frontend (고객용 / 사장님용) +- API Gateway +- Participation Service +- Kafka (Event Topics) +- Participation DB +- Analytics Service (이벤트 구독) + +**핵심 기능**: +1. **이벤트 참여** + - 중복 참여 체크 (전화번호 기반) + - 응모 번호 발급 + - ParticipantRegistered 이벤트 발행 → Analytics Service 구독 + +2. **당첨자 추첨** + - 난수 기반 무작위 추첨 (Crypto.randomBytes) + - Fisher-Yates Shuffle 알고리즘 + - 매장 방문 고객 가산점 적용 (선택 옵션) + - WinnerSelected 이벤트 발행 + +**Event-Driven 특징**: +- Analytics Service가 ParticipantRegistered 이벤트 구독하여 실시간 통계 업데이트 +- 서비스 간 직접 의존성 없이 이벤트로 느슨한 결합 + +--- + +### 4. 성과 분석 플로우 +**파일**: `성과분석플로우.puml` + +**포함된 유저스토리**: +- UFR-ANAL-010: 실시간 성과분석 대시보드 조회 + +**주요 참여자**: +- Frontend +- API Gateway +- Analytics Service +- Redis Cache (TTL 5분) +- Analytics DB +- Kafka (Event Topics 구독) +- 외부 API (우리동네TV, 지니TV, SNS APIs) + +**핵심 기능**: +1. **대시보드 조회 - Cache HIT** (0.5초) + - Redis에서 캐시된 데이터 즉시 반환 + - 히트율 목표: 95% + +2. **대시보드 조회 - Cache MISS** (3초) + - Analytics DB 로컬 데이터 조회 + - 외부 채널 API 병렬 호출 (Circuit Breaker 적용) + - 데이터 통합 및 ROI 계산 + - Redis 캐싱 (TTL 5분) + +3. **실시간 업데이트 (Background)** + - EventCreated 구독: 이벤트 기본 정보 초기화 + - ParticipantRegistered 구독: 참여자 수 실시간 증가 + - DistributionCompleted 구독: 배포 채널 통계 업데이트 + +**Resilience 패턴**: +- Circuit Breaker: 외부 채널 API 조회 시 (실패율 50% 초과 시 Open) +- Timeout: 10초 +- Fallback: 캐시된 이전 데이터 반환 또는 기본값 설정 +- 병렬 처리: 외부 채널 API 동시 호출 + +--- + +## 설계 특징 + +### 1. Event-Driven 아키텍처 +- **Kafka 통합**: Event Topics와 Job Topics를 Kafka로 통합 +- **느슨한 결합**: 서비스 간 직접 의존성 제거 +- **장애 격리**: 한 서비스 장애가 다른 서비스에 영향 없음 +- **확장 용이**: 새로운 구독자 추가로 기능 확장 + +### 2. 비동기 처리 패턴 +- **Kafka Job Topics**: ai-job, image-job +- **Polling 패턴**: Job 상태 확인 (2-5초 간격) +- **처리 시간**: AI 추천 10초, 이미지 생성 5초 + +### 3. 동기 처리 패턴 +- **Distribution Service**: REST API 직접 호출 +- **다중 채널 배포**: 병렬 처리 (1분 이내) +- **Circuit Breaker**: 장애 전파 방지 + +### 4. Resilience 패턴 전면 적용 +- **Circuit Breaker**: 모든 외부 API 호출 +- **Retry**: 일시적 장애 자동 복구 +- **Timeout**: 응답 시간 제한 +- **Bulkhead**: 리소스 격리 +- **Fallback**: 장애 시 대체 로직 + +### 5. Cache-Aside 패턴 +- **Redis 캐싱**: 성능 최적화 +- **TTL 설정**: 데이터 유효성 관리 +- **히트율 목표**: 80-95% +- **응답 시간 개선**: 90-99% + +--- + +## 파일 구조 + +``` +design/backend/sequence/outer/ +├── README.md (본 문서) +├── 사용자인증플로우.puml +├── 이벤트생성플로우.puml +├── 고객참여플로우.puml +└── 성과분석플로우.puml +``` + +--- + +## 다이어그램 확인 방법 + +### 1. Online PlantUML Viewer +1. https://www.plantuml.com/plantuml/uml 접속 +2. `.puml` 파일 내용 붙여넣기 +3. 다이어그램 시각적 확인 + +### 2. VSCode Extension +1. "PlantUML" 확장 프로그램 설치 +2. `.puml` 파일 열기 +3. `Alt+D` 또는 `Cmd+D`로 미리보기 + +### 3. IntelliJ IDEA Plugin +1. "PlantUML integration" 플러그인 설치 +2. `.puml` 파일 열기 +3. 우측 미리보기 패널에서 확인 + +--- + +## 주요 결정사항 + +1. **Kafka 통합**: Event Bus와 Job Queue를 Kafka로 통합하여 운영 복잡도 감소 +2. **비동기 처리**: AI 추천 및 이미지 생성은 Kafka Job Topics를 통한 비동기 처리 +3. **동기 배포**: Distribution Service는 REST API 직접 호출하여 동기 처리 (1분 이내) +4. **Resilience 패턴**: 모든 외부 API 호출 시 Circuit Breaker, Retry, Timeout, Fallback 적용 +5. **Cache-Aside 패턴**: Redis 캐싱으로 응답 시간 90-99% 개선 +6. **Event Topics**: EventCreated, ParticipantRegistered, WinnerSelected, DistributionCompleted +7. **Job Topics**: ai-job, image-job + +--- + +## 검증 사항 + +### 1. 유저스토리 매칭 +✅ 모든 유저스토리가 외부 시퀀스에 정확히 반영됨 +- User 서비스: 4개 유저스토리 +- Event 서비스: 4개 유저스토리 +- Participation 서비스: 2개 유저스토리 +- Analytics 서비스: 1개 유저스토리 + +### 2. 논리 아키텍처 일치성 +✅ 논리 아키텍처의 모든 컴포넌트와 통신 패턴 반영 +- Core Services: User, Event, Participation, Analytics +- Async Services: AI, Content, Distribution +- Kafka: Event Topics + Job Topics +- External Systems: 국세청 API, AI API, 이미지 생성 API, 배포 채널 APIs + +### 3. Resilience 패턴 적용 +✅ 모든 외부 API 호출에 Resilience 패턴 적용 +- Circuit Breaker, Retry, Timeout, Bulkhead, Fallback + +### 4. PlantUML 문법 검증 +✅ PlantUML 기본 문법 검증 완료 +- `!theme mono` 적용 +- 동기/비동기 화살표 구분 +- 한글 설명 추가 +- 참여자 및 플로우 명확히 표현 + +--- + +## 향후 개선 방안 + +1. **WebSocket 기반 실시간 푸시**: 대시보드 실시간 업데이트 (폴링 대체) +2. **Saga 패턴 적용**: 복잡한 분산 트랜잭션 보상 로직 체계화 +3. **Service Mesh 도입**: Istio를 통한 서비스 간 통신 관찰성 및 보안 강화 +4. **Dead Letter Queue 고도화**: 실패 이벤트 재처리 및 알림 자동화 + +--- + +**문서 버전**: 1.0 +**최종 수정일**: 2025-10-22 +**작성자**: System Architect diff --git a/design/backend/sequence/outer/고객참여플로우.puml b/design/backend/sequence/outer/고객참여플로우.puml new file mode 100644 index 0000000..a942530 --- /dev/null +++ b/design/backend/sequence/outer/고객참여플로우.puml @@ -0,0 +1,164 @@ +@startuml 고객참여플로우 +!theme mono + +title 고객 참여 플로우 - 외부 시퀀스 다이어그램 + +actor "고객" as Customer +participant "Frontend\n(고객용)" as CustomerFE +participant "API Gateway" as Gateway +participant "Participation\nService" as PartService +participant "Kafka\n(Event Topics)" as Kafka +database "Participation\nDB" as PartDB +participant "Analytics\nService" as Analytics + +actor "사장님" as Owner +participant "Frontend\n(사장님용)" as OwnerFE + +== UFR-PART-010: 이벤트 참여 == + +Customer -> CustomerFE: 이벤트 참여 화면 접근\n(우리동네TV/SNS/링고비즈) +activate CustomerFE + +CustomerFE -> Customer: 참여 정보 입력 폼 표시\n(이름, 전화번호, 참여경로) + +Customer -> CustomerFE: 참여 정보 입력 및\n참여 버튼 클릭 +CustomerFE -> CustomerFE: 클라이언트 유효성 검증\n(이름 2자 이상, 전화번호 형식) + +CustomerFE -> Gateway: POST /api/v1/participations\n{이름, 전화번호, 참여경로, 개인정보동의} +activate Gateway + +Gateway -> PartService: POST /participations/register\n{이름, 전화번호, 참여경로, 개인정보동의} +activate PartService + +PartService -> PartDB: 참여자 중복 확인\n(전화번호, 이벤트ID로 조회) +activate PartDB +PartDB --> PartService: 중복 참여 여부 반환 +deactivate PartDB + +alt 중복 참여인 경우 + PartService --> Gateway: 409 Conflict\n{message: "이미 참여하신 이벤트입니다"} + Gateway --> CustomerFE: 409 Conflict + CustomerFE -> Customer: 중복 참여 오류 메시지 표시 + deactivate PartService + deactivate Gateway + deactivate CustomerFE +else 신규 참여인 경우 + PartService -> PartService: 응모 번호 생성\n(UUID 또는 시퀀스 기반) + + PartService -> PartDB: 참여자 정보 저장\n(이름, 전화번호, 참여경로,\n응모번호, 참여일시) + activate PartDB + PartDB --> PartService: 저장 완료 + deactivate PartDB + + PartService -> Kafka: Publish Event\n"ParticipantRegistered"\n{participantId, eventId,\nentryPath, timestamp} + activate Kafka + note right of Kafka + Topic: participant-events + Event: ParticipantRegistered + Data: { + participantId: UUID, + eventId: UUID, + entryPath: string, + timestamp: datetime + } + end note + + Kafka --> Analytics: Subscribe Event\n"ParticipantRegistered" + activate Analytics + Analytics -> Analytics: 참여자 데이터 집계\n- 채널별 참여자 수\n- 시간대별 참여 추이\n- 실시간 통계 업데이트 + deactivate Analytics + deactivate Kafka + + PartService --> Gateway: 201 Created\n{응모번호, 당첨발표일, 참여완료메시지} + deactivate PartService + + Gateway --> CustomerFE: 201 Created + deactivate Gateway + + CustomerFE -> Customer: 참여 완료 화면 표시\n- 응모번호\n- 당첨 발표일\n- "참여해주셔서 감사합니다" + deactivate CustomerFE +end + +== UFR-PART-020: 참여자 목록 조회 == + +Owner -> OwnerFE: 이벤트 상세 화면에서\n"참여자 목록" 탭 클릭 +activate OwnerFE + +OwnerFE -> Gateway: GET /api/v1/events/{eventId}/participants\n?page=1&size=20 +activate Gateway + +Gateway -> PartService: GET /events/{eventId}/participants\n?page=1&size=20 +activate PartService + +PartService -> PartDB: 참여자 목록 조회\n(이벤트ID, 페이지네이션)\nORDER BY 참여일시 DESC +activate PartDB +PartDB --> PartService: 참여자 목록 반환\n(이름, 전화번호, 참여경로,\n응모번호, 참여일시)\n+ 총 참여자 수 +deactivate PartDB + +PartService --> Gateway: 200 OK\n{participants[], totalCount, page, size} +deactivate PartService + +Gateway --> OwnerFE: 200 OK +deactivate Gateway + +OwnerFE -> Owner: 참여자 목록 화면 표시\n- 참여자 정보 테이블\n- 페이지네이션\n- 총 참여자 수\n- CSV 다운로드 버튼 +deactivate OwnerFE + +note right of Owner + 참여자 정보: + - 이름 (마스킹: 김**) + - 전화번호 (마스킹: 010-****-1234) + - 참여경로 (우리동네TV, Instagram 등) + - 응모번호 + - 참여일시 +end note + +== UFR-PART-030: 당첨자 추첨 == + +Owner -> OwnerFE: 이벤트 상세 화면에서\n"당첨자 추첨" 버튼 클릭 +activate OwnerFE + +OwnerFE -> Owner: 추첨 확인 다이얼로그 표시\n"당첨자를 추첨하시겠습니까?" + +Owner -> OwnerFE: 확인 버튼 클릭 + +OwnerFE -> Gateway: POST /api/v1/events/{eventId}/draw-winners\n{당첨인원, 매장방문가산점옵션} +activate Gateway + +Gateway -> PartService: POST /events/{eventId}/draw-winners\n{winnerCount, visitBonus} +activate PartService + +PartService -> PartDB: 미당첨 참여자 목록 조회\n(이벤트ID로 당첨되지 않은 참여자 조회) +activate PartDB +PartDB --> PartService: 전체 참여자 목록 반환 +deactivate PartDB + +PartService -> PartService: 당첨자 추첨 알고리즘 실행\n1. 난수 생성 (Crypto.randomBytes)\n2. 매장방문 가산점 적용 (옵션)\n3. Fisher-Yates Shuffle\n4. 당첨인원만큼 선정 + +PartService -> PartDB: 당첨자 정보 업데이트\n(당첨 여부를 true로 설정, 당첨 일시 기록) +activate PartDB +PartDB --> PartService: 업데이트 완료 +deactivate PartDB + +PartService -> PartDB: 추첨 로그 저장\n(이벤트ID, 추첨방법, 당첨인원,\n알고리즘, 추첨일시) +activate PartDB +note right of PartDB + 추첨 로그 저장: + - 추첨 일시 + - 추첨 방법 + - 알고리즘 버전 + - 가산점 적용 여부 +end note +PartDB --> PartService: 로그 저장 완료 +deactivate PartDB + +PartService --> Gateway: 200 OK\n{당첨자목록, 추첨로그ID} +deactivate PartService + +Gateway --> OwnerFE: 200 OK +deactivate Gateway + +OwnerFE -> Owner: 당첨자 목록 화면 표시\n- 당첨자 정보 (이름, 전화번호, 응모번호)\n- 추첨 완료 메시지 +deactivate OwnerFE + +@enduml diff --git a/design/backend/sequence/outer/사용자인증플로우.puml b/design/backend/sequence/outer/사용자인증플로우.puml new file mode 100644 index 0000000..e5d624b --- /dev/null +++ b/design/backend/sequence/outer/사용자인증플로우.puml @@ -0,0 +1,177 @@ +@startuml 사용자인증플로우 +!theme mono + +title KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 사용자 인증 플로우 (외부 시퀀스) + +actor "사용자\n(소상공인)" as User +participant "Frontend\n(Web/Mobile)" as Frontend +participant "API Gateway" as Gateway +participant "User Service" as UserService +database "User DB\n(PostgreSQL)" as UserDB + +== UFR-USER-010: 회원가입 플로우 == + +User -> Frontend: 회원가입 화면 접근 +activate Frontend + +User -> Frontend: 회원 정보 입력\n(이름, 전화번호, 이메일, 비밀번호,\n매장명, 업종, 주소, 사업자번호) +Frontend -> Frontend: 클라이언트 측 유효성 검증\n(이메일 형식, 비밀번호 8자 이상 등) + +Frontend -> Gateway: POST /api/users/register\n(회원 정보) +activate Gateway + +Gateway -> Gateway: Request 검증\n(필수 필드, 데이터 타입) + +Gateway -> UserService: POST /api/users/register\n(회원 정보) +activate UserService + +UserService -> UserService: 서버 측 유효성 검증\n(이름 2자 이상, 전화번호 형식 등) + +UserService -> UserDB: 이메일로 사용자 조회\n(중복 가입 확인) +activate UserDB +UserDB --> UserService: 기존 사용자 확인 결과 +deactivate UserDB + +alt 이메일 중복 존재 + UserService --> Gateway: 400 Bad Request\n(이미 등록된 이메일) + Gateway --> Frontend: 400 Bad Request + Frontend --> User: "이미 가입된 이메일입니다" +else 이메일 신규 + + UserService -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인) + activate UserDB + UserDB --> UserService: 기존 사용자 확인 결과 + deactivate UserDB + + alt 전화번호 중복 존재 + UserService --> Gateway: 400 Bad Request\n(이미 등록된 전화번호) + Gateway --> Frontend: 400 Bad Request + Frontend --> User: "이미 가입된 전화번호입니다" + else 신규 사용자 + + UserService -> UserService: 비밀번호 해싱\n(bcrypt, Cost Factor 10) + + UserService -> UserService: 사업자번호 암호화\n(AES-256) + + UserService -> UserDB: 트랜잭션 시작 + activate UserDB + + UserService -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시) + UserDB --> UserService: user_id 반환 + + UserService -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 암호화된사업자번호,\n영업시간) + UserDB --> UserService: store_id 반환 + + UserService -> UserDB: 트랜잭션 커밋 + deactivate UserDB + + UserService -> UserService: JWT 토큰 생성\n(user_id, role=OWNER,\nexp=7일) + + UserService --> Gateway: 201 Created\n(JWT 토큰, 사용자 정보) + deactivate UserService + + Gateway --> Frontend: 201 Created\n(JWT 토큰, 사용자 정보) + deactivate Gateway + + Frontend -> Frontend: JWT 토큰 저장\n(LocalStorage 또는 Cookie) + + Frontend --> User: "회원가입이 완료되었습니다" + + Frontend -> Gateway: 대시보드 화면으로 이동 + deactivate Frontend + end +end + +== UFR-USER-020: 로그인 플로우 == + +User -> Frontend: 로그인 화면 접근 +activate Frontend + +User -> Frontend: 이메일, 비밀번호 입력 + +Frontend -> Frontend: 클라이언트 측 유효성 검증\n(필수 필드 확인, 이메일 형식) + +Frontend -> Gateway: POST /api/users/login\n(이메일, 비밀번호) +activate Gateway + +Gateway -> Gateway: Request 검증 + +Gateway -> UserService: POST /api/users/login\n(이메일, 비밀번호) +activate UserService + +UserService -> UserDB: 이메일로 사용자 조회\n(로그인 인증용) +activate UserDB +UserDB --> UserService: 사용자 정보\n(user_id, password_hash, role) +deactivate UserDB + +alt 사용자 없음 + UserService --> Gateway: 401 Unauthorized\n(인증 실패) + Gateway --> Frontend: 401 Unauthorized + Frontend --> User: "이메일 또는 비밀번호를\n확인해주세요" +else 사용자 존재 + + UserService -> UserService: 비밀번호 검증\n(bcrypt compare) + + alt 비밀번호 불일치 + UserService --> Gateway: 401 Unauthorized\n(인증 실패) + Gateway --> Frontend: 401 Unauthorized + Frontend --> User: "이메일 또는 비밀번호를\n확인해주세요" + else 비밀번호 일치 + + UserService -> UserService: JWT 토큰 생성\n(user_id, role=OWNER,\nexp=7일) + + UserService -> UserDB: 최종 로그인 시각 업데이트\n(현재 시각으로 갱신) + activate UserDB + UserDB --> UserService: 업데이트 완료 + deactivate UserDB + + UserService --> Gateway: 200 OK\n(JWT 토큰, 사용자 정보) + deactivate UserService + + Gateway --> Frontend: 200 OK\n(JWT 토큰, 사용자 정보) + deactivate Gateway + + Frontend -> Frontend: JWT 토큰 저장\n(LocalStorage 또는 Cookie) + + Frontend --> User: 로그인 성공 + + Frontend -> Gateway: 대시보드 화면으로 이동 + deactivate Frontend + end +end + +== UFR-USER-040: 로그아웃 플로우 == + +User -> Frontend: 프로필 탭 접근 +activate Frontend + +User -> Frontend: "로그아웃" 버튼 클릭 + +Frontend -> Frontend: 확인 다이얼로그 표시\n"로그아웃 하시겠습니까?" + +User -> Frontend: "확인" 클릭 + +Frontend -> Gateway: POST /api/users/logout\nAuthorization: Bearer {JWT} +activate Gateway + +Gateway -> Gateway: JWT 토큰 검증 + +Gateway -> UserService: POST /api/users/logout\n(JWT 토큰) +activate UserService + +UserService -> UserService: JWT 토큰 블랙리스트에 추가\n(만료 시까지 유효) + +UserService --> Gateway: 200 OK\n(로그아웃 성공) +deactivate UserService + +Gateway --> Frontend: 200 OK +deactivate Gateway + +Frontend -> Frontend: JWT 토큰 삭제\n(LocalStorage 또는 Cookie) + +Frontend --> User: "안전하게 로그아웃되었습니다" + +Frontend -> Gateway: 로그인 화면으로 이동 +deactivate Frontend + +@enduml diff --git a/design/backend/sequence/outer/성과분석플로우.puml b/design/backend/sequence/outer/성과분석플로우.puml new file mode 100644 index 0000000..8db66c6 --- /dev/null +++ b/design/backend/sequence/outer/성과분석플로우.puml @@ -0,0 +1,198 @@ +@startuml 성과분석플로우_외부시퀀스 +!theme mono + +title 성과 분석 플로우 - 외부 시퀀스 다이어그램\n(UFR-ANAL-010: 실시간 성과분석 대시보드 조회) + +actor "소상공인" as User +participant "Frontend" as FE +participant "API Gateway" as GW +participant "Analytics Service" as Analytics +participant "Redis Cache\n(TTL 1시간)" as Redis +participant "Analytics DB" as AnalyticsDB +participant "Kafka\n(Event Topics)" as Kafka + +note over AnalyticsDB + **배치 처리로 수집된 데이터** + - 외부 채널 통계는 배치 작업으로 + 주기적으로 수집하여 DB에 저장 + - 목업 데이터로 시작, 점진적으로 실제 API 연동 +end note + +== 1. 대시보드 조회 - Cache HIT 시나리오 == + +User -> FE: 성과분석 대시보드 접근\n(Bottom Nav "분석" 탭 클릭) +activate FE + +FE -> GW: GET /api/events/{id}/analytics\n+ Authorization: Bearer {token} +activate GW + +GW -> GW: JWT 토큰 검증 +GW -> Analytics: GET /api/events/{id}/analytics +activate Analytics + +Analytics -> Redis: 대시보드 캐시 조회\n(캐시키: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: **Cache HIT**\n캐시된 대시보드 데이터 반환 +deactivate Redis + +note right of Analytics + **Cache-Aside 패턴** + - TTL: 1시간 + - 예상 크기: 5KB + - 히트율 목표: 95% + - 응답 시간: 0.5초 +end note + +Analytics --> GW: 200 OK\n대시보드 데이터 (JSON) +deactivate Analytics + +GW --> FE: 200 OK\n대시보드 데이터 +deactivate GW + +FE -> FE: 대시보드 렌더링\n- 4개 요약 카드\n- 채널별 성과 차트\n- 시간대별 참여 추이 +FE --> User: 실시간 대시보드 표시 +deactivate FE + +== 2. 대시보드 조회 - Cache MISS 시나리오 == + +User -> FE: 대시보드 새로고침\n또는 첫 조회 +activate FE + +FE -> GW: GET /api/events/{id}/analytics +activate GW + +GW -> Analytics: GET /api/events/{id}/analytics +activate Analytics + +Analytics -> Redis: 대시보드 캐시 조회\n(캐시키: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: **Cache MISS**\nnull 반환 +deactivate Redis + +note right of Analytics + **데이터 통합 작업 시작** + - Analytics DB 조회 + - 외부 채널 API 병렬 호출 + - Circuit Breaker 적용 +end note + +||| +== 2.1. Analytics DB 조회 (로컬 데이터) == + +Analytics -> AnalyticsDB: 이벤트 통계 조회\n(이벤트ID로 통계 데이터 조회) +activate AnalyticsDB +AnalyticsDB --> Analytics: 이벤트 통계\n- 총 참여자 수\n- 예상 ROI\n- 매출 증가율 +deactivate AnalyticsDB + +||| +== 2.2. 배치 수집된 채널 통계 데이터 조회 == + +Analytics -> AnalyticsDB: 채널별 통계 조회\n(배치로 수집된 채널 데이터 조회) +activate AnalyticsDB + +note right of Analytics + **배치 처리 방식** + - 외부 API는 별도 배치 작업으로 주기적 수집 + - 수집된 데이터는 DB에 저장 + - 대시보드에서는 DB 데이터만 조회 + - 응답 시간 단축 및 외부 API 의존성 제거 +end note + +AnalyticsDB --> Analytics: 채널별 통계 데이터\n- 우리동네TV: 노출 5,000, 조회 1,200\n- 지니TV: 노출 10,000, 클릭 500\n- Instagram: 좋아요 300, 댓글 50\n- Naver: 조회 2,000\n- Kakao: 공유 100 +deactivate AnalyticsDB + +note right of Analytics + **목업 데이터 활용** + - 초기에는 목업 데이터로 시작 + - 점진적으로 실제 배치 작업 구현 + - 배치 주기: 5분마다 수집 +end note + +||| +== 2.3. 데이터 통합 및 ROI 계산 == + +Analytics -> Analytics: 데이터 통합 및 계산\n- 총 노출 수 = 외부 채널 노출 합계\n- 총 참여자 수 = Analytics DB\n- ROI 계산 = (수익 - 비용) / 비용 × 100\n- 채널별 전환율 계산 + +note right of Analytics + **ROI 계산 로직** + 총 비용 = 경품 비용 + 플랫폼 비용 + 예상 수익 = 매출 증가액 + 신규 고객 LTV + 투자 대비 수익률 = (수익 - 비용) / 비용 × 100 +end note + +||| +== 2.4. Redis 캐싱 및 응답 == + +Analytics -> Redis: 대시보드 데이터 캐시 저장\n(캐시키: analytics:dashboard:{eventId},\n값: 통합 데이터, TTL: 1시간) +activate Redis +Redis --> Analytics: OK +deactivate Redis + +Analytics --> GW: 200 OK\n대시보드 데이터 (JSON)\n{\n 총참여자: 1,234,\n 총노출: 17,200,\n ROI: 250%,\n 채널별성과: [...]\n} +deactivate Analytics + +GW --> FE: 200 OK\n대시보드 데이터 +deactivate GW + +FE -> FE: 대시보드 렌더링\n- 4개 요약 카드 표시\n- 채널별 성과 차트\n- 시간대별 참여 추이\n- 참여자 프로필 분석 +FE --> User: 실시간 대시보드 표시\n(응답 시간: 3초) +deactivate FE + +||| +== 3. 실시간 업데이트 (Background Event 구독) == + +note over Analytics, Kafka + **Analytics Service는 항상 Background에서 + Kafka Event Topics를 구독하여 + 실시간으로 통계를 업데이트합니다** +end note + +Kafka -> Analytics: **EventCreated** 이벤트\n{eventId, storeId, title, objective} +activate Analytics +Analytics -> AnalyticsDB: 이벤트 통계 초기화\n(이벤트 기본 정보 저장) +activate AnalyticsDB +AnalyticsDB --> Analytics: OK +deactivate AnalyticsDB +Analytics -> Redis: 캐시 무효화\n(캐시키 삭제: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: OK +deactivate Redis +deactivate Analytics + +...참여자 등록 시... + +Kafka -> Analytics: **ParticipantRegistered** 이벤트\n{participantId, eventId, phoneNumber} +activate Analytics +Analytics -> AnalyticsDB: 참여자 수 업데이트\n(참여자 수 1 증가) +activate AnalyticsDB +AnalyticsDB --> Analytics: OK +deactivate AnalyticsDB +Analytics -> Redis: 캐시 무효화\n(캐시키 삭제: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: OK +deactivate Redis +deactivate Analytics + +...배포 완료 시... + +Kafka -> Analytics: **DistributionCompleted** 이벤트\n{eventId, distributedChannels, completedAt} +activate Analytics +Analytics -> AnalyticsDB: 채널 통계 저장\n(배포 완료된 채널 정보 저장) +activate AnalyticsDB +AnalyticsDB --> Analytics: OK +deactivate AnalyticsDB +Analytics -> Redis: 캐시 무효화\n(캐시키 삭제: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: OK +deactivate Redis +deactivate Analytics + +note right of Analytics + **실시간 업데이트 메커니즘** + - EventCreated: 이벤트 기본 정보 초기화 + - ParticipantRegistered: 참여자 수 실시간 증가 + - DistributionCompleted: 배포 채널 통계 업데이트 + - 캐시 무효화: 다음 조회 시 최신 데이터 갱신 +end note + +@enduml diff --git a/design/backend/sequence/outer/이벤트생성플로우.puml b/design/backend/sequence/outer/이벤트생성플로우.puml new file mode 100644 index 0000000..a1933c9 --- /dev/null +++ b/design/backend/sequence/outer/이벤트생성플로우.puml @@ -0,0 +1,250 @@ +@startuml 이벤트생성플로우 +!theme mono + +title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램 + +actor "소상공인" as User +participant "Frontend" as FE +participant "API Gateway" as Gateway +participant "Event Service" as Event +participant "User Service" as UserSvc +participant "AI Service" as AI +participant "Content Service" as Content +participant "Distribution Service" as Dist +participant "Kafka" as Kafka +database "Event DB" as EventDB +database "User DB" as UserDB +database "Redis" as Redis +participant "외부 AI API" as AIApi +participant "이미지 생성 API" as ImageApi +participant "배포 채널 APIs" as ChannelApis + +== 1. 이벤트 목적 선택 (UFR-EVENT-020) == +User -> FE: 이벤트 목적 선택 +FE -> Gateway: GET /api/users/{userId}/store\n회원 및 매장정보 조회 +activate Gateway +Gateway -> UserSvc: GET /api/users/{userId}/store\n회원 및 매장정보 조회 +activate UserSvc +UserSvc -> UserDB: 사용자 및 매장 정보 조회 +activate UserDB +UserDB --> UserSvc: 사용자, 매장 정보 반환 +deactivate UserDB +UserSvc --> Gateway: 200 OK\n{userId, storeName, industry, address} +deactivate UserSvc +Gateway --> FE: 200 OK\n{userId, storeName, industry, address} +deactivate Gateway +FE -> Gateway: POST /events/purposes\n{목적, userId, storeName, industry, address} +Gateway -> Event: 이벤트 목적 저장 요청 +Event -> Redis: 이벤트 목적 정보 저장\nKey: draft:event:{eventDraftId}\n(목적, 매장정보 저장)\nTTL: 24시간 +activate Redis +Redis --> Event: 저장 완료 +deactivate Redis +Event --> Gateway: 저장 완료\n{eventDraftId} +Gateway --> FE: 200 OK +FE --> User: AI 추천 화면으로 이동 + +== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) == +User -> FE: AI 추천 요청 +FE -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{목적, 업종, 지역} +Gateway -> Event: AI 추천 요청 전달 +Event -> Kafka: Publish to ai-job-topic\n{jobId, eventDraftId, 목적, 업종, 지역} +Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING} +Gateway --> FE: 202 Accepted\n{jobId} +FE --> User: "AI가 분석 중입니다..." (로딩) + +note over AI: Kafka Consumer\nai 이벤트 생성 topic 구독 +Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...} + +AI -> AIApi: 트렌드 분석 및 이벤트 추천 요청\n{목적, 업종, 지역, 매장정보}\n[Circuit Breaker, Timeout: 5분] +AIApi --> AI: 3가지 추천안 + 트렌드 요약\n(예: "여름철 시원한 음료 선호도 증가") +AI -> Redis: AI 추천 결과 저장\nKey: ai:event:{eventDraftId}\n(3가지 추천안, 트렌드 요약)\nTTL: 24시간 +Redis --> AI: 저장 완료 +AI -> Redis: Job 상태 업데이트\n(상태를 COMPLETED로 변경) +note over AI, Redis: AI 추천 정보는 Redis에 저장\n- Content Service가 읽기 위함\n- 최종 승인 시 Event DB에 영구 저장 + +group Polling으로 상태 확인 + loop 상태 확인 (최대 30초) + FE -> Gateway: GET /jobs/{jobId}/status + Gateway -> Event: Job 상태 조회 + Event -> Redis: Job 상태 조회\n(jobId로 상태 및 결과 조회) + Redis --> Event: {status, result} + + alt Job 완료 + Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations, trendSummary} + Gateway --> FE: 추천 결과 및 트렌드 요약 반환 + FE --> User: 트렌드 요약 표시\n3가지 추천안 표시\n(제목/경품 수정 가능) + else Job 진행중 + Event --> Gateway: 200 OK\n{status: PENDING/PROCESSING} + Gateway --> FE: 진행중 상태 + note over FE: 2초 후 재요청 + end + end +end + +User -> FE: 추천안 선택\n(제목/경품 커스텀) +FE -> Gateway: PUT /events/drafts/{eventDraftId}\n{선택한 추천안, 커스텀 정보} +Gateway -> Event: 선택 저장 +Event -> Redis: 선택한 추천안 저장\nKey: draft:event:{eventDraftId}\n(이벤트 초안 업데이트)\nTTL: 24시간 +activate Redis +Redis --> Event: 업데이트 완료 +deactivate Redis +Event --> Gateway: 200 OK +Gateway --> FE: 저장 완료 +FE --> User: 콘텐츠 생성 화면으로 이동 + +== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) == +User -> FE: 이미지 생성 요청 +FE -> Gateway: POST /api/content/images/{eventDraftId}/generate +Gateway -> Content: 이미지 생성 요청 +Content -> Content: Job 생성\n{jobId, eventDraftId, status: PENDING} +Content --> Gateway: Job 생성 완료\n{jobId, status: PENDING} +Gateway --> FE: 202 Accepted\n{jobId} +FE --> User: "이미지 생성 중..." (로딩) + +note over Content: 백그라운드 워커\nRedis 폴링 또는 스케줄러 + +Content -> Redis: AI 이벤트 데이터 읽기\nKey: ai:event:{eventDraftId} +activate Redis +Redis --> Content: AI 추천 결과\n{선택된 추천안, 이벤트 정보} +deactivate Redis + +note over Content: inner sequence 참조:\ncontent-이미지생성.puml + +par 심플 스타일 + Content -> ImageApi: 심플 스타일 생성 요청\n[Circuit Breaker, Timeout: 5분] + ImageApi --> Content: 심플 이미지 URL +else 화려한 스타일 + Content -> ImageApi: 화려한 스타일 생성 요청\n[Circuit Breaker, Timeout: 5분] + ImageApi --> Content: 화려한 이미지 URL +else 트렌디 스타일 + Content -> ImageApi: 트렌디 스타일 생성 요청\n[Circuit Breaker, Timeout: 5분] + ImageApi --> Content: 트렌디 이미지 URL +end + +Content -> Redis: 이미지 URL 저장\nKey: content:image:{eventDraftId}\n{심플, 화려, 트렌디 URL}\nTTL: 7일 +activate Redis +Redis --> Content: 저장 완료 +deactivate Redis +Content -> Redis: Job 상태 업데이트\n(상태를 COMPLETED로 변경) +note over Content, Redis: 이미지 URL은 Redis에 저장\n- 최종 승인 시 Event DB에 영구 저장 + +group Polling으로 상태 확인 + loop 상태 확인 (최대 30초) + FE -> Gateway: GET /api/content/jobs/{jobId}/status + Gateway -> Content: Job 상태 조회 + Content -> Redis: Job 상태 조회\n(jobId로 상태 및 이미지 URL 조회) + Redis --> Content: {status, imageUrls} + + alt Job 완료 + Content --> Gateway: 200 OK\n{status: COMPLETED, imageUrls} + Gateway --> FE: 이미지 URL 반환 + FE --> User: 3가지 스타일 카드 표시 + else Job 진행중 + Content --> Gateway: 200 OK\n{status: PENDING/PROCESSING} + Gateway --> FE: 진행중 상태 + note over FE: 2초 후 재요청 + end + end +end + +User -> FE: 스타일 선택 및 편집 +FE -> Gateway: PUT /events/drafts/{eventDraftId}/content\n{선택한 이미지, 편집내용} +Gateway -> Event: 콘텐츠 선택 저장 +Event -> Redis: 선택한 콘텐츠 저장\nKey: draft:event:{eventDraftId}\n(이벤트 초안 업데이트)\nTTL: 24시간 +activate Redis +Redis --> Event: 업데이트 완료 +deactivate Redis +Event --> Gateway: 200 OK +Gateway --> FE: 저장 완료 +FE --> User: 배포 채널 선택 화면으로 이동 + +== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) == +User -> FE: 배포 채널 선택\n최종 승인 요청 +FE -> Gateway: POST /api/events/{eventDraftId}/publish\n{선택 채널 목록} +Gateway -> Event: 최종 승인 및 배포 처리 + +note over Event: Redis 데이터를 Event DB에 영구 저장 + +Event -> Redis: 이벤트 초안 조회\nKey: draft:event:{eventDraftId} +activate Redis +Redis --> Event: 이벤트 초안 데이터\n(목적, 매장정보, 추천안, 콘텐츠) +deactivate Redis + +Event -> Redis: AI 추천 결과 조회\nKey: ai:event:{eventDraftId} +activate Redis +Redis --> Event: AI 추천 결과 +deactivate Redis + +Event -> Redis: 이미지 URL 조회\nKey: content:image:{eventDraftId} +activate Redis +Redis --> Event: 이미지 URL 목록 +deactivate Redis + +Event -> EventDB: 이벤트 정보 영구 저장\n(목적, 매장정보, AI 추천, 이미지 URL, 배포 채널 포함) +EventDB --> Event: 저장 완료 + +Event -> EventDB: 이벤트 상태 변경\n(DRAFT → APPROVED로 업데이트) +EventDB --> Event: 상태 변경 완료 +Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보} + +note over Event: 동기 호출로 배포 진행\ninner sequence 참조:\ndistribution-다중채널배포.puml +Event -> Dist: REST API - 배포 요청\nPOST /api/distribution/distribute\n{eventId, channels[], contentUrls} + +note over Dist: Sprint 2: Mock 처리\n- 외부 API 호출 없음\n- 모든 배포 즉시 성공 처리\n- 배포 로그만 DB 기록 + +Dist -> EventDB: 배포 이력 초기화\n(이벤트ID, 상태: PENDING) +EventDB --> Dist: 배포 이력 ID +Dist -> EventDB: 배포 이력 상태 업데이트\n(상태: IN_PROGRESS) + +note over Dist: 다중 채널 Mock 배포\n(내부 처리 상세는 inner sequence 참조) + +par 우리동네TV + alt 우리동네TV 선택 + Dist -> Dist: Mock 처리\n(즉시 성공) + Dist -> EventDB: 채널 로그 저장\n(우리동네TV, 성공,\n배포ID, 예상노출수) + end +else 링고비즈 + alt 링고비즈 선택 + Dist -> Dist: Mock 처리\n(즉시 성공) + Dist -> EventDB: 채널 로그 저장\n(링고비즈, 성공,\n업데이트 시각) + end +else 지니TV + alt 지니TV 선택 + Dist -> Dist: Mock 처리\n(즉시 성공) + Dist -> EventDB: 채널 로그 저장\n(지니TV, 성공,\n광고ID, 스케줄) + end +else Instagram + alt Instagram 선택 + Dist -> Dist: Mock 처리\n(즉시 성공) + Dist -> EventDB: 채널 로그 저장\n(Instagram, 성공,\npostUrl, postId) + end +else Naver Blog + alt Naver Blog 선택 + Dist -> Dist: Mock 처리\n(즉시 성공) + Dist -> EventDB: 채널 로그 저장\n(NaverBlog, 성공,\npostUrl) + end +else Kakao Channel + alt Kakao Channel 선택 + Dist -> Dist: Mock 처리\n(즉시 성공) + Dist -> EventDB: 채널 로그 저장\n(KakaoChannel, 성공,\nmessageId) + end +end + +note over Dist: 모든 채널 배포 완료 (즉시 처리) + +Dist -> EventDB: 배포 이력 상태 업데이트\n(상태: COMPLETED, 완료일시) + +Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt} + +Dist --> Event: REST API 동기 응답\n200 OK\n{distributionId, status: COMPLETED, results[]} + +Event -> EventDB: 이벤트 상태 업데이트\n(APPROVED → ACTIVE로 변경) +EventDB --> Event: 업데이트 완료 + +Event --> Gateway: 200 OK\n{eventId, 배포결과} +Gateway --> FE: 배포 완료 +FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동 + +note over Event, Dist: Sprint 2 제약사항\n- 외부 API 호출 없음 (Mock)\n- 모든 배포 즉시 성공 처리\n- Circuit Breaker 미구현\n- Retry 로직 미구현\n\nSprint 3 이후 구현 예정\n- 실제 외부 채널 API 연동\n- Circuit Breaker 패턴\n- Retry 및 실패 처리 + +@enduml diff --git a/design/pattern/architecture-pattern.md b/design/pattern/architecture-pattern.md new file mode 100644 index 0000000..243552c --- /dev/null +++ b/design/pattern/architecture-pattern.md @@ -0,0 +1,619 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 클라우드 아키텍처 패턴 적용 방안 (MVP) + +## 문서 정보 +- **작성일**: 2025-10-21 +- **버전**: 2.0 (MVP 집중) +- **적용 패턴**: 4개 핵심 패턴 +- **목표**: 빠른 MVP 출시와 안정적 서비스 제공 + +--- + +## 1. 요구사항 분석 + +### 1.1 기능적 요구사항 + +**User 서비스** +- 회원가입/로그인 및 프로필 관리 +- 사업자번호 검증 (국세청 API) + +**Event 서비스** +- 이벤트 CRUD 및 상태 관리 +- 이벤트 목적 선택 → AI 추천 → 콘텐츠 생성 → 배포 → 분석 플로우 + +**AI 서비스** +- 트렌드 분석 (업종/지역/시즌) +- 3가지 이벤트 기획안 자동 생성 +- Claude API/GPT-4 API 연동 + +**Content 서비스** +- 3가지 스타일 SNS 이미지 자동 생성 +- Stable Diffusion/DALL-E API 연동 +- 플랫폼별 이미지 최적화 (Instagram, Naver, Kakao) + +**Distribution 서비스** +- 다중 채널 동시 배포 (우리동네TV, 링고비즈, 지니TV, SNS) +- 7개 외부 API 연동 + +**Participation 서비스** +- 이벤트 참여 접수 및 당첨자 추첨 + +**Analytics 서비스** +- 실시간 성과 대시보드 +- 채널별 성과 분석 + +### 1.2 비기능적 요구사항 + +**성능 요구사항** +- AI 트렌드 분석 + 이벤트 추천: **10초 이내** +- SNS 이미지 생성: **5초 이내** +- 대시보드 데이터 업데이트: **5분 간격** +- 다중 채널 배포: **1분 이내** + +**가용성 요구사항** +- 시스템 가용성: **99% 이상** (MVP 목표) +- 외부 API 장애 시 서비스 지속 가능 + +**확장성 요구사항** +- 초기 사용자: **100명** +- 동시 이벤트: **50개** +- 캐시 히트율: **80% 이상** + +**보안 요구사항** +- JWT 기반 인증/인가 +- API Gateway를 통한 중앙집중식 보안 + +### 1.3 외부 API 의존성 + +| 서비스 | 외부 API | 용도 | 응답시간 | +|--------|----------|------|----------| +| User | 국세청 사업자등록정보 진위확인 API | 사업자번호 검증 | 2-3초 | +| AI | Claude API / GPT-4 API | 트렌드 분석 및 이벤트 추천 | 5-10초 | +| Content | Stable Diffusion / DALL-E API | SNS 이미지 생성 | 3-5초 | +| Distribution | 우리동네TV API | 영상 업로드 및 송출 | 1-2초 | +| Distribution | 링고비즈 API | 연결음 업데이트 | 1초 | +| Distribution | 지니TV 광고 API | 광고 등록 | 1-2초 | +| Distribution | SNS API (Instagram, Naver, Kakao) | 자동 포스팅 | 1-3초 | + +### 1.4 기술적 도전과제 + +#### 1. AI 응답 시간 관리 (🔴 Critical) + +**문제**: AI API 응답이 10초 목표를 초과할 수 있음 +- Claude/GPT-4 API 응답 시간 변동성 (5-15초) +- 트렌드 분석 + 이벤트 추천 2단계 처리 필요 + +**해결방안**: +- **비동기 처리**: Job 기반 처리로 응답 대기 시간 최소화 +- **캐싱**: 동일 조건(업종, 지역, 목적) 결과 Redis 캐싱 (24시간) +- **응답 시간 90% 단축** (캐시 히트 시) + +#### 2. 다중 외부 API 의존성 (🔴 Critical) + +**문제**: 7개 외부 API 연동으로 인한 장애 전파 위험 +- 각 API별 응답 시간 및 성공률 상이 +- 하나의 API 장애가 전체 서비스 중단으로 이어질 수 있음 + +**해결방안**: +- **Circuit Breaker**: 장애 API 자동 차단 및 Fallback 처리 +- **독립적 처리**: 채널별 배포 실패가 다른 채널에 영향 없음 +- **자동 재시도**: 일시적 오류 시 최대 3회 재시도 + +#### 3. 이미지 생성 비용 및 시간 (🟡 Important) + +**문제**: AI 이미지 생성 API 비용이 높고 시간 소요 +- 이미지 1장당 생성 비용: $0.02-0.05 +- 3가지 스타일 생성 시 비용 3배 + +**해결방안**: +- **캐싱**: 동일 이벤트 정보 이미지 재사용 +- **비용 90% 절감** (캐시 히트 시) + +#### 4. 실시간 대시보드 성능 (🟡 Important) + +**문제**: 다중 데이터 소스 통합으로 인한 응답 지연 +- 7개 외부 API + 내부 서비스 데이터 통합 +- 복잡한 차트 및 계산 로직 + +**해결방안**: +- **캐싱**: Redis를 통한 5분 간격 데이터 캐싱 +- **응답 시간 80% 단축** + +--- + +## 2. 패턴 선정 및 평가 + +### 2.1 MVP 핵심 패턴 (4개) + +| 패턴 | 적용 목적 | 주요 효과 | +|------|----------|-----------| +| **Cache-Aside** | AI 응답 시간 단축 및 비용 절감 | 응답 시간 90% 단축, 비용 90% 절감 | +| **API Gateway** | 중앙집중식 보안 및 라우팅 | 개발 복잡도 감소, 보안 강화 | +| **Asynchronous Request-Reply** | 장시간 작업 응답 시간 개선 | 사용자 대기 시간 제거 | +| **Circuit Breaker** (Optional) | 외부 API 장애 격리 | 가용성 95% → 99% 개선 | + +### 2.2 정량적 평가 매트릭스 + +**평가 기준**: +- **기능 적합성** (35%): 요구사항 직접 해결 능력 +- **성능 효과** (25%): 응답시간 및 처리량 개선 +- **운영 복잡도** (20%): 구현 및 운영 용이성 +- **확장성** (15%): 미래 요구사항 대응력 +- **비용 효율성** (5%): 개발/운영 비용 대비 효과 + +| 패턴 | 기능 적합성
(35%) | 성능 효과
(25%) | 운영 복잡도
(20%) | 확장성
(15%) | 비용 효율성
(5%) | **총점** | 우선순위 | +|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| **Cache-Aside** | 10 × 0.35
= 3.5 | 10 × 0.25
= 2.5 | 8 × 0.20
= 1.6 | 9 × 0.15
= 1.35 | 10 × 0.05
= 0.5 | **9.45** | 🔴 Critical | +| **API Gateway** | 9 × 0.35
= 3.15 | 7 × 0.25
= 1.75 | 9 × 0.20
= 1.8 | 9 × 0.15
= 1.35 | 8 × 0.05
= 0.4 | **8.45** | 🔴 Critical | +| **Asynchronous Request-Reply** | 9 × 0.35
= 3.15 | 9 × 0.25
= 2.25 | 7 × 0.20
= 1.4 | 8 × 0.15
= 1.2 | 8 × 0.05
= 0.4 | **8.40** | 🔴 Critical | +| **Circuit Breaker** | 8 × 0.35
= 2.8 | 8 × 0.25
= 2.0 | 9 × 0.20
= 1.8 | 9 × 0.15
= 1.35 | 7 × 0.05
= 0.35 | **8.30** | 🟡 Optional | + +### 2.3 패턴별 상세 분석 + +#### 2.3.1 Cache-Aside (9.45점) - 🔴 Critical + +**적용 대상**: +- AI 서비스: 트렌드 분석 결과, 이벤트 추천 결과 +- Content 서비스: 생성된 SNS 이미지 +- User 서비스: 사업자번호 검증 결과 + +**구현 방식**: +``` +1. 요청 수신 → 캐시 확인 (Redis GET) +2. 캐시 HIT: 즉시 반환 (응답 시간 0.1초) +3. 캐시 MISS: + - 외부 API 호출 (응답 시간 5-10초) + - 결과를 캐시에 저장 (Redis SET, TTL 24시간) + - 결과 반환 +``` + +**기대 효과**: +- **응답 시간**: 10초 → 0.1초 (99% 개선, 캐시 히트 시) +- **비용 절감**: AI API 호출 90% 감소 → 월 $2,000 → $200 (90% 절감) +- **캐시 히트율**: 80% (동일 업종/지역 이벤트 반복 요청) + +**운영 고려사항**: +- Redis 메모리 관리: 최대 2GB (약 10,000개 캐시 항목) +- TTL 정책: 24시간 (트렌드 변화 반영) +- 캐시 무효화: 수동 무효화 API 제공 + +#### 2.3.2 API Gateway (8.45점) - 🔴 Critical + +**적용 대상**: +- 전체 마이크로서비스 (User, Event, AI, Content, Distribution, Participation, Analytics) + +**주요 기능**: +1. **인증/인가**: JWT 토큰 검증 (모든 요청) +2. **라우팅**: URL 기반 서비스 라우팅 +3. **Rate Limiting**: API 호출 제한 (사용자당 100 req/min) +4. **로깅**: 중앙집중식 접근 로그 + +**구현 방식**: +``` +Client → API Gateway (Kong/AWS API Gateway) + ├─ /api/users/* → User Service + ├─ /api/events/* → Event Service + ├─ /api/ai/* → AI Service + ├─ /api/content/* → Content Service + ├─ /api/distribution/* → Distribution Service + ├─ /api/participation/* → Participation Service + └─ /api/analytics/* → Analytics Service +``` + +**기대 효과**: +- **개발 생산성**: 각 서비스에서 인증 로직 제거 → 개발 시간 30% 단축 +- **보안 강화**: 중앙집중식 보안 정책 관리 +- **운영 효율**: 통합 모니터링 및 로깅 + +#### 2.3.3 Asynchronous Request-Reply (8.40점) - 🔴 Critical + +**적용 대상**: +- AI 서비스: 트렌드 분석 및 이벤트 추천 (10초 소요) +- Content 서비스: SNS 이미지 생성 (5초 소요) + +**구현 방식**: +``` +1. 클라이언트 요청 → Job ID 즉시 반환 (응답 시간 0.1초) +2. 백그라운드 처리: + - AI API 호출 및 결과 생성 (10초) + - 결과를 DB 저장 +3. 클라이언트 폴링: + - 5초 간격으로 Job 상태 확인 (GET /jobs/{id}) + - 완료 시 결과 수신 +``` + +**기대 효과**: +- **사용자 경험**: 대기 화면에서 진행 상황 표시 → 이탈률 감소 +- **서버 부하**: 동기 대기 제거 → 동시 처리 가능 요청 5배 증가 + +**운영 고려사항**: +- Job 상태 저장: Redis (TTL 1시간) +- 폴링 간격: 5초 (UX 고려) +- Job 만료: 1시간 후 자동 삭제 + +#### 2.3.4 Circuit Breaker (8.30점) - 🟡 Optional + +**적용 대상**: +- 모든 외부 API 연동 지점 (7개 API) + +**동작 방식**: +``` +1. Closed (정상): + - 외부 API 정상 호출 + - 실패율 5% 미만 + +2. Open (차단): + - 실패율 5% 초과 시 Circuit Open + - 모든 요청 즉시 실패 (Fallback 처리) + - 30초 대기 + +3. Half-Open (테스트): + - 30초 후 1개 요청 시도 + - 성공 시 Closed로 전환 + - 실패 시 Open 유지 +``` + +**Fallback 전략**: +- **AI 서비스**: 캐시된 이전 추천 결과 제공 + 안내 메시지 +- **Distribution 서비스**: 해당 채널 배포 스킵 + 알림 +- **User 서비스**: 사업자번호 검증 스킵 (수동 확인으로 대체) + +**기대 효과**: +- **가용성**: 95% → 99% (외부 API 장애 격리) +- **장애 복구**: 자동 복구 시간 5분 → 30초 + +--- + +## 3. 서비스별 패턴 적용 설계 + +### 3.1 전체 아키텍처 (MVP) + +```mermaid +graph TB + subgraph "클라이언트" + Client[Web/Mobile App] + end + + subgraph "API Gateway Layer" + Gateway[API Gateway
인증, 라우팅, Rate Limiting] + end + + subgraph "마이크로서비스" + UserSvc[User Service
회원관리] + EventSvc[Event Service
이벤트 관리] + AISvc[AI Service
트렌드 분석, 추천] + ContentSvc[Content Service
이미지 생성] + DistSvc[Distribution Service
다중 채널 배포] + PartSvc[Participation Service
참여자 관리] + AnalSvc[Analytics Service
성과 분석] + end + + subgraph "Cache Layer" + Redis[(Redis Cache
Cache-Aside)] + end + + subgraph "외부 API (Circuit Breaker 적용)" + TaxAPI[국세청 API] + ClaudeAPI[Claude API] + SDAPI[Stable Diffusion] + UriAPI[우리동네TV API] + RingoAPI[링고비즈 API] + GenieAPI[지니TV API] + SNSAPI[SNS API] + end + + Client -->|HTTPS| Gateway + Gateway --> UserSvc + Gateway --> EventSvc + Gateway --> AISvc + Gateway --> ContentSvc + Gateway --> DistSvc + Gateway --> PartSvc + Gateway --> AnalSvc + + UserSvc -.->|Cache-Aside| Redis + AISvc -.->|Cache-Aside| Redis + ContentSvc -.->|Cache-Aside| Redis + + UserSvc -->|Circuit Breaker| TaxAPI + AISvc -->|Circuit Breaker| ClaudeAPI + ContentSvc -->|Circuit Breaker| SDAPI + DistSvc -->|Circuit Breaker| UriAPI + DistSvc -->|Circuit Breaker| RingoAPI + DistSvc -->|Circuit Breaker| GenieAPI + DistSvc -->|Circuit Breaker| SNSAPI + + style Gateway fill:#e1f5ff + style Redis fill:#ffe1e1 + style AISvc fill:#fff4e1 + style ContentSvc fill:#fff4e1 +``` + +### 3.2 AI Service - Asynchronous Request-Reply 패턴 + +```mermaid +sequenceDiagram + participant Client as 클라이언트 + participant API as API Gateway + participant Event as Event Service + participant AI as AI Service + participant Cache as Redis Cache + participant Claude as Claude API + + Client->>API: POST /api/ai/recommendations
(업종, 지역, 목적) + API->>Event: 이벤트 추천 요청 + Event->>AI: 추천 요청 + + AI->>Cache: GET cached_result + alt Cache HIT + Cache-->>AI: 캐시된 결과 + AI-->>Event: 즉시 반환 (0.1초) + Event-->>API: 결과 + API-->>Client: 결과 (Total: 0.2초) + else Cache MISS + Cache-->>AI: null + AI-->>Event: Job ID 즉시 반환 + Event-->>API: Job ID + API-->>Client: Job ID + 처리중 상태 (0.1초) + + AI->>Claude: 비동기 AI 호출 + Note over AI,Claude: 백그라운드 처리 (10초) + Claude-->>AI: 추천 결과 + AI->>Cache: SET result (TTL 24h) + AI->>AI: Job 상태 = 완료 + + loop 폴링 (5초 간격) + Client->>API: GET /api/jobs/{id} + API->>Event: Job 상태 확인 + Event->>AI: Job 상태 + alt 완료 + AI-->>Event: 결과 + Event-->>API: 결과 + API-->>Client: 최종 결과 + else 진행중 + AI-->>Event: 진행중 + Event-->>API: 진행중 + API-->>Client: 진행 상황 (예: 70%) + end + end + end +``` + +### 3.3 Distribution Service - Circuit Breaker 패턴 + +```mermaid +graph TB + subgraph "Distribution Service" + DistCtrl[Distribution Controller] + CB_Uri[Circuit Breaker
우리동네TV] + CB_Ringo[Circuit Breaker
링고비즈] + CB_Genie[Circuit Breaker
지니TV] + CB_SNS[Circuit Breaker
SNS] + end + + subgraph "외부 API" + UriAPI[우리동네TV API] + RingoAPI[링고비즈 API] + GenieAPI[지니TV API] + SNSAPI[SNS API] + end + + DistCtrl --> CB_Uri + DistCtrl --> CB_Ringo + DistCtrl --> CB_Genie + DistCtrl --> CB_SNS + + CB_Uri -->|실패율 < 5%
Closed| UriAPI + CB_Uri -.->|실패율 >= 5%
Open, Fallback| Fallback_Uri[배포 스킵
+ 알림] + + CB_Ringo -->|실패율 < 5%
Closed| RingoAPI + CB_Ringo -.->|실패율 >= 5%
Open, Fallback| Fallback_Ringo[배포 스킵
+ 알림] + + CB_Genie -->|실패율 < 5%
Closed| GenieAPI + CB_Genie -.->|실패율 >= 5%
Open, Fallback| Fallback_Genie[배포 스킵
+ 알림] + + CB_SNS -->|실패율 < 5%
Closed| SNSAPI + CB_SNS -.->|실패율 >= 5%
Open, Fallback| Fallback_SNS[배포 스킵
+ 알림] + + style CB_Uri fill:#ffe1e1 + style CB_Ringo fill:#ffe1e1 + style CB_Genie fill:#ffe1e1 + style CB_SNS fill:#ffe1e1 +``` + +--- + +## 4. MVP 구현 로드맵 + +### 4.1 단일 Phase MVP 전략 + +**목표**: 빠른 출시와 안정적 서비스 제공 + +**기간**: 12주 + +**목표 지표**: +- 사용자: 100명 +- 동시 이벤트: 50개 +- 시스템 가용성: 99% +- AI 응답 시간: 10초 이내 (캐시 미스), 0.1초 (캐시 히트) + +### 4.2 구현 순서 및 일정 + +| 주차 | 작업 내용 | 패턴 적용 | 완료 기준 | +|------|----------|-----------|-----------| +| **1-2주** | 인프라 구축 | API Gateway | - Kong/AWS API Gateway 설정
- Redis 클러스터 구축
- JWT 인증 구현 | +| **3-4주** | User/Event 서비스 | Cache-Aside | - 회원가입/로그인 완료
- 사업자번호 검증 캐싱
- 이벤트 CRUD 완료 | +| **5-6주** | AI 서비스 | Asynchronous Request-Reply
Cache-Aside | - Claude API 연동
- Job 기반 비동기 처리
- AI 결과 캐싱 (24시간 TTL) | +| **7-8주** | Content 서비스 | Asynchronous Request-Reply
Cache-Aside | - Stable Diffusion 연동
- 이미지 생성 Job 처리
- 이미지 캐싱 및 CDN | +| **9-10주** | Distribution 서비스 | Circuit Breaker | - 7개 외부 API 연동
- 각 API별 Circuit Breaker
- Fallback 전략 구현 | +| **11주** | Participation/Analytics | Cache-Aside | - 참여자 관리 및 추첨
- 대시보드 데이터 캐싱 | +| **12주** | 테스트 및 출시 | 전체 패턴 검증 | - 부하 테스트 (100명)
- 장애 시나리오 테스트
- MVP 출시 | + +### 4.3 기술 스택 + +**백엔드**: +- Spring Boot 3.2 (Java 17) / Node.js 20 +- Redis 7.2 (Cache) +- PostgreSQL 15 (주 DB) +- RabbitMQ / Kafka (비동기 처리, Optional) + +**프론트엔드**: +- React 18 + TypeScript 5 +- Next.js 14 (SSR) +- Zustand (상태관리) +- React Query (서버 상태 관리) + +**인프라**: +- Docker + Kubernetes +- Kong API Gateway / AWS API Gateway +- Resilience4j (Circuit Breaker) +- Prometheus + Grafana (모니터링) + +**외부 API**: +- Claude API / GPT-4 API +- Stable Diffusion API +- 국세청 사업자등록정보 진위확인 API +- 우리동네TV, 링고비즈, 지니TV, SNS API + +--- + +## 5. 예상 성과 + +### 5.1 성능 개선 + +| 항목 | Before | After | 개선율 | +|------|--------|-------|--------| +| AI 응답 시간 (캐시 히트) | 10초 | 0.1초 | **99% 개선** | +| AI 응답 시간 (캐시 미스) | 10초 | 10초 | - | +| 이미지 생성 시간 (캐시 히트) | 5초 | 0.1초 | **98% 개선** | +| 대시보드 로딩 시간 | 3초 | 0.5초 | **83% 개선** | + +### 5.2 비용 절감 + +**AI API 비용 절감** (캐시 히트율 80% 가정): +- Before: 1,000 요청/일 × $0.02 = **$20/일** = **$600/월** +- After: 200 요청/일 × $0.02 = **$4/일** = **$120/월** +- **절감액**: $480/월 (**80% 절감**) + +**이미지 생성 비용 절감** (캐시 히트율 80% 가정): +- Before: 500 이미지/일 × $0.04 = **$20/일** = **$600/월** +- After: 100 이미지/일 × $0.04 = **$4/일** = **$120/월** +- **절감액**: $480/월 (**80% 절감**) + +**총 비용 절감**: +- Before: $1,200/월 +- After: $240/월 +- **총 절감액**: **$960/월** (**80% 절감**) + +### 5.3 가용성 개선 + +| 지표 | Before | After | 개선 | +|------|--------|-------|------| +| 시스템 가용성 | 95% | 99% | **+4%p** | +| 외부 API 장애 시 서비스 지속 | ❌ 불가 | ✅ 가능 (Fallback) | - | +| 장애 자동 복구 시간 | 5분 (수동) | 30초 (자동) | **90% 단축** | + +### 5.4 개발 생산성 + +| 항목 | 효과 | +|------|------| +| API Gateway | 각 서비스 인증 로직 제거 → 개발 시간 **30% 단축** | +| Cache-Aside | 반복 API 호출 제거 → 테스트 시간 **50% 단축** | +| Circuit Breaker | 장애 처리 로직 자동화 → 예외 처리 코드 **40% 감소** | +| Async Request-Reply | 동시 처리 능력 **5배 증가** | + +--- + +## 6. 운영 고려사항 + +### 6.1 모니터링 지표 + +**Cache-Aside**: +- 캐시 히트율 (목표: 80% 이상) +- Redis 메모리 사용률 (목표: 70% 이하) +- 캐시 응답 시간 (목표: 100ms 이하) + +**API Gateway**: +- 요청 처리량 (TPS) +- 평균 응답 시간 +- 인증 실패율 + +**Circuit Breaker**: +- API별 실패율 +- Circuit 상태 (Closed/Open/Half-Open) +- Fallback 호출 횟수 + +**Asynchronous Request-Reply**: +- Job 처리 시간 +- 동시 Job 수 +- Job 완료율 + +### 6.2 알람 임계값 + +| 지표 | Warning | Critical | +|------|---------|----------| +| 캐시 히트율 | < 70% | < 50% | +| Redis 메모리 | > 80% | > 90% | +| API 응답 시간 | > 500ms | > 1000ms | +| Circuit Breaker Open | 1개 | 3개 이상 | +| 시스템 가용성 | < 99% | < 95% | + +### 6.3 장애 대응 절차 + +**Circuit Breaker Open 발생 시**: +1. 알람 수신 (Slack/Email) +2. 해당 외부 API 상태 확인 +3. Fallback 전략 동작 확인 +4. 30초 후 자동 복구 확인 +5. 복구 실패 시 수동 개입 + +**캐시 장애 시**: +1. Redis 클러스터 상태 확인 +2. Failover 자동 수행 (Sentinel) +3. 캐시 미스로 전환 (성능 저하 허용) +4. 긴급 Redis 복구 + +--- + +## 7. 체크리스트 + +### 7.1 패턴 적용 완료 확인 + +- [x] **Cache-Aside**: AI 결과, 이미지, 사업자번호 검증 캐싱 완료 +- [x] **API Gateway**: 인증, 라우팅, Rate Limiting 구현 완료 +- [x] **Asynchronous Request-Reply**: AI 및 이미지 생성 Job 처리 완료 +- [x] **Circuit Breaker**: 7개 외부 API에 Circuit Breaker 및 Fallback 적용 완료 + +### 7.2 성능 목표 달성 확인 + +- [ ] AI 응답 시간: 10초 이내 (캐시 미스), 0.1초 (캐시 히트) +- [ ] 캐시 히트율: 80% 이상 +- [ ] 시스템 가용성: 99% 이상 +- [ ] 비용 절감: 월 $960 (80%) + +### 7.3 운영 준비 완료 확인 + +- [ ] 모니터링 대시보드 구축 (Prometheus + Grafana) +- [ ] 알람 설정 완료 (Slack/Email) +- [ ] 장애 대응 매뉴얼 작성 +- [ ] 부하 테스트 완료 (100명) + +--- + +## 8. 다음 단계 (Phase 2 이후) + +**MVP 이후 확장 계획** (선택 사항): + +- **Retry 패턴**: 일시적 오류 자동 재시도 (현재는 Circuit Breaker로 커버) +- **Queue-Based Load Leveling**: 트래픽 폭증 시 부하 분산 +- **Saga 패턴**: 복잡한 분산 트랜잭션 관리 (이벤트 생성 플로우) +- **CQRS**: 읽기/쓰기 분리로 대시보드 성능 최적화 +- **Event Sourcing**: 이벤트 변경 이력 추적 및 감사 + +--- + +## 참고 문서 + +- [유저스토리](../userstory.md) +- [UI/UX 설계서](../uiux/uiux.md) +- [클라우드 디자인 패턴 개요](../../claude/cloud-design-patterns.md) +- [백업 파일](./architecture-pattern-backup.md) - 이전 버전 (9개 패턴) diff --git a/design/pattern/backup/architecture-pattern-backup.md b/design/pattern/backup/architecture-pattern-backup.md new file mode 100644 index 0000000..46f9f30 --- /dev/null +++ b/design/pattern/backup/architecture-pattern-backup.md @@ -0,0 +1,1097 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 클라우드 아키텍처 패턴 적용 방안 + +**문서 정보** +- 프로젝트명: KT AI 기반 소상공인 이벤트 자동 생성 서비스 +- 작성일: 2025-10-21 +- 버전: 1.0 +- 작성자: System Architect + +--- + +## 목차 +1. [요구사항 분석 결과](#1-요구사항-분석-결과) +2. [패턴 평가 매트릭스](#2-패턴-평가-매트릭스) +3. [서비스별 패턴 적용 설계](#3-서비스별-패턴-적용-설계) +4. [Phase별 구현 로드맵](#4-phase별-구현-로드맵) +5. [예상 성과 지표](#5-예상-성과-지표) + +--- + +## 1. 요구사항 분석 결과 + +### 1.1 기능적 요구사항 + +#### 마이크로서비스별 핵심 기능 +1. **User 서비스** (4개 유저스토리) + - 회원가입/로그인/프로필 관리 + - 사업자번호 검증 (국세청 API 연동) + - 세션 관리 및 인증/인가 + +2. **Event 서비스** (7개 유저스토리) + - 이벤트 CRUD 관리 + - 대시보드 현황 조회 + - 이벤트 목적 선택 및 생성 플로우 관리 + - AI/Content/Distribution 서비스 연동 + +3. **AI 서비스** (1개 유저스토리) + - 업종/지역/시즌 트렌드 분석 (5초 이내) + - 3가지 이벤트 추천 생성 (5초 이내) + - Claude API/GPT-4 API 연동 + - **병렬 처리**: 트렌드 분석 + 이벤트 추천 동시 실행 (총 10초 목표) + +4. **Content 서비스** (2개 유저스토리) + - SNS 이미지 3가지 스타일 자동 생성 (5초 이내) + - 플랫폼별 이미지 최적화 (Instagram/Naver/Kakao) + - Stable Diffusion/DALL-E API 연동 + +5. **Distribution 서비스** (2개 유저스토리) + - 다중 채널 동시 배포 (우리동네TV, 링고비즈, 지니TV, SNS) + - 채널별 독립 처리 및 실패 복구 + - 배포 상태 실시간 모니터링 + +6. **Participation 서비스** (3개 유저스토리) + - 이벤트 참여자 관리 (중복 체크) + - 자동 추첨 시스템 + - 참여자 목록 조회 및 필터링 + +7. **Analytics 서비스** (1개 유저스토리) + - 실시간 통합 대시보드 (5분 간격 업데이트) + - 다중 데이터 소스 통합 (Participation, Distribution, POS, 외부 API) + - 채널별 성과 분석 및 투자대비수익률 계산 + +### 1.2 비기능적 요구사항 + +#### 성능 요구사항 +| 항목 | 목표 | 우선순위 | +|------|------|---------| +| AI 트렌드 분석 + 추천 | 10초 이내 | 🔴 Critical | +| SNS 이미지 생성 | 5초 이내 | 🔴 Critical | +| 다중 채널 배포 | 1분 이내 | 🟡 High | +| 실시간 대시보드 업데이트 | 5분 간격 | 🟢 Medium | +| API 응답 시간 (CRUD) | 200ms 이하 | 🟡 High | + +#### 가용성 및 신뢰성 +- **시스템 가용성**: 99.9% (월 43분 다운타임 허용) +- **외부 API 장애 대응**: 개별 채널 실패가 전체 서비스에 영향 없도록 격리 +- **데이터 일관성**: 이벤트 생성 트랜잭션 보장 + +#### 확장성 +- **사용자 증가 대응**: 초기 100명 → 1년 후 10,000명 +- **이벤트 처리량**: 동시 이벤트 생성 50개 +- **데이터 증가**: 일 평균 1,000개 참여 데이터 + +#### 보안 요구사항 +- JWT 토큰 기반 인증 +- API Gateway를 통한 중앙 인증/인가 +- 개인정보 암호화 저장 (전화번호, 이름) +- 사업자번호 검증 강화 + +### 1.3 UI/UX 분석에서 도출된 기술적 요구사항 + +#### 사용자 인터랙션 패턴 +- **Mobile First**: 60% 모바일, 40% 데스크톱 +- **짧은 세션**: 5-10분 내 이벤트 생성 완료 +- **실시간 피드백**: 로딩 상태, 진행률 표시 필수 +- **3 Tap Rule**: 모든 주요 기능 3번 탭 내 도달 + +#### 실시간 처리 요구사항 +- AI 처리 진행 상황 실시간 표시 +- 배포 상태 실시간 모니터링 +- 대시보드 자동 갱신 (5분 간격) + +### 1.4 기술적 도전과제 + +#### 1. AI 응답 시간 관리 (🔴 Critical) +**문제**: AI API 응답이 10초 목표를 초과할 수 있음 + +**영향**: +- 사용자 이탈 증가 +- 서비스 만족도 저하 + +**해결 필요성**: MVP 단계부터 필수 + +#### 2. 다중 외부 API 의존성 (🔴 Critical) +**문제**: 7개 외부 API 연동 (국세청, Claude/GPT-4, Stable Diffusion, 우리동네TV, 링고비즈, 지니TV, SNS) + +**영향**: +- API 장애 시 서비스 중단 위험 +- 응답 시간 불안정성 +- 비용 증가 (API 호출량) + +**해결 필요성**: MVP 단계부터 필수 + +#### 3. 실시간 대시보드 성능 (🟡 High) +**문제**: 다중 데이터 소스 통합 (Participation, Distribution, POS, 외부 API) + +**영향**: +- 대시보드 로딩 지연 +- 서버 부하 증가 + +**해결 필요성**: Phase 2 확장 단계에서 해결 + +#### 4. 복잡한 비즈니스 트랜잭션 (🟡 High) +**문제**: 이벤트 생성 → AI 추천 → 콘텐츠 생성 → 배포 → 참여자 관리 → 분석 + +**영향**: +- 부분 실패 시 데이터 불일치 +- 보상 트랜잭션 복잡도 증가 + +**해결 필요성**: Phase 1 MVP에서 기본 구조 확립 + +--- + +## 2. 패턴 평가 매트릭스 + +### 2.1 평가 기준 + +| 기준 | 가중치 | 평가 내용 | +|------|--------|-----------| +| **기능 적합성** | 35% | 요구사항을 직접 해결하는 능력 | +| **성능 효과** | 25% | 응답시간 및 처리량 개선 효과 | +| **운영 복잡도** | 20% | 구현 및 운영의 용이성 | +| **확장성** | 15% | 미래 요구사항에 대한 대응력 | +| **비용 효율성** | 5% | 개발/운영 비용 대비 효과(ROI) | + +### 2.2 핵심 패턴 평가 + +#### 2.2.1 API Gateway (필수 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 9 | 35% | 3.15 | 중앙 인증, 라우팅, 로깅 통합 제공 | +| 성능 효과 | 7 | 25% | 1.75 | 단일 엔드포인트로 네트워크 최적화 | +| 운영 복잡도 | 8 | 20% | 1.60 | 표준 솔루션 활용 (Kong, AWS API Gateway) | +| 확장성 | 9 | 15% | 1.35 | 마이크로서비스 증가에 유연 대응 | +| 비용 효율성 | 8 | 5% | 0.40 | 오픈소스(Kong) 또는 클라우드 서비스 활용 | +| **총점** | | | **8.25** | **선정** | + +**선정 이유**: +- 7개 마이크로서비스의 단일 진입점 필요 +- 횡단 관심사(인증, 로깅, Rate Limiting) 중앙 관리 +- Mobile First 환경에서 단일 엔드포인트 필수 + +**적용 방안**: +- Kong Gateway 또는 AWS API Gateway 활용 +- JWT 토큰 검증 중앙화 +- Rate Limiting 적용 (DoS 방지) +- Request/Response 로깅 + +--- + +#### 2.2.2 Cache-Aside (필수 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 9 | 35% | 3.15 | AI 트렌드 분석, 이미지 생성 결과 캐싱 | +| 성능 효과 | 10 | 25% | 2.50 | AI API 호출 대폭 감소 (10초 → 0.1초) | +| 운영 복잡도 | 9 | 20% | 1.80 | Redis 표준 패턴, 간단한 구현 | +| 확장성 | 8 | 15% | 1.20 | Redis Cluster로 확장 가능 | +| 비용 효율성 | 10 | 5% | 0.50 | AI API 비용 90% 절감 | +| **총점** | | | **9.15** | **선정** | + +**선정 이유**: +- AI API 비용 절감 (동일한 요청에 대한 반복 호출 방지) +- 성능 대폭 개선 (10초 → 캐시 히트 시 0.1초) +- 외부 API 의존성 감소 + +**적용 대상**: +1. **AI 트렌드 분석 결과**: 업종/지역/시즌별 1시간 캐싱 +2. **AI 이벤트 추천 결과**: 동일 조건 24시간 캐싱 +3. **SNS 이미지 생성 결과**: 영구 캐싱 (CDN 연동) +4. **사업자번호 검증 결과**: 30일 캐싱 + +--- + +#### 2.2.3 Asynchronous Request-Reply (필수 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 10 | 35% | 3.50 | AI 처리 비동기 대응 필수 | +| 성능 효과 | 9 | 25% | 2.25 | 사용자 대기 시간 단축, 서버 리소스 효율화 | +| 운영 복잡도 | 6 | 20% | 1.20 | 폴링/WebSocket 구현 필요 | +| 확장성 | 9 | 15% | 1.35 | 장시간 작업 증가 시 대응 가능 | +| 비용 효율성 | 7 | 5% | 0.35 | 개발 비용 증가하지만 사용자 경험 개선 | +| **총점** | | | **8.65** | **선정** | + +**선정 이유**: +- AI 트렌드 분석 + 이벤트 추천 (10초 이상 소요) +- SNS 이미지 생성 (5초 이상 소요) +- 사용자 이탈 방지 (로딩 인디케이터 + 진행률 표시) + +**적용 방안**: +1. **요청 단계**: 클라이언트 → API Gateway → AI 서비스 (Job ID 반환) +2. **처리 단계**: AI 서비스 비동기 처리, 진행 상황 Redis 저장 +3. **응답 단계**: 클라이언트 폴링 또는 WebSocket으로 진행 상황 확인 +4. **완료 단계**: 결과 반환 및 캐싱 + +--- + +#### 2.2.4 Circuit Breaker (필수 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 10 | 35% | 3.50 | 외부 API 장애 격리 필수 | +| 성능 효과 | 8 | 25% | 2.00 | 장애 전파 방지로 전체 시스템 안정성 유지 | +| 운영 복잡도 | 7 | 20% | 1.40 | 라이브러리 활용 (Resilience4j, Netflix Hystrix) | +| 확장성 | 9 | 15% | 1.35 | 외부 API 증가 시 동일 패턴 적용 | +| 비용 효율성 | 8 | 5% | 0.40 | 장애 시 전체 시스템 다운 방지로 손실 감소 | +| **총점** | | | **8.65** | **선정** | + +**선정 이유**: +- 7개 외부 API 연동 (Claude/GPT-4, Stable Diffusion, 우리동네TV 등) +- API 장애 시 전체 서비스 중단 방지 +- 빠른 실패 및 폴백 처리 + +**적용 대상**: +1. **AI API**: Claude/GPT-4 장애 시 미리 준비된 템플릿 반환 +2. **이미지 생성 API**: Stable Diffusion 장애 시 기본 템플릿 이미지 제공 +3. **배포 채널 API**: 개별 채널 장애가 다른 채널에 영향 없도록 격리 +4. **국세청 API**: 검증 실패 시 캐시된 결과 또는 수동 승인 플로우 + +--- + +#### 2.2.5 Retry (필수 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 9 | 35% | 3.15 | 일시적 네트워크 오류 대응 | +| 성능 효과 | 7 | 25% | 1.75 | 일시적 오류 복구로 사용자 재시도 불필요 | +| 운영 복잡도 | 9 | 20% | 1.80 | 라이브러리 활용 (Resilience4j) | +| 확장성 | 8 | 15% | 1.20 | 모든 외부 API에 동일 적용 | +| 비용 효율성 | 7 | 5% | 0.35 | 재시도 비용 < 실패 처리 비용 | +| **총점** | | | **8.25** | **선정** | + +**선정 이유**: +- 외부 API 일시적 오류 빈번 (네트워크 타임아웃 등) +- 사용자 경험 개선 (자동 복구) + +**적용 방안**: +- **Exponential Backoff**: 1초 → 2초 → 4초 +- **최대 재시도**: 3회 +- **적용 대상**: 모든 외부 API 호출 +- **Circuit Breaker 연동**: 재시도 실패 시 Circuit 오픈 + +--- + +#### 2.2.6 Queue-Based Load Leveling (확장 단계 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 7 | 35% | 2.45 | 대량 이벤트 생성 시 부하 분산 | +| 성능 효과 | 8 | 25% | 2.00 | 피크 시간대 서버 안정성 확보 | +| 운영 복잡도 | 5 | 20% | 1.00 | 메시지 큐 인프라 필요 (RabbitMQ/Kafka) | +| 확장성 | 9 | 15% | 1.35 | 트래픽 증가 시 수평 확장 용이 | +| 비용 효율성 | 6 | 5% | 0.30 | 인프라 추가 비용 발생 | +| **총점** | | | **7.10** | **Phase 2 선정** | + +**선정 이유**: +- MVP에서는 불필요 (사용자 100명 이하) +- Phase 2 확장 단계에서 사용자 증가 시 필요 + +**적용 시기**: Phase 2 (사용자 1,000명 이상) + +**적용 대상**: +- AI 트렌드 분석 요청 큐 +- SNS 이미지 생성 요청 큐 +- 배포 요청 큐 + +--- + +#### 2.2.7 CQRS (고도화 단계 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 8 | 35% | 2.80 | Analytics 대시보드 읽기 최적화 | +| 성능 효과 | 9 | 25% | 2.25 | 읽기 전용 DB로 대시보드 성능 대폭 개선 | +| 운영 복잡도 | 4 | 20% | 0.80 | 읽기/쓰기 DB 분리, 동기화 복잡도 증가 | +| 확장성 | 10 | 15% | 1.50 | 읽기/쓰기 독립 확장 가능 | +| 비용 효율성 | 5 | 5% | 0.25 | DB 인프라 비용 증가 | +| **총점** | | | **7.60** | **Phase 3 선정** | + +**선정 이유**: +- MVP에서는 과도한 복잡도 +- Phase 3 고도화 단계에서 대시보드 성능 최적화 시 적용 + +**적용 시기**: Phase 3 (사용자 5,000명 이상) + +**적용 대상**: +- Analytics 서비스 (읽기 전용 DB) +- 복잡한 집계 쿼리 최적화 + +--- + +#### 2.2.8 Saga (확장 단계 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 8 | 35% | 2.80 | 이벤트 생성 → AI → 콘텐츠 → 배포 트랜잭션 | +| 성능 효과 | 6 | 25% | 1.50 | 성능 개선 효과 미미 | +| 운영 복잡도 | 3 | 20% | 0.60 | 보상 트랜잭션 설계 복잡도 매우 높음 | +| 확장성 | 9 | 15% | 1.35 | 복잡한 비즈니스 플로우 증가 시 필수 | +| 비용 효율성 | 4 | 5% | 0.20 | 개발 비용 증가 | +| **총점** | | | **6.45** | **Phase 2 선정** | + +**선정 이유**: +- MVP에서는 단순 롤백 처리로 충분 +- Phase 2에서 복잡한 플로우 증가 시 필요 + +**적용 시기**: Phase 2 + +**적용 대상**: +- 이벤트 생성 플로우 (Event → AI → Content → Distribution) +- 결제 연동 시 (Phase 3) + +--- + +#### 2.2.9 Event Sourcing (고도화 단계 패턴) + +| 평가 기준 | 점수 | 가중치 | 계산 | 근거 | +|----------|-----|--------|------|------| +| 기능 적합성 | 7 | 35% | 2.45 | 이벤트 변경 이력 추적 | +| 성능 효과 | 5 | 25% | 1.25 | 성능 개선 효과 미미 | +| 운영 복잡도 | 3 | 20% | 0.60 | 이벤트 저장소 관리, 이벤트 재생 복잡도 매우 높음 | +| 확장성 | 8 | 15% | 1.20 | 감사 추적, 디버깅에 유용 | +| 비용 효율성 | 4 | 5% | 0.20 | 스토리지 비용 증가 | +| **총점** | | | **5.70** | **Phase 3 선정** | + +**선정 이유**: +- MVP에서는 불필요 +- Phase 3에서 감사 추적 요구사항 발생 시 적용 + +**적용 시기**: Phase 3 + +**적용 대상**: +- Event 서비스 (이벤트 생성/수정/삭제 이력) + +--- + +### 2.3 선정 패턴 요약 + +#### Phase 1 (MVP) - 필수 패턴 +| 패턴 | 총점 | 적용 서비스 | 우선순위 | +|------|------|------------|---------| +| Cache-Aside | 9.15 | AI, Content, User | 🔴 Critical | +| API Gateway | 8.25 | 전체 | 🔴 Critical | +| Asynchronous Request-Reply | 8.65 | AI, Content | 🔴 Critical | +| Circuit Breaker | 8.65 | AI, Content, Distribution | 🔴 Critical | +| Retry | 8.25 | 전체 (외부 API) | 🔴 Critical | + +#### Phase 2 (확장) - 추가 패턴 +| 패턴 | 총점 | 적용 서비스 | 우선순위 | +|------|------|------------|---------| +| Queue-Based Load Leveling | 7.10 | AI, Content, Distribution | 🟡 High | +| Saga | 6.45 | Event, AI, Content, Distribution | 🟡 High | + +#### Phase 3 (고도화) - 최적화 패턴 +| 패턴 | 총점 | 적용 서비스 | 우선순위 | +|------|------|------------|---------| +| CQRS | 7.60 | Analytics | 🟢 Medium | +| Event Sourcing | 5.70 | Event | 🟢 Medium | + +--- + +## 3. 서비스별 패턴 적용 설계 + +### 3.1 전체 아키텍처 구조 (Phase 1 MVP) + +```mermaid +graph TB + subgraph "클라이언트" + Mobile[모바일 앱] + Web[웹 브라우저] + end + + subgraph "API Gateway 레이어" + Gateway[API Gateway
- JWT 인증
- Rate Limiting
- Logging] + end + + Mobile --> Gateway + Web --> Gateway + + subgraph "마이크로서비스" + User[User 서비스
- 인증/인가
- 프로필 관리] + Event[Event 서비스
- 이벤트 CRUD
- 플로우 관리] + AI[AI 서비스
- 트렌드 분석
- 이벤트 추천
⏱️ Async] + Content[Content 서비스
- 이미지 생성
⏱️ Async] + Distribution[Distribution 서비스
- 다중 채널 배포] + Participation[Participation 서비스
- 참여자 관리
- 추첨] + Analytics[Analytics 서비스
- 실시간 대시보드] + end + + Gateway --> User + Gateway --> Event + Gateway --> Participation + Gateway --> Analytics + + Event --> AI + Event --> Content + Event --> Distribution + + subgraph "캐시 레이어" + Redis[Redis Cache
💾 Cache-Aside
- AI 결과
- 이미지
- 사업자번호] + end + + AI -.->|캐시 조회/저장| Redis + Content -.->|캐시 조회/저장| Redis + User -.->|캐시 조회/저장| Redis + + subgraph "외부 API (Circuit Breaker + Retry)" + Claude[Claude/GPT-4 API] + StableDiff[Stable Diffusion API] + NTS[국세청 API] + UDTV[우리동네TV API] + RingoBiz[링고비즈 API] + GenieTV[지니TV API] + SNS[SNS APIs] + end + + AI -->|🔴 Circuit Breaker| Claude + Content -->|🔴 Circuit Breaker| StableDiff + User -->|🔴 Circuit Breaker| NTS + Distribution -->|🔴 Circuit Breaker| UDTV + Distribution -->|🔴 Circuit Breaker| RingoBiz + Distribution -->|🔴 Circuit Breaker| GenieTV + Distribution -->|🔴 Circuit Breaker| SNS + + subgraph "데이터 레이어" + UserDB[(User DB)] + EventDB[(Event DB)] + ParticipationDB[(Participation DB)] + end + + User --> UserDB + Event --> EventDB + Participation --> ParticipationDB + + Analytics -.->|데이터 수집| ParticipationDB + Analytics -.->|데이터 수집| EventDB + Analytics -.->|데이터 수집| Distribution + + classDef asyncService fill:#ffe6e6,stroke:#ff4444,stroke-width:3px + classDef cacheLayer fill:#e6f3ff,stroke:#4444ff,stroke-width:3px + classDef gateway fill:#fff3e6,stroke:#ff8800,stroke-width:3px + + class AI,Content asyncService + class Redis cacheLayer + class Gateway gateway +``` + +### 3.2 서비스별 상세 패턴 적용 + +#### 3.2.1 User 서비스 + +**적용 패턴**: +1. **Cache-Aside** (사업자번호 검증 결과) + - 검증 완료된 사업자번호 30일 캐싱 + - 캐시 미스 시 국세청 API 호출 + - 국세청 API 비용 90% 절감 + +2. **Circuit Breaker** (국세청 API) + - 장애 시 폴백: 캐시된 결과 또는 수동 승인 플로우 + - Threshold: 10회 연속 실패 + - Timeout: 30초 + +3. **Retry** (국세청 API) + - 재시도: 3회 + - Backoff: 1초 → 2초 → 4초 + +**핵심 플로우**: +``` +회원가입 요청 + → 사업자번호 검증 + → Redis 캐시 조회 + → 캐시 히트: 즉시 반환 + → 캐시 미스: 국세청 API 호출 (Circuit Breaker + Retry) + → 성공: Redis 캐싱 (30일) + → 실패: 폴백 처리 +``` + +--- + +#### 3.2.2 AI 서비스 (🔴 Critical) + +**적용 패턴**: +1. **Asynchronous Request-Reply** (트렌드 분석 + 이벤트 추천) + - 비동기 처리: 10초 이상 소요 + - Job ID 반환 → 클라이언트 폴링 + - 진행 상황 Redis 저장 + +2. **Cache-Aside** (트렌드 분석 결과) + - 업종/지역/시즌별 1시간 캐싱 + - 동일 조건 요청 시 즉시 반환 (10초 → 0.1초) + +3. **Circuit Breaker** (Claude/GPT-4 API) + - 장애 시 폴백: 미리 준비된 템플릿 기반 추천 + - Threshold: 5회 연속 실패 + - Timeout: 15초 + +4. **Retry** (Claude/GPT-4 API) + - 재시도: 3회 + - Backoff: 2초 → 4초 → 8초 + +**핵심 플로우**: +```mermaid +sequenceDiagram + participant Client + participant API_GW as API Gateway + participant Event + participant AI + participant Redis + participant Claude as Claude API + + Client->>API_GW: POST /events/ai-recommend + API_GW->>Event: 이벤트 추천 요청 + Event->>AI: 트렌드 분석 + 추천 요청 + + AI->>Redis: 캐시 조회 (업종/지역/시즌) + + alt 캐시 히트 + Redis-->>AI: 캐시 데이터 반환 + AI-->>Event: 즉시 결과 반환 + else 캐시 미스 + AI->>AI: Job ID 생성 + AI-->>Event: Job ID 반환 + Event-->>Client: Job ID + 폴링 URL + + par 비동기 처리 + AI->>Claude: 트렌드 분석 요청 (Circuit Breaker) + Claude-->>AI: 분석 결과 + AI->>Redis: 진행률 업데이트 (50%) + + AI->>Claude: 이벤트 추천 요청 (Circuit Breaker) + Claude-->>AI: 추천 결과 + AI->>Redis: 진행률 업데이트 (100%) + + AI->>Redis: 결과 캐싱 (1시간) + end + + loop 폴링 (5초 간격) + Client->>API_GW: GET /jobs/{jobId} + API_GW->>AI: 진행 상황 조회 + AI->>Redis: 진행률 조회 + Redis-->>AI: 진행률 반환 + AI-->>Client: 진행률 또는 최종 결과 + end + end +``` + +--- + +#### 3.2.3 Content 서비스 (🔴 Critical) + +**적용 패턴**: +1. **Asynchronous Request-Reply** (이미지 생성) + - 비동기 처리: 5초 이상 소요 + - Job ID 반환 → 클라이언트 폴링 + +2. **Cache-Aside** (생성된 이미지) + - 영구 캐싱 (S3 + CloudFront CDN) + - 동일 요청 시 CDN URL 즉시 반환 + +3. **Circuit Breaker** (Stable Diffusion API) + - 장애 시 폴백: 기본 템플릿 이미지 제공 + - Threshold: 5회 연속 실패 + - Timeout: 10초 + +4. **Retry** (Stable Diffusion API) + - 재시도: 3회 + - Backoff: 2초 → 4초 → 8초 + +**핵심 플로우**: +``` +이미지 생성 요청 + → Job ID 반환 + → 비동기 처리 + → Stable Diffusion API 호출 (Circuit Breaker + Retry) + → 성공: S3 업로드 + CDN URL 반환 + Redis 캐싱 + → 실패: 기본 템플릿 이미지 반환 + → 클라이언트 폴링으로 결과 확인 +``` + +--- + +#### 3.2.4 Distribution 서비스 + +**적용 패턴**: +1. **Bulkhead** (채널별 격리) + - 우리동네TV, 링고비즈, 지니TV, SNS 독립 처리 + - 하나의 채널 실패가 다른 채널에 영향 없음 + +2. **Circuit Breaker** (채널별 API) + - 장애 시 폴백: 해당 채널 배포 스킵 + - Threshold: 5회 연속 실패 + - Timeout: 30초 + +3. **Retry** (채널별 API) + - 재시도: 3회 + - Backoff: 1초 → 2초 → 4초 + +**핵심 플로우**: +```mermaid +graph LR + Distribution[Distribution 서비스] + + subgraph "병렬 배포 (Bulkhead)" + UDTV[우리동네TV
Circuit Breaker] + Ringo[링고비즈
Circuit Breaker] + Genie[지니TV
Circuit Breaker] + SNS[SNS
Circuit Breaker] + end + + Distribution -->|독립 처리| UDTV + Distribution -->|독립 처리| Ringo + Distribution -->|독립 처리| Genie + Distribution -->|독립 처리| SNS + + UDTV -->|성공/실패| Result[배포 결과 집계] + Ringo -->|성공/실패| Result + Genie -->|성공/실패| Result + SNS -->|성공/실패| Result +``` + +--- + +#### 3.2.5 Analytics 서비스 + +**적용 패턴**: +1. **Cache-Aside** (대시보드 데이터) + - 5분 간격 데이터 캐싱 + - 실시간성 vs 성능 트레이드오프 + +2. **Materialized View** (Phase 2) + - 복잡한 집계 쿼리 미리 계산 + - 5분 간격 업데이트 + +**핵심 플로우**: +``` +대시보드 조회 요청 + → Redis 캐시 조회 + → 캐시 히트 (5분 이내): 즉시 반환 + → 캐시 미스: + → Participation DB 조회 + → Distribution 서비스 API 호출 + → 외부 API 호출 (우리동네TV, SNS 통계) + → 집계 계산 + → Redis 캐싱 (5분) + → 결과 반환 +``` + +--- + +### 3.3 Phase 2 확장 단계 아키텍처 (Queue 추가) + +```mermaid +graph TB + subgraph "클라이언트" + Mobile[모바일 앱] + Web[웹 브라우저] + end + + subgraph "API Gateway 레이어" + Gateway[API Gateway] + end + + Mobile --> Gateway + Web --> Gateway + + subgraph "마이크로서비스" + Event[Event 서비스] + AI[AI 서비스
Worker Pool] + Content[Content 서비스
Worker Pool] + Distribution[Distribution 서비스
Worker Pool] + end + + Gateway --> Event + + subgraph "메시지 큐 (Queue-Based Load Leveling)" + AIQueue[AI 요청 큐] + ContentQueue[콘텐츠 요청 큐] + DistQueue[배포 요청 큐] + end + + Event --> AIQueue + Event --> ContentQueue + Event --> DistQueue + + AIQueue --> AI + ContentQueue --> Content + DistQueue --> Distribution + + subgraph "캐시 레이어" + Redis[Redis Cache] + end + + AI -.->|캐시| Redis + Content -.->|캐시| Redis + + classDef queueLayer fill:#fff3e6,stroke:#ff8800,stroke-width:3px + class AIQueue,ContentQueue,DistQueue queueLayer +``` + +**Phase 2 추가 패턴**: +- **Queue-Based Load Leveling**: 피크 시간대 부하 분산 +- **Saga**: 이벤트 생성 플로우 트랜잭션 관리 + +--- + +### 3.4 Phase 3 고도화 단계 아키텍처 (CQRS 추가) + +```mermaid +graph TB + subgraph "Analytics 서비스 (CQRS)" + AnalyticsAPI[Analytics API] + + subgraph "Command Side (쓰기)" + CommandHandler[Command Handler] + WriteDB[(Write DB
PostgreSQL)] + end + + subgraph "Query Side (읽기)" + QueryHandler[Query Handler] + ReadDB[(Read DB
MongoDB)] + end + end + + AnalyticsAPI --> CommandHandler + AnalyticsAPI --> QueryHandler + + CommandHandler --> WriteDB + WriteDB -.->|이벤트 발행| EventBus[이벤트 버스] + EventBus --> ReadDB + QueryHandler --> ReadDB + + Client[클라이언트] --> AnalyticsAPI +``` + +**Phase 3 추가 패턴**: +- **CQRS**: Analytics 서비스 읽기/쓰기 분리 +- **Event Sourcing**: Event 서비스 변경 이력 추적 + +--- + +## 4. Phase별 구현 로드맵 + +### 4.1 Phase 1: MVP (12주) + +**목표**: 핵심 기능 구현 및 사용자 검증 + +| 주차 | 작업 내용 | 적용 패턴 | 담당 서비스 | +|------|----------|----------|------------| +| 1-2 | 인프라 구축 | API Gateway | 전체 | +| 3-4 | User 서비스 개발 | Cache-Aside, Circuit Breaker, Retry | User | +| 5-6 | Event 서비스 개발 | - | Event | +| 7-8 | AI 서비스 개발 | Asynchronous Request-Reply, Cache-Aside, Circuit Breaker, Retry | AI | +| 9-10 | Content 서비스 개발 | Asynchronous Request-Reply, Cache-Aside, Circuit Breaker, Retry | Content | +| 11 | Distribution 서비스 개발 | Circuit Breaker, Retry, Bulkhead | Distribution | +| 12 | Participation & Analytics | Cache-Aside | Participation, Analytics | + +**Phase 1 완료 기준**: +- ✅ 20개 유저스토리 완료 +- ✅ AI 응답 시간 10초 이내 +- ✅ 외부 API 장애 격리 +- ✅ 사용자 100명 지원 + +--- + +### 4.2 Phase 2: 확장 (8주) + +**목표**: 사용자 증가 대응 및 성능 최적화 + +| 주차 | 작업 내용 | 적용 패턴 | 담당 서비스 | +|------|----------|----------|------------| +| 1-2 | 메시지 큐 도입 | Queue-Based Load Leveling | AI, Content, Distribution | +| 3-4 | Saga 패턴 구현 | Saga | Event, AI, Content, Distribution | +| 5-6 | 수평 확장 테스트 | - | 전체 | +| 7-8 | 모니터링 강화 | - | 전체 | + +**Phase 2 완료 기준**: +- ✅ 동시 이벤트 생성 50개 지원 +- ✅ 사용자 1,000명 지원 +- ✅ 피크 시간대 안정성 확보 + +--- + +### 4.3 Phase 3: 고도화 (8주) + +**목표**: 성능 최적화 및 고급 기능 추가 + +| 주차 | 작업 내용 | 적용 패턴 | 담당 서비스 | +|------|----------|----------|------------| +| 1-3 | CQRS 구현 | CQRS | Analytics | +| 4-6 | Event Sourcing 구현 | Event Sourcing | Event | +| 7-8 | 성능 튜닝 | - | 전체 | + +**Phase 3 완료 기준**: +- ✅ 대시보드 로딩 1초 이내 +- ✅ 사용자 10,000명 지원 +- ✅ 감사 추적 기능 + +--- + +## 5. 예상 성과 지표 + +### 5.1 성능 개선 효과 + +| 지표 | Before (패턴 미적용) | After (패턴 적용) | 개선율 | +|------|-------------------|-----------------|-------| +| **AI 응답 시간** (캐시 히트) | 10초 | 0.1초 | **99%** ↓ | +| **이미지 생성 시간** (캐시 히트) | 5초 | 0.1초 | **98%** ↓ | +| **사업자번호 검증** (캐시 히트) | 2초 | 0.05초 | **97.5%** ↓ | +| **API Gateway 응답 시간** | - | 10ms | - | +| **외부 API 장애 복구 시간** | 수동 처리 (5분+) | 자동 폴백 (0.1초) | **99.9%** ↓ | +| **대시보드 로딩 시간** (Phase 3) | 5초 | 1초 | **80%** ↓ | + +### 5.2 비용 절감 효과 + +| 항목 | Before (월간) | After (월간) | 절감율 | +|------|-------------|------------|-------| +| **AI API 호출 비용** | $1,000 | $100 | **90%** ↓ | +| **이미지 생성 API 비용** | $500 | $50 | **90%** ↓ | +| **국세청 API 호출 비용** | $200 | $20 | **90%** ↓ | +| **서버 인프라 비용** (Phase 2) | $500 | $300 | **40%** ↓ | +| **총 운영 비용** | $2,200 | $470 | **78.6%** ↓ | + +**연간 절감액**: $20,760 + +### 5.3 안정성 개선 효과 + +| 지표 | Before | After | 개선율 | +|------|--------|-------|-------| +| **시스템 가용성** | 95% | 99.9% | **4.9%** ↑ | +| **외부 API 장애 영향도** | 전체 서비스 중단 | 해당 기능만 폴백 | **99%** ↓ | +| **평균 복구 시간 (MTTR)** | 30분 | 0.1초 (자동) | **99.9%** ↓ | +| **사용자 이탈률** (에러 발생 시) | 80% | 5% | **93.75%** ↓ | + +### 5.4 개발 생산성 효과 + +| 지표 | Before | After | 개선율 | +|------|--------|-------|-------| +| **외부 API 연동 시간** | 2주 | 3일 | **78.6%** ↓ | +| **에러 처리 코드 작성 시간** | 1주 | 1일 | **85.7%** ↓ | +| **모니터링 구축 시간** | 2주 | 3일 | **78.6%** ↓ | +| **총 개발 기간** | 20주 | 12주 (Phase 1) | **40%** ↓ | + +### 5.5 사용자 경험 개선 효과 + +| 지표 | Before | After | 개선율 | +|------|--------|-------|-------| +| **이벤트 생성 완료 시간** | 15분 | 5분 | **66.7%** ↓ | +| **사용자 만족도** | 70점 | 90점 | **28.6%** ↑ | +| **재방문율** | 40% | 70% | **75%** ↑ | +| **이벤트 생성 성공률** | 80% | 98% | **22.5%** ↑ | + +--- + +## 6. 구현 시 고려사항 + +### 6.1 Cache-Aside 구현 + +**Redis 캐시 키 전략**: +``` +ai:trend:{업종}:{지역}:{시즌} → TTL: 1시간 +ai:recommend:{목적}:{업종} → TTL: 24시간 +content:image:{eventId} → TTL: 영구 (CDN) +user:bizno:{사업자번호} → TTL: 30일 +``` + +**캐시 무효화 전략**: +- AI 트렌드: 1시간 자동 만료 +- 이미지: 영구 보관 (S3 + CDN) +- 사업자번호: 30일 자동 만료 또는 재검증 요청 시 무효화 + +### 6.2 Circuit Breaker 설정 + +**Resilience4j 설정 예시**: +```yaml +resilience4j.circuitbreaker: + instances: + claudeAPI: + failureRateThreshold: 50 + waitDurationInOpenState: 60s + slidingWindowSize: 10 + minimumNumberOfCalls: 5 + permittedNumberOfCallsInHalfOpenState: 3 +``` + +**폴백 전략**: +- **AI API**: 미리 준비된 템플릿 기반 추천 +- **이미지 생성 API**: 기본 템플릿 이미지 +- **국세청 API**: 캐시된 결과 또는 수동 승인 +- **배포 채널 API**: 해당 채널 스킵, 다른 채널 계속 진행 + +### 6.3 Asynchronous Request-Reply 구현 + +**폴링 vs WebSocket**: +- **Phase 1**: 폴링 (간단한 구현) + - 클라이언트: 5초 간격 폴링 + - 서버: Job 상태 Redis 저장 +- **Phase 2**: WebSocket (실시간 업데이트) + - 진행률 실시간 푸시 + - 서버 부하 감소 + +**Job 상태 관리**: +```json +{ + "jobId": "job-12345", + "status": "processing", + "progress": 50, + "result": null, + "error": null, + "createdAt": "2025-10-21T10:00:00Z", + "updatedAt": "2025-10-21T10:00:05Z" +} +``` + +### 6.4 Bulkhead 구현 + +**채널별 스레드 풀 분리**: +```java +@Configuration +public class ThreadPoolConfig { + @Bean("udtv-pool") + public ThreadPoolTaskExecutor udtvThreadPool() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(100); + return executor; + } + + // 링고비즈, 지니TV, SNS도 동일하게 분리 +} +``` + +--- + +## 7. 위험 요소 및 대응 방안 + +### 7.1 AI API 비용 폭발 + +**위험**: +- 캐싱 미적용 시 AI API 비용 월 $10,000+ 발생 가능 + +**대응 방안**: +1. Cache-Aside 패턴 필수 적용 +2. 캐시 히트율 모니터링 (목표: 90% 이상) +3. 월간 비용 임계값 설정 ($500) +4. 임계값 초과 시 알림 및 자동 차단 + +### 7.2 외부 API 의존성 + +**위험**: +- 7개 외부 API 중 하나라도 장애 시 서비스 중단 + +**대응 방안**: +1. Circuit Breaker 패턴 필수 적용 +2. 폴백 전략 명확히 정의 +3. SLA 계약 체결 (우리동네TV, 지니TV 등) +4. 대체 API 준비 (예: Claude ↔ GPT-4 전환) + +### 7.3 캐시 데이터 불일치 + +**위험**: +- 트렌드 데이터 변경 시 캐시 무효화 누락 + +**대응 방안**: +1. TTL 기반 자동 만료 +2. 수동 무효화 API 제공 +3. 캐시 버전 관리 + +### 7.4 비동기 처리 복잡도 + +**위험**: +- Job 상태 관리 실패, 폴링 부하 + +**대응 방안**: +1. Redis를 통한 안정적인 상태 관리 +2. Job TTL 설정 (24시간 자동 삭제) +3. Phase 2에서 WebSocket으로 전환 + +--- + +## 8. 모니터링 및 운영 + +### 8.1 핵심 메트릭 + +**성능 메트릭**: +- AI 응답 시간 (p50, p95, p99) +- 이미지 생성 시간 (p50, p95, p99) +- 캐시 히트율 (목표: 90%) +- API Gateway 응답 시간 (목표: 10ms) + +**안정성 메트릭**: +- Circuit Breaker 상태 (Open/Half-Open/Closed) +- 외부 API 실패율 (목표: 1% 이하) +- 시스템 가용성 (목표: 99.9%) + +**비용 메트릭**: +- AI API 호출 횟수 및 비용 +- 이미지 생성 API 호출 횟수 및 비용 +- 캐시 절감 효과 (예상 비용 - 실제 비용) + +### 8.2 알림 규칙 + +**Critical 알림**: +- Circuit Breaker Open 상태 +- AI 응답 시간 > 20초 +- 캐시 히트율 < 80% +- 시스템 가용성 < 99% + +**Warning 알림**: +- 외부 API 실패율 > 5% +- 월간 AI API 비용 > $500 +- 동시 접속자 > 80% 임계값 + +--- + +## 9. 결론 + +### 9.1 선정된 패턴 요약 + +**Phase 1 (MVP)**: +1. ✅ API Gateway - 중앙 인증 및 라우팅 +2. ✅ Cache-Aside - AI/이미지 비용 90% 절감 +3. ✅ Asynchronous Request-Reply - 사용자 경험 개선 +4. ✅ Circuit Breaker - 외부 API 장애 격리 +5. ✅ Retry - 일시적 오류 자동 복구 + +**Phase 2 (확장)**: +6. ✅ Queue-Based Load Leveling - 부하 분산 +7. ✅ Saga - 복잡한 트랜잭션 관리 + +**Phase 3 (고도화)**: +8. ✅ CQRS - 대시보드 성능 최적화 +9. ✅ Event Sourcing - 감사 추적 + +### 9.2 예상 효과 + +**성능**: +- AI 응답 시간: 99% 개선 (캐시 히트 시) +- 이미지 생성: 98% 개선 (캐시 히트 시) + +**비용**: +- 연간 $20,760 절감 (78.6% 감소) + +**안정성**: +- 시스템 가용성: 95% → 99.9% +- 외부 API 장애 영향: 99% 감소 + +**사용자 경험**: +- 이벤트 생성 시간: 66.7% 단축 +- 사용자 만족도: 28.6% 향상 + +### 9.3 다음 단계 + +1. **Phase 1 (주 1-2)**: API Gateway 구축 +2. **Phase 1 (주 3-4)**: User 서비스 + Cache-Aside + Circuit Breaker +3. **Phase 1 (주 5-6)**: Event 서비스 +4. **Phase 1 (주 7-8)**: AI 서비스 + Asynchronous Request-Reply +5. **Phase 1 (주 9-10)**: Content 서비스 + Asynchronous Request-Reply +6. **Phase 1 (주 11)**: Distribution 서비스 + Bulkhead +7. **Phase 1 (주 12)**: Participation & Analytics + 통합 테스트 + +--- + +**문서 작성자**: System Architect - 박영자 +**검토자**: Backend Developer - 최수연, DevOps Engineer - 송근정 +**승인일**: 2025-10-21 diff --git a/design/pattern/backup/architecture-pattern-backup2.md b/design/pattern/backup/architecture-pattern-backup2.md new file mode 100644 index 0000000..f1fa3f4 --- /dev/null +++ b/design/pattern/backup/architecture-pattern-backup2.md @@ -0,0 +1,1165 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 클라우드 아키텍처 패턴 적용 방안 + +## 문서 정보 +- **작성일**: 2025-10-22 +- **버전**: 3.0 (Azure Kubernetes 기반) +- **적용 패턴**: 4개 핵심 패턴 +- **인프라 환경**: Azure Kubernetes Service (AKS) +- **목표**: 빠른 MVP 출시와 안정적 서비스 제공 + +--- + +## 1. 요구사항 분석 + +### 1.1 기능적 요구사항 + +**User 서비스** +- 회원가입/로그인 및 프로필 관리 +- 사업자번호 검증 (국세청 API) + +**Event 서비스** +- 이벤트 CRUD 및 상태 관리 +- 이벤트 목적 선택 → AI 추천 → 콘텐츠 생성 → 배포 → 분석 플로우 + +**AI 서비스** +- 트렌드 분석 (업종/지역/시즌) +- 3가지 이벤트 기획안 자동 생성 +- Claude API/GPT-4 API 연동 + +**Content 서비스** +- 3가지 스타일 SNS 이미지 자동 생성 +- Stable Diffusion/DALL-E API 연동 +- 플랫폼별 이미지 최적화 (Instagram, Naver, Kakao) + +**Distribution 서비스** +- 다중 채널 동시 배포 (우리동네TV, 링고비즈, 지니TV, SNS) +- 7개 외부 API 연동 + +**Participation 서비스** +- 이벤트 참여 접수 및 당첨자 추첨 + +**Analytics 서비스** +- 실시간 성과 대시보드 +- 채널별 성과 분석 + +### 1.2 비기능적 요구사항 + +**성능 요구사항** +- AI 트렌드 분석 + 이벤트 추천: **10초 이내** +- SNS 이미지 생성: **5초 이내** +- 대시보드 데이터 업데이트: **5분 간격** +- 다중 채널 배포: **1분 이내** + +**가용성 요구사항** +- 시스템 가용성: **99% 이상** (MVP 목표) +- 외부 API 장애 시 서비스 지속 가능 + +**확장성 요구사항** +- 초기 사용자: **100명** +- 동시 이벤트: **50개** +- 캐시 히트율: **80% 이상** + +**보안 요구사항** +- JWT 기반 인증/인가 +- API Gateway를 통한 중앙집중식 보안 + +### 1.3 외부 API 의존성 + +| 서비스 | 외부 API | 용도 | 응답시간 | +|--------|----------|------|----------| +| User | 국세청 사업자등록정보 진위확인 API | 사업자번호 검증 | 2-3초 | +| AI | Claude API / GPT-4 API | 트렌드 분석 및 이벤트 추천 | 5-10초 | +| Content | Stable Diffusion / DALL-E API | SNS 이미지 생성 | 3-5초 | +| Distribution | 우리동네TV API | 영상 업로드 및 송출 | 1-2초 | +| Distribution | 링고비즈 API | 연결음 업데이트 | 1초 | +| Distribution | 지니TV 광고 API | 광고 등록 | 1-2초 | +| Distribution | SNS API (Instagram, Naver, Kakao) | 자동 포스팅 | 1-3초 | + +--- + +## 2. 적용 패턴 개요 + +### 2.1 선정된 4가지 핵심 패턴 + +| 패턴 | 적용 목적 | Azure 서비스 | 우선순위 | +|------|----------|-------------|---------| +| **Cache-Aside** | AI 응답 시간 단축 및 비용 절감 | Azure Cache for Redis | 🔴 Critical | +| **API Gateway** | 중앙집중식 보안 및 라우팅 | Azure API Management / Nginx Ingress | 🔴 Critical | +| **Asynchronous Request-Reply** | 장시간 작업 응답 시간 개선 | Azure Service Bus / RabbitMQ | 🔴 Critical | +| **Circuit Breaker** | 외부 API 장애 격리 | Resilience4j / Polly | 🟡 Optional | + +### 2.2 Azure Kubernetes 환경 고려사항 + +**인프라 구성**: +- **AKS (Azure Kubernetes Service)**: 컨테이너 오케스트레이션 +- **Azure Cache for Redis**: 분산 캐시 +- **Azure Container Registry (ACR)**: 컨테이너 이미지 저장소 +- **Azure Database for PostgreSQL**: 관계형 데이터베이스 +- **Azure Service Bus**: 메시지 큐 (Optional, RabbitMQ 대체 가능) +- **Azure Monitor + Application Insights**: 모니터링 및 로깅 +- **Azure API Management**: API Gateway (Optional, Nginx Ingress 대체 가능) + +**개발 환경 vs 운영 환경**: +- **개발 환경**: AKS 개발 클러스터, Redis Standard tier, PostgreSQL Basic tier +- **운영 환경**: AKS 운영 클러스터 (다중 노드), Redis Premium tier (HA), PostgreSQL General Purpose (HA) + +--- + +## 3. 패턴별 상세 설계 + +### 3.1 Cache-Aside 패턴 + +#### 3.1.1 개요 + +**목적**: AI API 호출 비용 절감 및 응답 시간 단축 + +**적용 대상**: +- AI 서비스: 트렌드 분석 결과, 이벤트 추천 결과 +- Content 서비스: 생성된 SNS 이미지 +- User 서비스: 사업자번호 검증 결과 + +#### 3.1.2 Azure 구현 방식 + +**Azure Cache for Redis 구성**: +```yaml +# 개발 환경 +tier: Standard +capacity: C1 (1GB) +availability: Single instance + +# 운영 환경 +tier: Premium +capacity: P1 (6GB) +availability: Zone redundant (고가용성) +``` + +**동작 흐름**: +``` +1. 요청 수신 → Redis GET {key} +2. Cache HIT: + - 즉시 반환 (응답 시간 ~100ms) +3. Cache MISS: + - 외부 API 호출 (응답 시간 5-10초) + - Redis SET {key} {value} EX {TTL} + - 결과 반환 +``` + +**캐시 키 설계**: +``` +ai:trend:{industry}:{region}:{season} +ai:recommendation:{industry}:{region}:{purpose} +content:image:{event_id}:{style} +user:business:{business_number} +``` + +**TTL 정책**: +- AI 트렌드 분석: **24시간** (트렌드 변화 주기 고려) +- AI 이벤트 추천: **12시간** (빈번한 업데이트) +- 생성 이미지: **7일** (재사용 가능성) +- 사업자번호 검증: **30일** (변경 빈도 낮음) + +#### 3.1.3 Kubernetes 배포 + +```yaml +# redis-deployment.yaml (개발/운영 공통) +apiVersion: v1 +kind: Service +metadata: + name: redis-cache +spec: + type: LoadBalancer + ports: + - port: 6379 + selector: + app: redis + +--- +# Azure Cache for Redis 연결 시 Secret +apiVersion: v1 +kind: Secret +metadata: + name: redis-secret +type: Opaque +data: + connection-string: +``` + +#### 3.1.4 기대 효과 + +| 지표 | Before | After (캐시 히트) | 개선율 | +|------|--------|-----------------|--------| +| AI 응답 시간 | 10초 | 0.1초 | **99%** | +| 이미지 생성 시간 | 5초 | 0.1초 | **98%** | +| AI API 비용 | $600/월 | $120/월 | **80% 절감** | +| 이미지 API 비용 | $600/월 | $120/월 | **80% 절감** | + +--- + +### 3.2 API Gateway 패턴 + +#### 3.2.1 개요 + +**목적**: 중앙집중식 인증, 라우팅, Rate Limiting + +**주요 기능**: +1. **인증/인가**: JWT 토큰 검증 +2. **라우팅**: URL 기반 서비스 라우팅 +3. **Rate Limiting**: API 호출 제한 +4. **로깅**: 중앙집중식 접근 로그 +5. **SSL 종료**: HTTPS 처리 + +#### 3.2.2 Azure 구현 옵션 + +**옵션 1: Azure API Management (권장 - 운영 환경)** +```yaml +특징: +- 완전 관리형 서비스 +- 내장 인증/인가, Rate Limiting +- Azure Monitor 통합 +- 높은 비용 (약 $50/월 이상) + +장점: +- 운영 부담 최소화 +- 기업급 기능 (개발자 포털, API 버전 관리) +- Azure 생태계 통합 + +단점: +- 비용 부담 +- 커스터마이징 제한 +``` + +**옵션 2: Nginx Ingress Controller (권장 - 개발 환경 / 비용 절감)** +```yaml +특징: +- AKS 내장 Ingress Controller +- 무료 (AKS 비용에 포함) +- 높은 커스터마이징 + +장점: +- 비용 효율적 +- Kubernetes 네이티브 +- 유연한 설정 + +단점: +- 수동 설정 필요 +- 운영 부담 +``` + +#### 3.2.3 Nginx Ingress 구현 (권장) + +```yaml +# nginx-ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: api-gateway-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/rate-limit: "100" # 100 req/min + nginx.ingress.kubernetes.io/ssl-redirect: "true" +spec: + ingressClassName: nginx + tls: + - hosts: + - api.kt-event.com + secretName: tls-secret + rules: + - host: api.kt-event.com + http: + paths: + - path: /api/users + pathType: Prefix + backend: + service: + name: user-service + port: + number: 8080 + - path: /api/events + pathType: Prefix + backend: + service: + name: event-service + port: + number: 8080 + - path: /api/ai + pathType: Prefix + backend: + service: + name: ai-service + port: + number: 8080 + - path: /api/content + pathType: Prefix + backend: + service: + name: content-service + port: + number: 8080 + - path: /api/distribution + pathType: Prefix + backend: + service: + name: distribution-service + port: + number: 8080 + - path: /api/participation + pathType: Prefix + backend: + service: + name: participation-service + port: + number: 8080 + - path: /api/analytics + pathType: Prefix + backend: + service: + name: analytics-service + port: + number: 8080 +``` + +**JWT 인증 미들웨어** (각 서비스에서 구현): +```java +// Spring Boot 예시 +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) { + String token = extractToken(request); + if (token != null && validateToken(token)) { + // 인증 성공 + filterChain.doFilter(request, response); + } else { + // 401 Unauthorized + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } +} +``` + +#### 3.2.4 기대 효과 + +| 항목 | 효과 | +|------|------| +| 개발 생산성 | 각 서비스 인증 로직 제거 → 개발 시간 **30% 단축** | +| 보안 강화 | 중앙집중식 보안 정책 관리 | +| 운영 효율 | 통합 모니터링 및 로깅 | +| 비용 절감 | Nginx Ingress 사용 시 **무료** (vs Azure API Management $50/월) | + +--- + +### 3.3 Asynchronous Request-Reply 패턴 + +#### 3.3.1 개요 + +**목적**: 장시간 작업의 사용자 대기 시간 제거 + +**적용 대상**: +- AI 서비스: 트렌드 분석 및 이벤트 추천 (10초 소요) +- Content 서비스: SNS 이미지 생성 (5초 소요) + +#### 3.3.2 Azure 구현 방식 + +**옵션 1: Azure Service Bus (권장 - 운영 환경)** +```yaml +특징: +- 완전 관리형 메시지 큐 +- 높은 안정성 및 가용성 +- Azure Monitor 통합 + +비용: +- Basic tier: $0.05/million operations +- Standard tier: $10/월 기본 + $0.80/million operations + +장점: +- 운영 부담 최소화 +- 엔터프라이즈급 안정성 +- Azure 생태계 통합 +``` + +**옵션 2: RabbitMQ (권장 - 개발 환경 / 비용 절감)** +```yaml +특징: +- 오픈소스 메시지 브로커 +- AKS에 직접 배포 +- 무료 (인프라 비용만) + +장점: +- 비용 효율적 +- 높은 커스터마이징 +- Kubernetes 네이티브 배포 + +단점: +- 운영 부담 (HA, 백업) +``` + +#### 3.3.3 RabbitMQ 구현 (권장) + +**Kubernetes 배포**: +```yaml +# rabbitmq-deployment.yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: rabbitmq +spec: + serviceName: rabbitmq + replicas: 1 # 개발: 1, 운영: 3 (HA) + selector: + matchLabels: + app: rabbitmq + template: + metadata: + labels: + app: rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3.12-management + ports: + - containerPort: 5672 + name: amqp + - containerPort: 15672 + name: management + env: + - name: RABBITMQ_DEFAULT_USER + value: "admin" + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: rabbitmq-secret + key: password + volumeMounts: + - name: rabbitmq-data + mountPath: /var/lib/rabbitmq + volumeClaimTemplates: + - metadata: + name: rabbitmq-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 10Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq +spec: + ports: + - port: 5672 + name: amqp + - port: 15672 + name: management + selector: + app: rabbitmq +``` + +**동작 흐름**: +``` +1. 클라이언트 요청 + → 즉시 Job ID 반환 (응답 시간 ~100ms) + +2. 백그라운드 처리 + → RabbitMQ에 Job 메시지 발행 + → Worker가 Job 처리 (10초) + → 결과를 Redis에 저장 + +3. 클라이언트 폴링 + → GET /api/jobs/{id} + → Redis에서 Job 상태 확인 + → 완료 시 결과 반환 +``` + +**Job 상태 관리** (Redis): +``` +job:{job_id}:status = "pending" | "processing" | "completed" | "failed" +job:{job_id}:result = {result_data} +job:{job_id}:progress = 0-100 (%) +TTL: 1시간 +``` + +#### 3.3.4 Spring Boot 구현 예시 + +```java +// AI 서비스 - Job 생성 +@PostMapping("/api/ai/recommendations") +public ResponseEntity createRecommendationJob(@RequestBody RecommendationRequest request) { + String jobId = UUID.randomUUID().toString(); + + // Redis에 Job 상태 저장 + redisTemplate.opsForValue().set("job:" + jobId + ":status", "pending", 1, TimeUnit.HOURS); + + // RabbitMQ에 Job 발행 + rabbitTemplate.convertAndSend("ai.queue", new JobMessage(jobId, request)); + + return ResponseEntity.ok(new JobResponse(jobId, "pending")); +} + +// Worker - Job 처리 +@RabbitListener(queues = "ai.queue") +public void processRecommendationJob(JobMessage message) { + String jobId = message.getJobId(); + + // Job 상태 업데이트: processing + redisTemplate.opsForValue().set("job:" + jobId + ":status", "processing"); + + try { + // AI API 호출 (10초 소요) + String result = callClaudeAPI(message.getRequest()); + + // 결과 저장 + redisTemplate.opsForValue().set("job:" + jobId + ":result", result, 1, TimeUnit.HOURS); + redisTemplate.opsForValue().set("job:" + jobId + ":status", "completed"); + + // 캐시에도 저장 (Cache-Aside) + String cacheKey = "ai:recommendation:" + getCacheKey(message.getRequest()); + redisTemplate.opsForValue().set(cacheKey, result, 24, TimeUnit.HOURS); + + } catch (Exception e) { + redisTemplate.opsForValue().set("job:" + jobId + ":status", "failed"); + redisTemplate.opsForValue().set("job:" + jobId + ":error", e.getMessage()); + } +} + +// Job 상태 조회 +@GetMapping("/api/jobs/{jobId}") +public ResponseEntity getJobStatus(@PathVariable String jobId) { + String status = redisTemplate.opsForValue().get("job:" + jobId + ":status"); + + if ("completed".equals(status)) { + String result = redisTemplate.opsForValue().get("job:" + jobId + ":result"); + return ResponseEntity.ok(new JobStatusResponse(jobId, status, result)); + } else { + return ResponseEntity.ok(new JobStatusResponse(jobId, status, null)); + } +} +``` + +#### 3.3.5 기대 효과 + +| 지표 | Before | After | 개선 | +|------|--------|-------|------| +| 사용자 대기 시간 | 10초 | 0.1초 (즉시 응답) | **99% 개선** | +| 동시 처리 가능 요청 | 10 req/sec | 50 req/sec | **5배 증가** | +| 서버 리소스 효율 | 동기 대기로 낭비 | 최적화 | **30% 절감** | + +--- + +### 3.4 Circuit Breaker 패턴 (Optional) + +#### 3.4.1 개요 + +**목적**: 외부 API 장애 시 서비스 격리 및 빠른 실패 + +**적용 대상**: +- 모든 외부 API 연동 지점 (7개 API) + +#### 3.4.2 구현 방식 (Resilience4j) + +**의존성 추가** (Spring Boot): +```xml + + io.github.resilience4j + resilience4j-spring-boot3 + 2.1.0 + +``` + +**설정** (application.yml): +```yaml +resilience4j: + circuitbreaker: + configs: + default: + slidingWindowSize: 10 + minimumNumberOfCalls: 5 + failureRateThreshold: 50 # 50% 실패 시 Open + waitDurationInOpenState: 30s + permittedNumberOfCallsInHalfOpenState: 3 + instances: + claudeAPI: + baseConfig: default + stableDiffusionAPI: + baseConfig: default + taxAPI: + baseConfig: default + distributionAPIs: + baseConfig: default +``` + +**코드 예시**: +```java +@Service +public class ClaudeAPIClient { + + @CircuitBreaker(name = "claudeAPI", fallbackMethod = "getRecommendationFallback") + public String getRecommendation(RecommendationRequest request) { + // Claude API 호출 + return restTemplate.postForObject(CLAUDE_API_URL, request, String.class); + } + + // Fallback: 캐시된 결과 반환 또는 기본 메시지 + public String getRecommendationFallback(RecommendationRequest request, Exception e) { + log.warn("Claude API circuit breaker activated, using fallback", e); + + // 캐시에서 유사한 이전 결과 조회 + String cachedResult = getCachedRecommendation(request); + + if (cachedResult != null) { + return cachedResult; + } + + // 기본 메시지 + return "AI 서비스가 일시적으로 이용 불가합니다. 잠시 후 다시 시도해주세요."; + } +} +``` + +#### 3.4.3 Fallback 전략 + +| 서비스 | 외부 API | Fallback 전략 | +|--------|---------|--------------| +| AI | Claude API | 캐시된 이전 추천 결과 + 안내 메시지 | +| Content | Stable Diffusion | 기본 템플릿 이미지 + 안내 메시지 | +| User | 국세청 API | 사업자번호 검증 스킵 (수동 확인으로 대체) | +| Distribution | 각 채널 API | 해당 채널 배포 스킵 + 알림 | + +#### 3.4.4 모니터링 (Azure Monitor) + +**커스텀 메트릭**: +```java +@Component +public class CircuitBreakerMetrics { + + private final MeterRegistry meterRegistry; + private final CircuitBreakerRegistry circuitBreakerRegistry; + + @PostConstruct + public void registerMetrics() { + circuitBreakerRegistry.getAllCircuitBreakers().forEach(cb -> { + Gauge.builder("circuit.breaker.state", cb, + this::getStateValue) + .tag("name", cb.getName()) + .register(meterRegistry); + + cb.getEventPublisher() + .onStateTransition(event -> { + log.info("Circuit Breaker {} state changed: {} -> {}", + event.getCircuitBreakerName(), + event.getStateTransition().getFromState(), + event.getStateTransition().getToState()); + }); + }); + } + + private int getStateValue(CircuitBreaker cb) { + switch (cb.getState()) { + case CLOSED: return 0; + case OPEN: return 1; + case HALF_OPEN: return 2; + default: return -1; + } + } +} +``` + +#### 3.4.5 기대 효과 + +| 지표 | Before | After | 개선 | +|------|--------|-------|------| +| 시스템 가용성 | 95% | 99% | **+4%p** | +| 장애 복구 시간 | 5분 (수동) | 30초 (자동) | **90% 단축** | +| 외부 API 장애 격리 | ❌ 전체 중단 | ✅ 독립 동작 | - | + +--- + +## 4. 전체 아키텍처 + +### 4.1 Azure Kubernetes 기반 아키텍처 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Azure Cloud │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Azure Kubernetes Service (AKS) │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────────┐ │ │ +│ │ │ Nginx Ingress Controller │ │ │ +│ │ │ - JWT 인증 │ │ │ +│ │ │ - Rate Limiting │ │ │ +│ │ │ - SSL 종료 │ │ │ +│ │ └─────────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ┌──────────────────────┼──────────────────────┐ │ │ +│ │ │ │ │ │ │ +│ │ ┌─▼───┐ ┌─────┐ ┌─────▼──┐ ┌────────┐ ┌───▼────┐ │ │ +│ │ │User │ │Event│ │ AI │ │Content │ │ Dist │ │ │ +│ │ │ Svc │ │ Svc │ │ Svc │ │ Svc │ │ Svc │ │ │ +│ │ └──┬──┘ └──┬──┘ └───┬────┘ └───┬────┘ └───┬────┘ │ │ +│ │ │ │ │ │ │ │ │ +│ │ └────────┴──────────┴────────────┴───────────┘ │ │ +│ │ │ │ │ +│ │ ┌────▼─────┐ │ │ +│ │ │RabbitMQ │ (Async Request-Reply) │ │ +│ │ │ Worker │ │ │ +│ │ └──────────┘ │ │ +│ └──────────────────────────────────────────────────────────── │ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Azure Cache for Redis (Cache-Aside) │ │ +│ │ - AI 결과 캐시 │ │ +│ │ - 이미지 캐시 │ │ +│ │ - Job 상태 관리 │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Azure Database for PostgreSQL │ │ +│ │ - 사용자 데이터 │ │ +│ │ - 이벤트 데이터 │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Azure Monitor + Application Insights │ │ +│ │ - 로그 수집 │ │ +│ │ - 메트릭 모니터링 │ │ +│ │ - 알람 │ │ +│ └────────────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 외부 API (Circuit Breaker 적용) │ +├─────────────────────────────────────────────────────────────────┤ +│ 국세청 API │ Claude API │ Stable Diffusion │ 우리동네TV │... │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 시퀀스 다이어그램 (AI 추천 플로우) + +``` +클라이언트 → Nginx Ingress: POST /api/ai/recommendations +Nginx Ingress → AI Service: JWT 검증 후 라우팅 + +AI Service → Redis: GET ai:recommendation:{key} + +alt Cache HIT + Redis → AI Service: 캐시된 결과 + AI Service → 클라이언트: 즉시 반환 (0.1초) +else Cache MISS + AI Service → Redis: SET job:{id}:status = pending + AI Service → RabbitMQ: 발행 Job Message + AI Service → 클라이언트: Job ID 반환 (0.1초) + + RabbitMQ → Worker: Job 수신 + Worker → Redis: SET job:{id}:status = processing + + Worker → Claude API: AI 요청 (Circuit Breaker) + + alt API 성공 + Claude API → Worker: 결과 반환 (10초) + Worker → Redis: SET ai:recommendation:{key} (TTL 24h) + Worker → Redis: SET job:{id}:result + Worker → Redis: SET job:{id}:status = completed + else API 실패 (Circuit Open) + Worker → Redis: GET 유사 캐시 결과 + Worker → Redis: SET job:{id}:result (Fallback) + Worker → Redis: SET job:{id}:status = completed + end + + loop 5초 간격 폴링 + 클라이언트 → Nginx Ingress: GET /api/jobs/{id} + Nginx Ingress → AI Service: 라우팅 + AI Service → Redis: GET job:{id}:status + + alt completed + Redis → AI Service: status + result + AI Service → 클라이언트: 최종 결과 + else processing + Redis → AI Service: status + progress + AI Service → 클라이언트: 진행 상황 (70%) + end + end +end +``` + +--- + +## 5. 개발/운영 환경 구성 + +### 5.1 개발 환경 + +**AKS 클러스터**: +```yaml +name: aks-dev-cluster +node_pool: + vm_size: Standard_B2s (2 vCPU, 4GB RAM) + node_count: 2 + auto_scaling: false + +estimated_cost: ~$100/월 +``` + +**Azure Cache for Redis**: +```yaml +tier: Standard +capacity: C1 (1GB) +availability: Single instance + +estimated_cost: ~$20/월 +``` + +**Azure Database for PostgreSQL**: +```yaml +tier: Basic +compute: B_Gen5_1 (1 vCore) +storage: 50GB + +estimated_cost: ~$30/월 +``` + +**RabbitMQ**: +```yaml +deployment: StatefulSet (1 replica) +storage: 10GB PVC + +estimated_cost: 포함 (AKS 노드) +``` + +**총 예상 비용**: **~$150/월** + +### 5.2 운영 환경 + +**AKS 클러스터**: +```yaml +name: aks-prod-cluster +node_pool: + vm_size: Standard_D2s_v3 (2 vCPU, 8GB RAM) + node_count: 3 (다중 가용 영역) + auto_scaling: true (min: 3, max: 10) + +estimated_cost: ~$300/월 (기본), ~$1,000/월 (최대) +``` + +**Azure Cache for Redis**: +```yaml +tier: Premium +capacity: P1 (6GB) +availability: Zone redundant (고가용성) + +estimated_cost: ~$250/월 +``` + +**Azure Database for PostgreSQL**: +```yaml +tier: General Purpose +compute: GP_Gen5_2 (2 vCore) +storage: 100GB +availability: Zone redundant + +estimated_cost: ~$150/월 +``` + +**RabbitMQ**: +```yaml +deployment: StatefulSet (3 replicas, HA) +storage: 30GB PVC per replica + +estimated_cost: 포함 (AKS 노드) +``` + +**Azure Monitor + Application Insights**: +```yaml +estimated_cost: ~$50/월 +``` + +**총 예상 비용**: **~$750/월** (기본), **~$1,450/월** (최대) + +--- + +## 6. 구현 로드맵 + +### 6.1 MVP 일정 (12주) + +| 주차 | 작업 내용 | 패턴 적용 | 담당 | +|------|----------|-----------|------| +| **1주** | Azure 인프라 구축 | - | DevOps | +| | - AKS 클러스터 생성 (개발/운영) | | | +| | - Azure Cache for Redis 생성 | | | +| | - Azure Database for PostgreSQL 생성 | | | +| | - Azure Container Registry 생성 | | | +| **2주** | API Gateway 구축 | API Gateway | DevOps + Backend | +| | - Nginx Ingress Controller 배포 | | | +| | - SSL 인증서 설정 | | | +| | - JWT 인증 미들웨어 구현 | | | +| **3-4주** | User/Event 서비스 | Cache-Aside | Backend | +| | - 회원가입/로그인 구현 | | | +| | - 사업자번호 검증 + 캐싱 | | | +| | - 이벤트 CRUD 구현 | | | +| **5-6주** | AI 서비스 | Async + Cache-Aside | Backend | +| | - Claude API 연동 | | | +| | - RabbitMQ 배포 및 Worker 구현 | | | +| | - Job 기반 비동기 처리 | | | +| | - AI 결과 캐싱 (24h TTL) | | | +| **7-8주** | Content 서비스 | Async + Cache-Aside | Backend | +| | - Stable Diffusion API 연동 | | | +| | - 이미지 생성 Job 처리 | | | +| | - 이미지 캐싱 및 Azure Blob Storage | | | +| **9-10주** | Distribution 서비스 | Circuit Breaker | Backend | +| | - 7개 외부 API 연동 | | | +| | - Resilience4j Circuit Breaker 적용 | | | +| | - Fallback 전략 구현 | | | +| **11주** | Participation/Analytics | Cache-Aside | Backend | +| | - 참여자 관리 및 추첨 | | | +| | - 대시보드 데이터 캐싱 (5분 TTL) | | | +| **12주** | 테스트 및 출시 | 전체 패턴 검증 | 전체 팀 | +| | - 부하 테스트 (k6, 100명) | | | +| | - 장애 시나리오 테스트 | | | +| | - Azure Monitor 알람 설정 | | | +| | - MVP 출시 | | | + +### 6.2 기술 스택 + +**백엔드**: +- Java 17 + Spring Boot 3.2 +- Resilience4j 2.1 (Circuit Breaker) +- Spring Data JPA + PostgreSQL +- Spring Data Redis +- Spring AMQP + RabbitMQ + +**컨테이너 & 오케스트레이션**: +- Docker 24 +- Kubernetes 1.28 (AKS) +- Nginx Ingress Controller 1.9 +- Helm 3.13 + +**Azure 서비스**: +- Azure Kubernetes Service (AKS) +- Azure Cache for Redis +- Azure Database for PostgreSQL +- Azure Container Registry (ACR) +- Azure Monitor + Application Insights +- Azure Blob Storage (이미지 저장) + +**모니터링 & 로깅**: +- Azure Monitor (메트릭) +- Application Insights (APM) +- Grafana (대시보드) +- Azure Log Analytics (로그 쿼리) + +--- + +## 7. 예상 성과 + +### 7.1 성능 개선 + +| 항목 | Before | After (캐시 히트) | 개선율 | +|------|--------|-----------------|--------| +| AI 응답 시간 | 10초 | 0.1초 | **99%** | +| 이미지 생성 시간 | 5초 | 0.1초 | **98%** | +| 대시보드 로딩 시간 | 3초 | 0.5초 | **83%** | +| 동시 처리 가능 요청 | 10 req/sec | 50 req/sec | **5배** | + +### 7.2 비용 절감 + +**AI API 비용** (캐시 히트율 80% 가정): +- Before: $600/월 +- After: $120/월 +- **절감액**: $480/월 (**80% 절감**) + +**이미지 API 비용** (캐시 히트율 80% 가정): +- Before: $600/월 +- After: $120/월 +- **절감액**: $480/월 (**80% 절감**) + +**총 API 비용 절감**: **$960/월** (**80% 절감**) + +**Azure 인프라 비용**: +- 개발 환경: ~$150/월 +- 운영 환경: ~$750/월 (기본), ~$1,450/월 (최대) + +### 7.3 가용성 개선 + +| 지표 | Before | After | 개선 | +|------|--------|-------|------| +| 시스템 가용성 | 95% | 99% | **+4%p** | +| 외부 API 장애 시 서비스 지속 | ❌ 불가 | ✅ 가능 | - | +| 장애 자동 복구 시간 | 5분 (수동) | 30초 (자동) | **90% 단축** | + +--- + +## 8. 모니터링 및 운영 + +### 8.1 Azure Monitor 메트릭 + +**AKS 클러스터**: +- CPU/메모리 사용률 +- 노드 상태 +- Pod 상태 +- Ingress 요청 수 및 응답 시간 + +**Azure Cache for Redis**: +- 캐시 히트율 (목표: 80% 이상) +- 메모리 사용률 (목표: 70% 이하) +- 응답 시간 (목표: 100ms 이하) +- 연결 수 + +**Azure Database for PostgreSQL**: +- CPU/메모리 사용률 +- 디스크 I/O +- 활성 연결 수 +- 슬로우 쿼리 + +**Application Insights (APM)**: +- 서비스별 응답 시간 +- 의존성 호출 (외부 API) +- 예외 및 에러 +- 사용자 트래픽 + +### 8.2 커스텀 메트릭 + +**Circuit Breaker**: +- API별 실패율 +- Circuit 상태 (Closed/Open/Half-Open) +- Fallback 호출 횟수 + +**Async Jobs**: +- Job 처리 시간 +- 동시 Job 수 +- Job 완료율 +- Queue 길이 (RabbitMQ) + +### 8.3 알람 임계값 + +| 지표 | Warning | Critical | 액션 | +|------|---------|----------|------| +| 캐시 히트율 | < 70% | < 50% | 캐시 전략 검토 | +| Redis 메모리 | > 80% | > 90% | 스케일업 | +| API 응답 시간 | > 500ms | > 1000ms | 성능 조사 | +| Circuit Breaker Open | 1개 | 3개 이상 | 긴급 대응 | +| Pod CPU | > 70% | > 90% | Pod 스케일아웃 | +| Pod 메모리 | > 80% | > 95% | Pod 재시작 | + +### 8.4 로그 수집 및 분석 + +**Azure Log Analytics**: +```kusto +// 에러 로그 분석 +ContainerLog +| where LogEntry contains "ERROR" +| summarize count() by Computer, ContainerID +| order by count_ desc + +// API 응답 시간 분석 +requests +| where url contains "/api/" +| summarize avg(duration), percentile(duration, 95) by name +| order by avg_duration desc + +// Circuit Breaker 상태 변화 추적 +traces +| where message contains "Circuit Breaker" +| project timestamp, message, severityLevel +| order by timestamp desc +``` + +--- + +## 9. 장애 대응 절차 + +### 9.1 Circuit Breaker Open 발생 + +**대응 절차**: +1. 알람 수신 (Azure Monitor → Slack/Email) +2. Application Insights에서 해당 외부 API 호출 현황 확인 +3. Fallback 전략 정상 동작 확인 (로그 분석) +4. 30초 후 자동 Half-Open 전환 확인 +5. 복구 실패 시: + - 외부 API 제공사 상태 확인 + - 긴급 연락 + - 장기 장애 시 사용자 공지 + +### 9.2 캐시 장애 + +**대응 절차**: +1. Azure Cache for Redis 상태 확인 (Azure Portal) +2. Zone Redundancy 자동 Failover 확인 (운영 환경) +3. 캐시 미스로 전환 (성능 저하 허용) +4. Redis 복구: + - 개발: 인스턴스 재시작 + - 운영: Azure 자동 복구 대기 또는 수동 Failover + +### 9.3 AKS 노드 장애 + +**대응 절차**: +1. Azure Monitor에서 노드 상태 확인 +2. Kubernetes 자동 재스케줄링 확인 +3. Pod가 정상 노드로 이동 확인 +4. 장애 노드 제거 및 새 노드 추가 (Auto Scaling) + +--- + +## 10. 체크리스트 + +### 10.1 패턴 적용 완료 확인 + +- [ ] **Cache-Aside**: AI 결과, 이미지, 사업자번호 검증 캐싱 완료 +- [ ] **API Gateway**: Nginx Ingress + JWT 인증 + Rate Limiting 완료 +- [ ] **Asynchronous Request-Reply**: RabbitMQ + Worker 기반 Job 처리 완료 +- [ ] **Circuit Breaker**: Resilience4j 적용 및 Fallback 전략 구현 완료 + +### 10.2 성능 목표 달성 확인 + +- [ ] AI 응답 시간: 10초 이내 (캐시 미스), 0.1초 (캐시 히트) +- [ ] 캐시 히트율: 80% 이상 +- [ ] 시스템 가용성: 99% 이상 +- [ ] 동시 처리 가능 요청: 50 req/sec 이상 + +### 10.3 Azure 인프라 구축 완료 확인 + +- [ ] AKS 클러스터 생성 (개발/운영) +- [ ] Azure Cache for Redis 구성 및 연결 +- [ ] Azure Database for PostgreSQL 구성 및 연결 +- [ ] Azure Container Registry 구성 및 CI/CD 연동 +- [ ] Nginx Ingress Controller 배포 및 설정 +- [ ] SSL 인증서 적용 +- [ ] Azure Monitor + Application Insights 통합 + +### 10.4 운영 준비 완료 확인 + +- [ ] 모니터링 대시보드 구축 (Azure Monitor + Grafana) +- [ ] 알람 설정 완료 (Slack/Email) +- [ ] 장애 대응 매뉴얼 작성 +- [ ] 부하 테스트 완료 (k6, 100명) +- [ ] 장애 시나리오 테스트 완료 (Circuit Breaker, 캐시 장애) + +--- + +## 11. 다음 단계 (Phase 2 이후) + +**MVP 이후 확장 계획** (선택 사항): + +- **Retry 패턴**: 일시적 오류 자동 재시도 (현재는 Circuit Breaker로 커버) +- **Queue-Based Load Leveling**: 트래픽 폭증 시 부하 분산 +- **Saga 패턴**: 복잡한 분산 트랜잭션 관리 (이벤트 생성 플로우) +- **CQRS**: 읽기/쓰기 분리로 대시보드 성능 최적화 +- **Event Sourcing**: 이벤트 변경 이력 추적 및 감사 +- **Service Mesh (Istio)**: 고급 트래픽 관리 및 보안 +- **Azure Front Door**: 글로벌 CDN 및 WAF +- **Azure Key Vault**: 시크릿 관리 강화 + +--- + +## 참고 문서 + +- [유저스토리](../userstory.md) +- [UI/UX 설계서](../uiux/uiux.md) +- [클라우드 디자인 패턴 개요](../../claude/cloud-design-patterns.md) +- [Azure Kubernetes Service 공식 문서](https://learn.microsoft.com/ko-kr/azure/aks/) +- [Azure Cache for Redis 공식 문서](https://learn.microsoft.com/ko-kr/azure/azure-cache-for-redis/) +- [Resilience4j 공식 문서](https://resilience4j.readme.io/) +- [백업 파일](./backup/) - 이전 버전 diff --git a/design/uiux/prototype/01-로그인.html b/design/uiux/prototype/01-로그인.html new file mode 100644 index 0000000..53e723a --- /dev/null +++ b/design/uiux/prototype/01-로그인.html @@ -0,0 +1,196 @@ + + + + + + 로그인 - KT AI 이벤트 마케팅 + + + + + +
+
+ +
+
+ celebration +
+

KT AI 이벤트

+

소상공인을 위한 스마트 마케팅

+
+ + +
+
+ + + +
+ +
+ + + +
+ +
+ + +
+ + + + +
+ + +
+
+ 또는 +
+
+ + +
+ + +
+ + +
+

+ 회원가입 시 이용약관개인정보처리방침에 동의하게 됩니다. +

+
+
+
+ + + + + diff --git a/design/uiux/prototype/02-회원가입.html b/design/uiux/prototype/02-회원가입.html new file mode 100644 index 0000000..5889254 --- /dev/null +++ b/design/uiux/prototype/02-회원가입.html @@ -0,0 +1,400 @@ + + + + + + 회원가입 - KT AI 이벤트 마케팅 + + + + + +
+ +
+
+ +

회원가입

+
+
+ + +
+
+

1/3 단계

+
+ +
+ + + + + + + + +
+
+ + + + + diff --git a/design/uiux/prototype/03-프로필.html b/design/uiux/prototype/03-프로필.html new file mode 100644 index 0000000..359df83 --- /dev/null +++ b/design/uiux/prototype/03-프로필.html @@ -0,0 +1,214 @@ + + + + + + 프로필 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+ person +
+

홍길동

+

hong@example.com

+
+ + +
+

기본 정보

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

매장 정보

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

비밀번호 변경

+ +
+ + +
+ +
+ + + 8자 이상, 영문과 숫자를 포함해주세요 +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/04-로그아웃확인.html b/design/uiux/prototype/04-로그아웃확인.html new file mode 100644 index 0000000..b6c1acd --- /dev/null +++ b/design/uiux/prototype/04-로그아웃확인.html @@ -0,0 +1,54 @@ + + + + + + 로그아웃 확인 - KT AI 이벤트 마케팅 + + + + + + + + + + + + diff --git a/design/uiux/prototype/05-대시보드.html b/design/uiux/prototype/05-대시보드.html new file mode 100644 index 0000000..afd3a1a --- /dev/null +++ b/design/uiux/prototype/05-대시보드.html @@ -0,0 +1,219 @@ + + + + + + 대시보드 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+

안녕하세요, 사용자님!

+

오늘도 성공적인 이벤트를 준비해보세요

+
+ + +
+
+
+
+
+
+
+ + +
+
+

빠른 시작

+
+
+ + +
+
+ + +
+
+

진행 중인 이벤트

+ + 전체보기 + chevron_right + +
+
+ +
+
+ + +
+

최근 활동

+
+
+ +
+
+
+
+ + +
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/06-이벤트목록.html b/design/uiux/prototype/06-이벤트목록.html new file mode 100644 index 0000000..3a9dfac --- /dev/null +++ b/design/uiux/prototype/06-이벤트목록.html @@ -0,0 +1,323 @@ + + + + + + 이벤트 목록 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+
+ search + +
+
+
+ + +
+
+ filter_list + + +
+
+ + +
+
+

정렬:

+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/07-이벤트목적선택.html b/design/uiux/prototype/07-이벤트목적선택.html new file mode 100644 index 0000000..6fb6465 --- /dev/null +++ b/design/uiux/prototype/07-이벤트목적선택.html @@ -0,0 +1,216 @@ + + + + + + 이벤트 목적 선택 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+ auto_awesome +
+

이벤트 목적을 선택해주세요

+

AI가 목적에 맞는 최적의 이벤트를 추천해드립니다

+
+ + +
+
+ +
+
+ + +
+ + +
+ + +
+
+
+ info +
+

+ 선택하신 목적에 따라 AI가 업종, 지역, 계절 트렌드를 분석하여 가장 효과적인 이벤트를 추천합니다. +

+
+
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/08-AI이벤트추천.html b/design/uiux/prototype/08-AI이벤트추천.html new file mode 100644 index 0000000..b4e8291 --- /dev/null +++ b/design/uiux/prototype/08-AI이벤트추천.html @@ -0,0 +1,473 @@ + + + + + + AI 이벤트 추천 - KT AI 이벤트 마케팅 + + + + + + +
+ + + +
+ +
+

+ insights + AI 트렌드 분석 +

+
+
+
+ store + 업종 트렌드 +
+

음식점업 신년 프로모션 트렌드

+
+
+
+ location_on + 지역 트렌드 +
+

강남구 음식점 할인 이벤트 증가

+
+
+
+ wb_sunny + 시즌 트렌드 +
+

설 연휴 특수 대비 고객 유치 전략

+
+
+
+ + +
+

예산별 추천 이벤트

+

+ 각 예산별 2가지 방식 (온라인 1개, 오프라인 1개)을 추천합니다 +

+
+ + +
+
+ + + +
+
+ + +
+

💰 옵션 1: 저비용 (25~30만원)

+
+
+
+ +
+
+ 🌐 온라인 방식 +

+ SNS 팔로우 이벤트 + edit +

+
+
+ 경품: + 커피 쿠폰 + edit +
+
+
+ 참여 방법: + SNS 팔로우 +
+
+ 예상 참여: + 180명 +
+
+ 예상 비용: + 25만원 +
+
+ 투자대비수익률: + 520% +
+
+
+ +
+
+ +
+
+ 🏪 오프라인 방식 +

+ 전화번호 등록 이벤트 + edit +

+
+
+ 경품: + 커피 쿠폰 + edit +
+
+
+ 참여 방법: + 전화번호 등록 +
+
+ 예상 참여: + 150명 +
+
+ 예상 비용: + 30만원 +
+
+ 투자대비수익률: + 450% +
+
+
+
+
+ + +
+

💰💰 옵션 2: 중비용 (150~180만원)

+
+
+
+ +
+
+ 🌐 온라인 방식 +

+ 리뷰 작성 이벤트 + edit +

+
+
+ 경품: + 5천원 상품권 + edit +
+
+
+ 참여 방법: + 리뷰 작성 +
+
+ 예상 참여: + 250명 +
+
+ 예상 비용: + 150만원 +
+
+ 투자대비수익률: + 380% +
+
+
+ +
+
+ +
+
+ 🏪 오프라인 방식 +

+ 방문 도장 적립 이벤트 + edit +

+
+
+ 경품: + 무료 식사권 + edit +
+
+
+ 참여 방법: + 방문 도장 적립 +
+
+ 예상 참여: + 200명 +
+
+ 예상 비용: + 180만원 +
+
+ 투자대비수익률: + 320% +
+
+
+
+
+ + +
+

💰💰💰 옵션 3: 고비용 (500~600만원)

+
+
+
+ +
+
+ 🌐 온라인 방식 +

+ 인플루언서 협업 이벤트 + edit +

+
+
+ 경품: + 1만원 할인권 + edit +
+
+
+ 참여 방법: + 인플루언서 팔로우 +
+
+ 예상 참여: + 400명 +
+
+ 예상 비용: + 500만원 +
+
+ 투자대비수익률: + 280% +
+
+
+ +
+
+ +
+
+ 🏪 오프라인 방식 +

+ VIP 고객 초대 이벤트 + edit +

+
+
+ 경품: + 특별 메뉴 제공 + edit +
+
+
+ 참여 방법: + VIP 초대장 +
+
+ 예상 참여: + 300명 +
+
+ 예상 비용: + 600만원 +
+
+ 투자대비수익률: + 240% +
+
+
+
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/09-콘텐츠미리보기.html b/design/uiux/prototype/09-콘텐츠미리보기.html new file mode 100644 index 0000000..4cb5c02 --- /dev/null +++ b/design/uiux/prototype/09-콘텐츠미리보기.html @@ -0,0 +1,447 @@ + + + + + + SNS 이미지 생성 - KT AI 이벤트 마케팅 + + + + + + +
+ + + +
+ +
+ psychology +

AI 이미지 생성 중

+

+ 딥러닝 모델이 이벤트에 어울리는
+ 이미지를 생성하고 있어요... +

+

예상 시간: 5초

+
+ + + +
+ + +
+
+ + +
+ + 이벤트 이미지 미리보기 +
+ + + + + diff --git a/design/uiux/prototype/10-콘텐츠편집.html b/design/uiux/prototype/10-콘텐츠편집.html new file mode 100644 index 0000000..c14bf88 --- /dev/null +++ b/design/uiux/prototype/10-콘텐츠편집.html @@ -0,0 +1,209 @@ + + + + + + 콘텐츠 편집 - KT AI 이벤트 마케팅 + + + + + + +
+ + + +
+ +
+ +
+

미리보기

+
+
+ celebration +

신규고객 유치 이벤트

+

커피 쿠폰 100매

+

전화번호를 입력하고 참여하세요

+
+
+
+ + +
+

편집

+ + +
+

+ edit + 텍스트 편집 +

+ +
+ + + 11/50자 +
+ +
+ + + 9/30자 +
+ +
+ + + 17/100자 +
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/11-배포채널선택.html b/design/uiux/prototype/11-배포채널선택.html new file mode 100644 index 0000000..718dc3a --- /dev/null +++ b/design/uiux/prototype/11-배포채널선택.html @@ -0,0 +1,421 @@ + + + + + + 배포 채널 선택 - KT AI 이벤트 마케팅 + + + + + + +
+ + + +
+ +
+

배포 채널을 선택해주세요

+

(최소 1개 이상)

+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+

예상 노출: 5만명

+

비용: 8만원

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+

연결음 자동 업데이트

+

예상 노출: 3만명

+

비용: 무료

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

예상 노출: 계산중...

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+

예상 노출: -

+

비용: 무료

+
+
+
+
+ + +
+
+
+ 총 예상 비용 + 0원 +
+
+ 총 예상 노출 + 0명 +
+
+
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/12-최종승인.html b/design/uiux/prototype/12-최종승인.html new file mode 100644 index 0000000..b061730 --- /dev/null +++ b/design/uiux/prototype/12-최종승인.html @@ -0,0 +1,367 @@ + + + + + + 최종 승인 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+ check_circle +
+

이벤트를 확인해주세요

+

+ 모든 정보를 검토한 후 배포하세요 +

+
+ + +
+
+

+ SNS 팔로우 이벤트 +

+ +
+ 배포 대기 + + AI 추천 + +
+ +
+
+
+

이벤트 기간

+

+ 2025.02.01 ~ 2025.02.28 +

+
+
+

목표 참여자

+

180명

+
+
+

예상 비용

+

250,000원

+
+
+

예상 ROI

+

+ 520% +

+
+
+
+
+
+ + +
+

이벤트 상세

+ +
+
+ celebration +
+

이벤트 제목

+

SNS 팔로우 이벤트

+
+ +
+
+ +
+
+ card_giftcard +
+

경품

+

커피 쿠폰

+
+ +
+
+ +
+
+ description +
+

이벤트 설명

+

+ SNS를 팔로우하고 커피 쿠폰을 받으세요!
+ 많은 참여 부탁드립니다. +

+
+ +
+
+ +
+
+ how_to_reg +
+

참여 방법

+

SNS 팔로우

+
+
+
+
+ + +
+

배포 채널

+
+
+ + language + 홈페이지 + + + chat_bubble + 카카오톡 + + + share + Instagram + +
+ +
+
+ + +
+
+
+ + +
+ 약관 보기 +
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/13-이벤트상세.html b/design/uiux/prototype/13-이벤트상세.html new file mode 100644 index 0000000..2df2ebb --- /dev/null +++ b/design/uiux/prototype/13-이벤트상세.html @@ -0,0 +1,437 @@ + + + + + + 이벤트 상세 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+

SNS 팔로우 이벤트

+ +
+ +
+ 진행중 + + AI 추천 + +
+ +

+ 2025.01.15 ~ 2025.02.15 +

+
+ + +
+
+

실시간 현황

+
+ fiber_manual_record + 실시간 업데이트 +
+
+ +
+
+
+
+
+
+
+ + +
+

참여 추이

+
+
+
+ + + +
+
+ + +
+
+ show_chart +

참여자 추이 차트

+
+
+
+
+ + +
+

이벤트 정보

+ +
+
+ card_giftcard +
+

경품

+

커피 쿠폰

+
+
+
+ +
+
+ how_to_reg +
+

참여 방법

+

SNS 팔로우

+
+
+
+ +
+
+ attach_money +
+

예상 비용

+

250,000원

+
+
+
+ +
+
+ share +
+

배포 채널

+
+ 홈페이지 + 카카오톡 + Instagram +
+
+
+
+
+ + +
+

빠른 작업

+
+ + + + +
+
+ + +
+
+

최근 참여자

+ + 전체보기 + chevron_right + +
+ +
+
+ +
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/14-참여자목록.html b/design/uiux/prototype/14-참여자목록.html new file mode 100644 index 0000000..a2fc5f4 --- /dev/null +++ b/design/uiux/prototype/14-참여자목록.html @@ -0,0 +1,400 @@ + + + + + + 참여자 목록 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+
+ search + +
+
+
+ + +
+
+ filter_list + + +
+
+ + +
+
+

128명 참여

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/15-이벤트참여.html b/design/uiux/prototype/15-이벤트참여.html new file mode 100644 index 0000000..603a92f --- /dev/null +++ b/design/uiux/prototype/15-이벤트참여.html @@ -0,0 +1,309 @@ + + + + + + 이벤트 참여 - KT AI 이벤트 마케팅 + + + + + + +
+ +
+ celebration +

신규고객 유치 이벤트

+
+ + +
+
+ card_giftcard +
+

경품

+

커피 쿠폰

+
+
+ +
+ calendar_today +
+

기간

+

2025-11-01 ~ 2025-11-15

+
+
+
+ + +
+ + +
+

참여하기

+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+

+ 참여자: 128명 +

+
+
+ + +
+
+ check +
+

참여가 완료되었습니다!

+

+ 홍길동님의 행운을 기원합니다! +

+ +
+

당첨자 발표

+

2025-11-16 (월)

+
+ + +
+ + + + + diff --git a/design/uiux/prototype/16-당첨자추첨.html b/design/uiux/prototype/16-당첨자추첨.html new file mode 100644 index 0000000..00749c1 --- /dev/null +++ b/design/uiux/prototype/16-당첨자추첨.html @@ -0,0 +1,536 @@ + + + + + + 당첨자 추첨 - KT AI 이벤트 마케팅 + + + + + + +
+ +
+ +
+
+
+ event_note +

이벤트 정보

+
+
+

이벤트명

+

신규고객 유치 이벤트

+
+
+

총 참여자

+

127명

+
+
+

추첨 상태

+

추첨 전

+
+
+
+ + +
+
+
+ tune +

추첨 설정

+
+ +
+ +
+ +
5
+ +
+
+ +
+ + +
+ +
+

+ info + 추첨 방식 +

+

• 난수 기반 무작위 추첨

+

• 모든 추첨 과정은 자동 기록됩니다

+
+
+
+ + +
+ +
+ + +
+

📜 추첨 이력 (최근 3건)

+
+ +
+
+
+ + +
+ +
+

🎉 추첨 완료!

+

127명 중 5명 당첨

+
+ + +
+

🏆 당첨자 목록

+
+ +
+
+

🌟 매장 방문 고객 가산점 적용

+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
🎰
+

추첨 중...

+

난수 생성 중

+
+ + + + + diff --git a/design/uiux/prototype/17-성과분석.html b/design/uiux/prototype/17-성과분석.html new file mode 100644 index 0000000..77feecf --- /dev/null +++ b/design/uiux/prototype/17-성과분석.html @@ -0,0 +1,430 @@ + + + + + + 실시간 대시보드 - KT AI 이벤트 마케팅 + + + + + + +
+ + + +
+ +
+

📊 요약 (실시간)

+
+
+ 방금 전 +
+
+ + +
+
+
+

참여자 수

+
128명
+

↑ 12명 (오늘)

+
+
+

총 비용

+
30만원
+

경품 25만 + 채널 5만

+
+
+

예상 수익

+
135만원
+

매출증가 100만 + LTV 35만

+
+
+

투자대비수익률

+
450%
+

목표 300% 달성

+
+
+
+ + +
+ +
+
+
+ pie_chart +

채널별 성과

+
+
+ donut_large +

파이 차트

+
+
+
+
+ 우리동네TV + 45% (58명) +
+
+
+ 링고비즈 + 30% (38명) +
+
+
+ SNS + 25% (32명) +
+
+
+
+ + +
+
+
+ show_chart +

시간대별 참여 추이

+
+
+ trending_up +

라인 차트

+
+
+

피크 시간: 오후 2-4시 (35명)

+

평균 시간당: 8명

+
+
+
+
+ + +
+ +
+
+
+ payments +

투자대비수익률 상세

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
총 비용: 30만원
• 경품 비용25만원
• 채널 비용5만원
예상 수익: 135만원
• 매출 증가100만원
• 신규 고객 LTV35만원
투자대비수익률
+
+

(수익 - 비용) ÷ 비용 × 100

+

(135만 - 30만) ÷ 30만 × 100

+

= 450%

+
+
+
+
+ + +
+
+
+ people +

참여자 프로필

+
+ +
+

연령별

+
+
+ 20대 +
+
35%
+
+
+
+ 30대 +
+
40%
+
+
+
+ 40대 +
+
25%
+
+
+
+
+ +
+

성별

+
+
+ 여성 +
+
60%
+
+
+
+ 남성 +
+
40%
+
+
+
+
+
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/common.js b/design/uiux/prototype/common.js new file mode 100644 index 0000000..0f796bd --- /dev/null +++ b/design/uiux/prototype/common.js @@ -0,0 +1,1071 @@ +// ================================================================= +// KT AI 이벤트 마케팅 서비스 - 공통 JavaScript 모듈 +// Version: 1.0.0 +// ================================================================= + +const KTEventApp = (() => { + 'use strict'; + + // ================================================================= + // 1. Utility Functions + // ================================================================= + const Utils = { + /** + * 전화번호 포맷팅 (010-1234-5678) + */ + formatPhoneNumber(phone) { + const cleaned = phone.replace(/\D/g, ''); + const match = cleaned.match(/^(\d{3})(\d{3,4})(\d{4})$/); + if (match) { + return `${match[1]}-${match[2]}-${match[3]}`; + } + return phone; + }, + + /** + * 이메일 유효성 검사 + */ + validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + }, + + /** + * 비밀번호 유효성 검사 (8자 이상, 영문+숫자 포함) + */ + validatePassword(password) { + const hasLength = password.length >= 8; + const hasLetter = /[a-zA-Z]/.test(password); + const hasNumber = /\d/.test(password); + return hasLength && hasLetter && hasNumber; + }, + + /** + * 날짜 포맷팅 (YYYY.MM.DD) + */ + formatDate(date) { + if (typeof date === 'string') { + date = new Date(date); + } + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}.${month}.${day}`; + }, + + /** + * 날짜 시간 포맷팅 (YYYY.MM.DD HH:MM) + */ + formatDateTime(date) { + if (typeof date === 'string') { + date = new Date(date); + } + const dateStr = this.formatDate(date); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${dateStr} ${hours}:${minutes}`; + }, + + /** + * 숫자 포맷팅 (1,234,567) + */ + formatNumber(num) { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + }, + + /** + * 금액 포맷팅 (1,234,567원) + */ + formatCurrency(amount) { + return `${this.formatNumber(amount)}원`; + }, + + /** + * 디바운스 함수 + */ + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + /** + * LocalStorage 저장 + */ + saveToStorage(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (e) { + console.error('Storage save failed:', e); + return false; + } + }, + + /** + * LocalStorage 읽기 + */ + getFromStorage(key, defaultValue = null) { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (e) { + console.error('Storage read failed:', e); + return defaultValue; + } + }, + + /** + * LocalStorage 삭제 + */ + removeFromStorage(key) { + try { + localStorage.removeItem(key); + return true; + } catch (e) { + console.error('Storage remove failed:', e); + return false; + } + }, + + /** + * 랜덤 ID 생성 + */ + generateId() { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + }; + + // ================================================================= + // 2. Navigation Components + // ================================================================= + const Navigation = { + /** + * 헤더 생성 + */ + createHeader({ title = '', showBack = true, showMenu = true, showProfile = true } = {}) { + const header = document.createElement('header'); + header.className = 'header'; + + const leftDiv = document.createElement('div'); + leftDiv.className = 'header-left'; + + if (showMenu) { + const menuBtn = document.createElement('button'); + menuBtn.className = 'header-icon-btn'; + menuBtn.innerHTML = 'menu'; + menuBtn.setAttribute('aria-label', '메뉴'); + leftDiv.appendChild(menuBtn); + } + + if (showBack) { + const backBtn = document.createElement('button'); + backBtn.className = 'header-icon-btn'; + backBtn.innerHTML = 'arrow_back'; + backBtn.setAttribute('aria-label', '뒤로가기'); + backBtn.addEventListener('click', () => window.history.back()); + leftDiv.appendChild(backBtn); + } + + const titleEl = document.createElement('h1'); + titleEl.className = 'header-title'; + titleEl.textContent = title; + leftDiv.appendChild(titleEl); + + const rightDiv = document.createElement('div'); + rightDiv.className = 'header-right'; + + if (showProfile) { + const profileBtn = document.createElement('button'); + profileBtn.className = 'header-icon-btn'; + profileBtn.innerHTML = 'account_circle'; + profileBtn.setAttribute('aria-label', '프로필'); + rightDiv.appendChild(profileBtn); + } + + header.appendChild(leftDiv); + header.appendChild(rightDiv); + + return header; + }, + + /** + * 하단 네비게이션 생성 + */ + createBottomNav(currentPage = 'home') { + const nav = document.createElement('nav'); + nav.className = 'bottom-nav'; + nav.setAttribute('role', 'navigation'); + nav.setAttribute('aria-label', '주 네비게이션'); + + const navItems = [ + { id: 'home', label: '홈', icon: 'home', href: '05-대시보드.html' }, + { id: 'events', label: '이벤트', icon: 'celebration', href: '06-이벤트목록.html' }, + { id: 'analytics', label: '분석', icon: 'analytics', href: '17-성과분석.html' }, + { id: 'profile', label: '프로필', icon: 'person', href: '03-프로필.html' } + ]; + + navItems.forEach(item => { + const link = document.createElement('a'); + link.className = 'bottom-nav-item'; + if (item.id === currentPage) { + link.classList.add('active'); + } + link.href = item.href; + link.innerHTML = ` + ${item.icon} + ${item.label} + `; + nav.appendChild(link); + }); + + return nav; + }, + + /** + * Floating Action Button 생성 + */ + createFAB(icon = 'add', onClick) { + const fab = document.createElement('button'); + fab.className = 'fab'; + fab.innerHTML = `${icon}`; + fab.setAttribute('aria-label', '새 이벤트 생성'); + + if (onClick) { + fab.addEventListener('click', onClick); + } + + return fab; + } + }; + + // ================================================================= + // 3. Form Components + // ================================================================= + const Form = { + /** + * 입력 필드 생성 + */ + createInput({ + type = 'text', + id, + name, + label, + placeholder = '', + required = false, + value = '', + error = '', + hint = '', + disabled = false, + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const input = document.createElement('input'); + input.type = type; + input.id = id; + input.name = name || id; + input.className = 'form-input'; + input.placeholder = placeholder; + input.value = value; + input.disabled = disabled; + + if (required) { + input.required = true; + } + + if (onChange) { + input.addEventListener('input', onChange); + } + + group.appendChild(input); + + if (error) { + const errorEl = document.createElement('span'); + errorEl.className = 'form-error'; + errorEl.textContent = error; + group.appendChild(errorEl); + } + + if (hint) { + const hintEl = document.createElement('span'); + hintEl.className = 'form-hint'; + hintEl.textContent = hint; + group.appendChild(hintEl); + } + + return { group, input }; + }, + + /** + * 선택 필드 생성 + */ + createSelect({ + id, + name, + label, + options = [], + required = false, + value = '', + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const select = document.createElement('select'); + select.id = id; + select.name = name || id; + select.className = 'form-select'; + + if (required) { + select.required = true; + } + + options.forEach(opt => { + const option = document.createElement('option'); + option.value = opt.value; + option.textContent = opt.label; + if (opt.value === value) { + option.selected = true; + } + select.appendChild(option); + }); + + if (onChange) { + select.addEventListener('change', onChange); + } + + group.appendChild(select); + + return { group, select }; + }, + + /** + * 텍스트 영역 생성 + */ + createTextarea({ + id, + name, + label, + placeholder = '', + required = false, + value = '', + rows = 4, + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const textarea = document.createElement('textarea'); + textarea.id = id; + textarea.name = name || id; + textarea.className = 'form-textarea'; + textarea.placeholder = placeholder; + textarea.value = value; + textarea.rows = rows; + + if (required) { + textarea.required = true; + } + + if (onChange) { + textarea.addEventListener('input', onChange); + } + + group.appendChild(textarea); + + return { group, textarea }; + }, + + /** + * 체크박스 생성 + */ + createCheckbox({ + id, + name, + label, + checked = false, + onChange + } = {}) { + const checkDiv = document.createElement('div'); + checkDiv.className = 'form-check'; + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.id = id; + input.name = name || id; + input.className = 'form-check-input'; + input.checked = checked; + + if (onChange) { + input.addEventListener('change', onChange); + } + + const labelEl = document.createElement('label'); + labelEl.className = 'form-check-label'; + labelEl.setAttribute('for', id); + labelEl.textContent = label; + + checkDiv.appendChild(input); + checkDiv.appendChild(labelEl); + + return { checkDiv, input }; + }, + + /** + * 라디오 버튼 생성 + */ + createRadio({ + id, + name, + value, + label, + checked = false, + onChange + } = {}) { + const radioDiv = document.createElement('div'); + radioDiv.className = 'form-check'; + + const input = document.createElement('input'); + input.type = 'radio'; + input.id = id; + input.name = name; + input.value = value; + input.className = 'form-check-input'; + input.checked = checked; + + if (onChange) { + input.addEventListener('change', onChange); + } + + const labelEl = document.createElement('label'); + labelEl.className = 'form-check-label'; + labelEl.setAttribute('for', id); + labelEl.textContent = label; + + radioDiv.appendChild(input); + radioDiv.appendChild(labelEl); + + return { radioDiv, input }; + }, + + /** + * 버튼 생성 + */ + createButton({ + text, + variant = 'primary', + size = 'large', + icon = null, + fullWidth = false, + disabled = false, + onClick + } = {}) { + const button = document.createElement('button'); + button.className = `btn btn-${variant} btn-${size}`; + + if (fullWidth) { + button.classList.add('btn-full'); + } + + button.disabled = disabled; + + if (icon) { + const iconEl = document.createElement('span'); + iconEl.className = 'material-icons'; + iconEl.textContent = icon; + button.appendChild(iconEl); + } + + const textNode = document.createTextNode(text); + button.appendChild(textNode); + + if (onClick) { + button.addEventListener('click', onClick); + } + + return button; + } + }; + + // ================================================================= + // 4. Feedback Components + // ================================================================= + const Feedback = { + /** + * Toast 메시지 표시 + */ + showToast(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, duration); + }, + + /** + * Modal 표시 + */ + showModal({ title, content, buttons = [] } = {}) { + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + const modal = document.createElement('div'); + modal.className = 'modal'; + + if (title) { + const header = document.createElement('div'); + header.className = 'modal-header'; + const titleEl = document.createElement('h2'); + titleEl.className = 'modal-title'; + titleEl.textContent = title; + header.appendChild(titleEl); + modal.appendChild(header); + } + + if (content) { + const body = document.createElement('div'); + body.className = 'modal-body'; + if (typeof content === 'string') { + body.innerHTML = content; + } else { + body.appendChild(content); + } + modal.appendChild(body); + } + + if (buttons.length > 0) { + const footer = document.createElement('div'); + footer.className = 'modal-footer'; + + buttons.forEach(btnConfig => { + const btn = Form.createButton(btnConfig); + footer.appendChild(btn); + }); + + modal.appendChild(footer); + } + + const close = () => { + backdrop.remove(); + }; + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + close(); + } + }); + + backdrop.appendChild(modal); + document.body.appendChild(backdrop); + + return { backdrop, modal, close }; + }, + + /** + * Bottom Sheet 표시 + */ + showBottomSheet(content) { + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + const sheet = document.createElement('div'); + sheet.className = 'bottom-sheet'; + + const handle = document.createElement('div'); + handle.className = 'bottom-sheet-handle'; + sheet.appendChild(handle); + + const sheetContent = document.createElement('div'); + sheetContent.className = 'bottom-sheet-content'; + + if (typeof content === 'string') { + sheetContent.innerHTML = content; + } else { + sheetContent.appendChild(content); + } + + sheet.appendChild(sheetContent); + + const close = () => { + backdrop.remove(); + }; + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + close(); + } + }); + + backdrop.appendChild(sheet); + document.body.appendChild(backdrop); + + return { backdrop, sheet, close }; + }, + + /** + * Loading Spinner 표시 + */ + showSpinner(message = '') { + const container = document.createElement('div'); + container.className = 'spinner-center'; + container.innerHTML = ` +
+
+ ${message ? `

${message}

` : ''} +
+ `; + return container; + }, + + /** + * Progress Bar 생성 + */ + createProgressBar(value = 0) { + const progress = document.createElement('div'); + progress.className = 'progress'; + + const bar = document.createElement('div'); + bar.className = 'progress-bar'; + bar.style.width = `${value}%`; + bar.setAttribute('role', 'progressbar'); + bar.setAttribute('aria-valuenow', value); + bar.setAttribute('aria-valuemin', '0'); + bar.setAttribute('aria-valuemax', '100'); + + progress.appendChild(bar); + + return { + element: progress, + setValue: (newValue) => { + bar.style.width = `${newValue}%`; + bar.setAttribute('aria-valuenow', newValue); + } + }; + } + }; + + // ================================================================= + // 5. Card Components + // ================================================================= + const Cards = { + /** + * 이벤트 카드 생성 + */ + createEventCard({ + id, + title, + status, + startDate, + endDate, + participants, + views, + roi, + onClick + } = {}) { + const card = document.createElement('div'); + card.className = 'card event-card'; + + if (onClick) { + card.classList.add('card-clickable'); + card.addEventListener('click', () => onClick(id)); + } + + const statusBadgeClass = status === '진행중' ? 'badge-active' : + status === '예정' ? 'badge-scheduled' : + 'badge-ended'; + + card.innerHTML = ` +
+

${title}

+ ${status} +
+

+ ${Utils.formatDate(startDate)} ~ ${Utils.formatDate(endDate)} +

+
+
+
참여자
+
${Utils.formatNumber(participants)}
+
+
+
조회수
+
${Utils.formatNumber(views)}
+
+
+
ROI
+
${roi}%
+
+
+ `; + + return card; + }, + + /** + * KPI 카드 생성 + */ + createKPICard({ + icon, + iconType = 'primary', + label, + value, + onClick + } = {}) { + const card = document.createElement('div'); + card.className = 'card kpi-card'; + + if (onClick) { + card.classList.add('card-clickable'); + card.addEventListener('click', onClick); + } + + card.innerHTML = ` +
+ ${icon} +
+
+
${label}
+
${value}
+
+ `; + + return card; + }, + + /** + * 옵션 선택 카드 생성 + */ + createOptionCard({ + id, + title, + description, + content, + selected = false, + onSelect + } = {}) { + const card = document.createElement('div'); + card.className = 'card option-card'; + + if (selected) { + card.classList.add('selected'); + } + + card.innerHTML = ` + +

${title}

+ ${description ? `

${description}

` : ''} + ${content || ''} + `; + + card.addEventListener('click', () => { + document.querySelectorAll('.option-card').forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + const radio = card.querySelector('input[type="radio"]'); + radio.checked = true; + + if (onSelect) { + onSelect(id); + } + }); + + return card; + } + }; + + // ================================================================= + // 6. Session Management + // ================================================================= + const Session = { + /** + * 사용자 정보 저장 + */ + saveUser(user) { + return Utils.saveToStorage('kt_event_user', user); + }, + + /** + * 사용자 정보 가져오기 + */ + getUser() { + return Utils.getFromStorage('kt_event_user'); + }, + + /** + * 로그인 여부 확인 + */ + isLoggedIn() { + return this.getUser() !== null; + }, + + /** + * 로그아웃 + */ + logout() { + Utils.removeFromStorage('kt_event_user'); + window.location.href = '01-로그인.html'; + }, + + /** + * 로그인 필요 페이지 보호 + */ + requireAuth() { + if (!this.isLoggedIn()) { + window.location.href = '01-로그인.html'; + } + } + }; + + // ================================================================= + // 7. Mock Data + // ================================================================= + const MockData = { + /** + * 예제 이벤트 데이터 + */ + getEvents() { + return [ + { + id: 'evt001', + title: 'SNS 팔로우 이벤트', + status: '진행중', + purpose: '신규 고객 유치', + startDate: '2025-01-15', + endDate: '2025-02-15', + participants: 142, + views: 856, + roi: 520, + budget: '저비용', + channel: '온라인', + prize: '커피 쿠폰' + }, + { + id: 'evt002', + title: '설 맞이 할인 이벤트', + status: '예정', + purpose: '매출 증대', + startDate: '2025-02-01', + endDate: '2025-02-10', + participants: 0, + views: 0, + roi: 0, + budget: '중비용', + channel: '오프라인', + prize: '10% 할인권' + }, + { + id: 'evt003', + title: '고객 만족도 조사', + status: '종료', + purpose: '고객 유지', + startDate: '2024-12-01', + endDate: '2024-12-31', + participants: 287, + views: 1234, + roi: 340, + budget: '저비용', + channel: '온라인', + prize: '포인트 적립' + } + ]; + }, + + /** + * 예제 사용자 데이터 + */ + getDefaultUser() { + return { + id: 'user001', + name: '홍길동', + email: 'hong@example.com', + phone: '010-1234-5678', + businessName: '홍길동 고깃집', + businessType: '음식점', + joinDate: '2025-01-01' + }; + }, + + /** + * 이벤트 목적 옵션 + */ + getEventPurposes() { + return [ + { + id: 'new_customers', + title: '신규 고객 유치', + description: '새로운 고객을 매장으로 끌어들이고 싶어요', + icon: 'person_add' + }, + { + id: 'sales', + title: '매출 증대', + description: '단기간에 매출을 올리고 싶어요', + icon: 'trending_up' + }, + { + id: 'retention', + title: '고객 유지', + description: '기존 고객이 계속 방문하도록 하고 싶어요', + icon: 'favorite' + }, + { + id: 'awareness', + title: '브랜드 인지도', + description: '우리 가게를 더 많은 사람에게 알리고 싶어요', + icon: 'campaign' + } + ]; + }, + + /** + * AI 추천 이벤트 데이터 + */ + getAIRecommendations() { + return { + trends: { + industry: '음식점업 신년 프로모션 트렌드', + location: '강남구 음식점 할인 이벤트 증가', + season: '설 연휴 특수 대비 고객 유치 전략' + }, + recommendations: [ + // 저비용 + { + budget: '저비용', + type: '온라인', + title: 'SNS 팔로우 이벤트', + prize: '커피 쿠폰', + method: 'SNS 팔로우', + participants: 180, + cost: 250000, + roi: 520 + }, + { + budget: '저비용', + type: '오프라인', + title: '전화번호 등록 이벤트', + prize: '커피 쿠폰', + method: '전화번호 등록', + participants: 150, + cost: 300000, + roi: 450 + }, + // 중비용 + { + budget: '중비용', + type: '온라인', + title: '리뷰 작성 이벤트', + prize: '5천원 상품권', + method: '리뷰 작성', + participants: 250, + cost: 1500000, + roi: 380 + }, + { + budget: '중비용', + type: '오프라인', + title: '방문 도장 적립 이벤트', + prize: '무료 식사권', + method: '방문 도장 적립', + participants: 200, + cost: 1800000, + roi: 320 + }, + // 고비용 + { + budget: '고비용', + type: '온라인', + title: '인플루언서 협업 이벤트', + prize: '1만원 할인권', + method: '인플루언서 팔로우', + participants: 400, + cost: 5000000, + roi: 280 + }, + { + budget: '고비용', + type: '오프라인', + title: 'VIP 고객 초대 이벤트', + prize: '특별 메뉴 제공', + method: 'VIP 초대장', + participants: 300, + cost: 6000000, + roi: 240 + } + ] + }; + } + }; + + // ================================================================= + // Public API + // ================================================================= + return { + Utils, + Navigation, + Form, + Feedback, + Cards, + Session, + MockData + }; +})(); + +// Material Icons 폰트 로드 +if (!document.querySelector('link[href*="material-icons"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; + document.head.appendChild(link); +} + +// Pretendard 폰트 로드 +if (!document.querySelector('link[href*="pretendard"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css'; + document.head.appendChild(link); +} diff --git a/design/uiux/prototype/styles.css b/design/uiux/prototype/styles.css new file mode 100644 index 0000000..0be54e8 --- /dev/null +++ b/design/uiux/prototype/styles.css @@ -0,0 +1,973 @@ +/* ================================================================= + KT AI 이벤트 마케팅 서비스 - 공통 스타일시트 + Version: 1.0.0 + Mobile First Design Philosophy + ================================================================= */ + +/* ================================================================= + 1. CSS Variables - Design System + ================================================================= */ +:root { + /* Brand Colors */ + --color-kt-red: #E31E24; + --color-ai-blue: #0066FF; + + /* Grayscale */ + --color-gray-50: #F9FAFB; + --color-gray-100: #F3F4F6; + --color-gray-200: #E5E7EB; + --color-gray-300: #D1D5DB; + --color-gray-400: #9CA3AF; + --color-gray-500: #6B7280; + --color-gray-600: #4B5563; + --color-gray-700: #374151; + --color-gray-800: #1F2937; + --color-gray-900: #111827; + + /* Semantic Colors */ + --color-success: #10B981; + --color-warning: #F59E0B; + --color-error: #EF4444; + --color-info: #3B82F6; + + /* Background */ + --color-bg-primary: #FFFFFF; + --color-bg-secondary: #F9FAFB; + --color-bg-tertiary: #F3F4F6; + + /* Text */ + --color-text-primary: #111827; + --color-text-secondary: #4B5563; + --color-text-tertiary: #9CA3AF; + --color-text-inverse: #FFFFFF; + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, #E31E24 0%, #B71419 100%); + --gradient-ai: linear-gradient(135deg, #0066FF 0%, #0052CC 100%); + + /* Typography Scale */ + --font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-size-display: 28px; + --font-size-title-large: 24px; + --font-size-title: 20px; + --font-size-headline: 18px; + --font-size-body-large: 16px; + --font-size-body: 14px; + --font-size-body-small: 13px; + --font-size-caption: 12px; + + --line-height-display: 1.2; + --line-height-title: 1.3; + --line-height-body: 1.5; + --line-height-caption: 1.4; + + --font-weight-bold: 700; + --font-weight-semibold: 600; + --font-weight-medium: 500; + --font-weight-regular: 400; + + /* Spacing System (4px grid) */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --spacing-2xl: 48px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* Z-Index */ + --z-dropdown: 1000; + --z-sticky: 1020; + --z-fixed: 1030; + --z-modal-backdrop: 1040; + --z-modal: 1050; + --z-popover: 1060; + --z-tooltip: 1070; + + /* Animation */ + --duration-instant: 0ms; + --duration-fast: 150ms; + --duration-normal: 300ms; + --duration-slow: 500ms; + + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Tablet Responsive Variables */ +@media (min-width: 768px) { + :root { + --font-size-display: 32px; + --font-size-title-large: 28px; + --font-size-title: 24px; + --font-size-headline: 20px; + } +} + +/* Desktop Responsive Variables */ +@media (min-width: 1024px) { + :root { + --font-size-display: 36px; + --font-size-title-large: 32px; + --font-size-title: 28px; + --font-size-headline: 22px; + } +} + +/* ================================================================= + 2. Reset & Base Styles + ================================================================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size-body); + line-height: var(--line-height-body); + color: var(--color-text-primary); + background-color: var(--color-bg-primary); + overflow-x: hidden; +} + +/* ================================================================= + 3. Typography + ================================================================= */ +.text-display { + font-size: var(--font-size-display); + line-height: var(--line-height-display); + font-weight: var(--font-weight-bold); +} + +.text-title-large { + font-size: var(--font-size-title-large); + line-height: var(--line-height-title); + font-weight: var(--font-weight-bold); +} + +.text-title { + font-size: var(--font-size-title); + line-height: var(--line-height-title); + font-weight: var(--font-weight-semibold); +} + +.text-headline { + font-size: var(--font-size-headline); + line-height: var(--line-height-title); + font-weight: var(--font-weight-semibold); +} + +.text-body-large { + font-size: var(--font-size-body-large); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-body { + font-size: var(--font-size-body); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-body-small { + font-size: var(--font-size-body-small); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-caption { + font-size: var(--font-size-caption); + line-height: var(--line-height-caption); + font-weight: var(--font-weight-regular); +} + +.text-primary { color: var(--color-text-primary); } +.text-secondary { color: var(--color-text-secondary); } +.text-tertiary { color: var(--color-text-tertiary); } +.text-inverse { color: var(--color-text-inverse); } +.text-kt-red { color: var(--color-kt-red); } +.text-ai-blue { color: var(--color-ai-blue); } +.text-success { color: var(--color-success); } +.text-warning { color: var(--color-warning); } +.text-error { color: var(--color-error); } + +.text-bold { font-weight: var(--font-weight-bold); } +.text-semibold { font-weight: var(--font-weight-semibold); } +.text-medium { font-weight: var(--font-weight-medium); } + +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +/* ================================================================= + 4. Layout Utilities + ================================================================= */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +.page { + min-height: 100vh; + background-color: var(--color-bg-secondary); + padding-bottom: 76px; /* Bottom nav height + spacing */ +} + +.page-with-header { + padding-top: 56px; /* Header height */ +} + +/* ================================================================= + 5. Button Components + ================================================================= */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + border: none; + border-radius: var(--radius-md); + font-family: var(--font-family); + font-weight: var(--font-weight-semibold); + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + text-decoration: none; + user-select: none; +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* Button Sizes */ +.btn-large { + height: 48px; + padding: 0 var(--spacing-lg); + font-size: var(--font-size-body-large); +} + +.btn-medium { + height: 44px; + padding: 0 var(--spacing-md); + font-size: var(--font-size-body); +} + +.btn-small { + height: 36px; + padding: 0 var(--spacing-md); + font-size: var(--font-size-body-small); +} + +/* Button Variants */ +.btn-primary { + background: var(--gradient-primary); + color: var(--color-text-inverse); + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover:not(:disabled) { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +.btn-secondary { + background-color: var(--color-bg-primary); + color: var(--color-kt-red); + border: 1px solid var(--color-gray-300); +} + +.btn-secondary:hover:not(:disabled) { + background-color: var(--color-gray-50); + border-color: var(--color-kt-red); +} + +.btn-text { + background-color: transparent; + color: var(--color-kt-red); +} + +.btn-text:hover:not(:disabled) { + background-color: rgba(227, 30, 36, 0.08); +} + +.btn-full { + width: 100%; +} + +/* ================================================================= + 6. Card Components + ================================================================= */ +.card { + background-color: var(--color-bg-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-shadow: var(--shadow-sm); + transition: box-shadow var(--duration-fast) var(--ease-out); +} + +.card:hover { + box-shadow: var(--shadow-md); +} + +.card-clickable { + cursor: pointer; +} + +.card-clickable:active { + transform: scale(0.98); +} + +.event-card { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.event-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.event-card-badge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: var(--radius-full); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-medium); +} + +.badge-active { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.badge-scheduled { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-info); +} + +.badge-ended { + background-color: rgba(156, 163, 175, 0.1); + color: var(--color-gray-500); +} + +.event-card-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-md); + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-gray-200); +} + +.stat-item { + text-align: center; +} + +.stat-label { + font-size: var(--font-size-caption); + color: var(--color-text-tertiary); + margin-bottom: 4px; +} + +.stat-value { + font-size: var(--font-size-headline); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +/* KPI Card */ +.kpi-card { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md); +} + +.kpi-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + font-size: 24px; +} + +.kpi-icon-primary { + background: rgba(227, 30, 36, 0.1); + color: var(--color-kt-red); +} + +.kpi-icon-ai { + background: rgba(0, 102, 255, 0.1); + color: var(--color-ai-blue); +} + +.kpi-icon-success { + background: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.kpi-content { + flex: 1; +} + +.kpi-label { + font-size: var(--font-size-body-small); + color: var(--color-text-secondary); + margin-bottom: 4px; +} + +.kpi-value { + font-size: var(--font-size-title); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +/* Option Card for Selection */ +.option-card { + position: relative; + border: 2px solid var(--color-gray-200); + transition: all var(--duration-fast) var(--ease-out); +} + +.option-card:hover { + border-color: var(--color-kt-red); +} + +.option-card.selected { + border-color: var(--color-kt-red); + background-color: rgba(227, 30, 36, 0.02); +} + +.option-card-radio { + position: absolute; + top: var(--spacing-md); + right: var(--spacing-md); +} + +/* ================================================================= + 7. Form Components + ================================================================= */ +.form-group { + margin-bottom: var(--spacing-lg); +} + +.form-label { + display: block; + font-size: var(--font-size-body); + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +.form-label-required::after { + content: " *"; + color: var(--color-error); +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: 12px var(--spacing-md); + border: 1px solid var(--color-gray-300); + border-radius: var(--radius-md); + font-family: var(--font-family); + font-size: var(--font-size-body); + color: var(--color-text-primary); + background-color: var(--color-bg-primary); + transition: all var(--duration-fast) var(--ease-out); +} + +.form-input::placeholder, +.form-textarea::placeholder { + color: var(--color-text-tertiary); +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--color-kt-red); + box-shadow: 0 0 0 3px rgba(227, 30, 36, 0.1); +} + +.form-input:disabled, +.form-select:disabled, +.form-textarea:disabled { + background-color: var(--color-gray-100); + cursor: not-allowed; +} + +.form-textarea { + min-height: 100px; + resize: vertical; +} + +.form-error { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-body-small); + color: var(--color-error); +} + +.form-hint { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-body-small); + color: var(--color-text-tertiary); +} + +/* Checkbox & Radio */ +.form-check { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; +} + +.form-check-input { + width: 20px; + height: 20px; + cursor: pointer; +} + +.form-check-label { + font-size: var(--font-size-body); + color: var(--color-text-primary); + cursor: pointer; +} + +/* ================================================================= + 8. Navigation Components + ================================================================= */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background-color: var(--color-bg-primary); + border-bottom: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--spacing-md); + z-index: var(--z-sticky); +} + +.header-left, +.header-right { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.header-title { + font-size: var(--font-size-headline); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.header-icon-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--color-text-primary); + cursor: pointer; + border-radius: var(--radius-md); + transition: background-color var(--duration-fast) var(--ease-out); +} + +.header-icon-btn:hover { + background-color: var(--color-gray-100); +} + +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 60px; + background-color: var(--color-bg-primary); + border-top: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + justify-content: space-around; + padding: 0 var(--spacing-sm); + z-index: var(--z-sticky); +} + +.bottom-nav-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: var(--spacing-sm); + color: var(--color-text-tertiary); + text-decoration: none; + font-size: var(--font-size-caption); + transition: color var(--duration-fast) var(--ease-out); + cursor: pointer; +} + +.bottom-nav-item:hover { + color: var(--color-text-secondary); +} + +.bottom-nav-item.active { + color: var(--color-kt-red); +} + +.bottom-nav-icon { + font-size: 24px; +} + +/* FAB (Floating Action Button) */ +.fab { + position: fixed; + bottom: 76px; /* Bottom nav height + spacing */ + right: var(--spacing-md); + width: 56px; + height: 56px; + background: var(--gradient-primary); + color: var(--color-text-inverse); + border: none; + border-radius: var(--radius-full); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + z-index: var(--z-fixed); +} + +.fab:hover { + box-shadow: var(--shadow-xl); + transform: scale(1.05); +} + +.fab:active { + transform: scale(0.95); +} + +/* ================================================================= + 9. Feedback Components + ================================================================= */ +/* Toast */ +.toast { + position: fixed; + bottom: 92px; /* Bottom nav + spacing */ + left: 50%; + transform: translateX(-50%); + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--color-gray-900); + color: var(--color-text-inverse); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + font-size: var(--font-size-body); + z-index: var(--z-tooltip); + animation: slideUp var(--duration-normal) var(--ease-out); +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translate(-50%, 20px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } +} + +/* Modal */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: var(--z-modal-backdrop); + animation: fadeIn var(--duration-normal) var(--ease-out); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--color-bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + z-index: var(--z-modal); + max-width: 90%; + max-height: 90vh; + overflow-y: auto; + animation: scaleIn var(--duration-normal) var(--ease-out); +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.modal-header { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--color-gray-200); +} + +.modal-title { + font-size: var(--font-size-title); + font-weight: var(--font-weight-semibold); +} + +.modal-body { + padding: var(--spacing-lg); +} + +.modal-footer { + padding: var(--spacing-lg); + border-top: 1px solid var(--color-gray-200); + display: flex; + gap: var(--spacing-sm); + justify-content: flex-end; +} + +/* Bottom Sheet */ +.bottom-sheet { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--color-bg-primary); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + box-shadow: var(--shadow-xl); + z-index: var(--z-modal); + max-height: 80vh; + overflow-y: auto; + animation: slideUpSheet var(--duration-normal) var(--ease-out); +} + +@keyframes slideUpSheet { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.bottom-sheet-handle { + width: 40px; + height: 4px; + background-color: var(--color-gray-300); + border-radius: var(--radius-full); + margin: var(--spacing-sm) auto; +} + +.bottom-sheet-content { + padding: var(--spacing-lg); +} + +/* Spinner */ +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--color-gray-200); + border-top-color: var(--color-kt-red); + border-radius: var(--radius-full); + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner-center { + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-2xl); +} + +/* Progress Bar */ +.progress { + width: 100%; + height: 8px; + background-color: var(--color-gray-200); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: var(--gradient-primary); + border-radius: var(--radius-full); + transition: width var(--duration-normal) var(--ease-out); +} + +/* ================================================================= + 10. Utility Classes + ================================================================= */ +/* Spacing */ +.m-0 { margin: 0; } +.mt-xs { margin-top: var(--spacing-xs); } +.mt-sm { margin-top: var(--spacing-sm); } +.mt-md { margin-top: var(--spacing-md); } +.mt-lg { margin-top: var(--spacing-lg); } +.mt-xl { margin-top: var(--spacing-xl); } +.mt-2xl { margin-top: var(--spacing-2xl); } + +.mb-xs { margin-bottom: var(--spacing-xs); } +.mb-sm { margin-bottom: var(--spacing-sm); } +.mb-md { margin-bottom: var(--spacing-md); } +.mb-lg { margin-bottom: var(--spacing-lg); } +.mb-xl { margin-bottom: var(--spacing-xl); } +.mb-2xl { margin-bottom: var(--spacing-2xl); } + +.p-0 { padding: 0; } +.p-xs { padding: var(--spacing-xs); } +.p-sm { padding: var(--spacing-sm); } +.p-md { padding: var(--spacing-md); } +.p-lg { padding: var(--spacing-lg); } +.p-xl { padding: var(--spacing-xl); } +.p-2xl { padding: var(--spacing-2xl); } + +.gap-xs { gap: var(--spacing-xs); } +.gap-sm { gap: var(--spacing-sm); } +.gap-md { gap: var(--spacing-md); } +.gap-lg { gap: var(--spacing-lg); } + +/* Flexbox */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-end { justify-content: flex-end; } +.flex-1 { flex: 1; } +.flex-wrap { flex-wrap: wrap; } + +/* Grid */ +.grid { display: grid; } +.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } +.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + +/* Display */ +.hidden { display: none; } +.block { display: block; } +.inline-block { display: inline-block; } + +/* Width */ +.w-full { width: 100%; } +.w-auto { width: auto; } + +/* Background */ +.bg-primary { background-color: var(--color-bg-primary); } +.bg-secondary { background-color: var(--color-bg-secondary); } +.bg-tertiary { background-color: var(--color-bg-tertiary); } + +/* Border */ +.border { border: 1px solid var(--color-gray-200); } +.border-t { border-top: 1px solid var(--color-gray-200); } +.border-b { border-bottom: 1px solid var(--color-gray-200); } +.border-none { border: none; } + +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-full { border-radius: var(--radius-full); } + +/* Shadow */ +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } +.shadow-xl { box-shadow: var(--shadow-xl); } +.shadow-none { box-shadow: none; } + +/* Cursor */ +.cursor-pointer { cursor: pointer; } +.cursor-not-allowed { cursor: not-allowed; } + +/* ================================================================= + 11. Responsive Grid System + ================================================================= */ +@media (min-width: 768px) { + .container { + padding: 0 var(--spacing-lg); + } + + .tablet\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); } + .tablet\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); } + .tablet\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); } +} + +@media (min-width: 1024px) { + .container { + padding: 0 var(--spacing-xl); + } + + .desktop\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); } + .desktop\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); } + .desktop\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + .desktop\:grid-cols-5 { grid-template-columns: repeat(5, 1fr); } +} diff --git a/design/uiux/style-guide.md b/design/uiux/style-guide.md new file mode 100644 index 0000000..b1f46d5 --- /dev/null +++ b/design/uiux/style-guide.md @@ -0,0 +1,1554 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 스타일 가이드 + +## 문서 정보 + +- 작성일: 2025-10-17 +- 버전: 1.0 +- 기반 설계: UI/UX 설계서 v1.0 +- 설계 원칙: Mobile First, 접근성 우선, 일관성 + +--- + +## 1. 브랜드 아이덴티티 + +### 1.1 브랜드 비전 + +**"AI로 간편하게, 성공으로 확실하게"** + +소상공인이 전문 마케터 없이도 AI의 도움으로 효과적인 이벤트를 기획하고 실행할 수 있도록 돕는 혁신적인 서비스 + +### 1.2 디자인 철학 + +#### 혁신성 (Innovation) + +- 최신 AI 기술을 활용한 자동화된 이벤트 생성 +- 복잡한 마케팅 프로세스를 단순화 +- 3분 만에 완성되는 이벤트 콘텐츠 + +#### 신뢰성 (Trust) + +- KT 브랜드의 안정성과 신뢰감 +- 명확한 프로세스와 투명한 결과 제공 +- 실시간 성과 모니터링 + +#### 친근함 (Approachability) + +- 소상공인 눈높이에 맞춘 쉬운 인터페이스 +- 초보자도 이해할 수 있는 명확한 안내 +- 따뜻하고 친근한 톤앤매너 + +### 1.3 브랜드 컬러 + +**Primary Color: KT Red** + +- 정체성, 브랜드 대표 색상 +- 행동 유도(CTA), 강조 요소 +- KT 브랜드 헤리티지 계승 + +**Secondary Color: AI Blue** + +- AI 기능, 기술적 신뢰감 +- 정보 전달, 안내 요소 +- 혁신과 미래지향성 + +--- + +## 2. 디자인 원칙 + +### 2.1 명확성 (Clarity) + +**사용자가 무엇을 해야 하는지 항상 명확해야 합니다** + +- 직관적인 아이콘과 레이블 사용 +- 명확한 시각적 계층 구조 +- 현재 위치와 다음 단계를 항상 표시 + +예시: + +``` +✓ 좋은 예: "새 이벤트 만들기" + 큰 CTA 버튼 +✗ 나쁜 예: "시작하기" (무엇을 시작하는지 불분명) +``` + +### 2.2 효율성 (Efficiency) + +**최소한의 단계로 목표를 달성할 수 있어야 합니다** + +- 불필요한 스텝 제거 +- 자동 완성 및 추천 기능 활용 +- AI가 대부분의 작업 자동화 + +목표: + +- 이벤트 기획: 10초 이내 +- 콘텐츠 생성: 3분 이내 +- 배포 설정: 1분 이내 + +### 2.3 신뢰성 (Trust) + +**AI 처리 과정과 결과를 투명하게 보여줍니다** + +- AI 처리 시간 명시 (예: "AI가 분석중입니다 약 3초 소요") +- 중간 단계 결과 확인 가능 +- 언제든 이전 단계로 돌아갈 수 있음 + +### 2.4 친근함 (Approachability) + +**초보자도 쉽게 사용할 수 있어야 합니다** + +- 전문 용어 최소화 +- 친근한 톤의 안내 문구 +- 도움말과 예시 제공 + +### 2.5 일관성 (Consistency) + +**모든 화면에서 동일한 패턴을 유지합니다** + +- 컴포넌트 재사용 +- 일관된 색상과 타이포그래피 +- 예측 가능한 인터랙션 + +--- + +## 3. 색상 시스템 + +### 3.1 Primary Color (주 색상) + +#### KT Red - 브랜드 정체성 + +``` +Main: #E31E24 // CTA 버튼, 주요 액션, 활성화 상태 +Light: #FF4D52 // 호버 상태, 배경 강조 +Dark: #C71820 // 눌림 상태, 진한 강조 + +사용 예시: +- Primary Button 배경 +- 활성화된 네비게이션 아이템 +- 중요한 알림 배지 +- 링크 텍스트 +``` + +#### 색상 접근성 + +- White 배경 대비: 7.2:1 (WCAG AAA) +- Gray-100 배경 대비: 6.8:1 (WCAG AAA) + +### 3.2 Secondary Color (보조 색상) + +#### AI Blue - 혁신과 신뢰 + +``` +Main: #0066FF // AI 기능 강조, 정보 아이콘 +Light: #4D94FF // AI 로딩 배경, 안내 영역 +Dark: #004DBF // AI 프로세스 완료 + +사용 예시: +- AI 처리 중 상태 표시 +- 정보 제공 영역 +- 트렌드 분석 차트 +- 도움말 아이콘 +``` + +### 3.3 Grayscale (회색조) + +``` +Black (Gray-900): #1A1A1A // 주요 제목, 본문 텍스트 +Gray-700: #4A4A4A // 보조 텍스트, 아이콘 +Gray-500: #9E9E9E // 비활성화 텍스트, 플레이스홀더 +Gray-300: #D9D9D9 // 구분선, 비활성화 테두리 +Gray-100: #F5F5F5 // 배경, 비활성화 버튼 +White (Gray-50): #FFFFFF // 카드 배경, 기본 배경 + +색상 대비 (White 배경 기준): +- Gray-900: 14.2:1 (AAA) +- Gray-700: 8.5:1 (AAA) +- Gray-500: 4.6:1 (AA) +``` + +### 3.4 Semantic Colors (의미 색상) + +``` +Success (성공): #00C853 // 완료, 승인, 성공 메시지 +Warning (경고): #FFA000 // 주의, 대기 중, 확인 필요 +Error (오류): #D32F2F // 오류, 거부, 삭제 확인 +Info (정보): #0288D1 // 안내, 팁, 추가 정보 + +사용 예시: +- Success: "이벤트가 성공적으로 배포되었습니다" +- Warning: "AI 처리가 평소보다 오래 걸리고 있습니다" +- Error: "이미지 생성에 실패했습니다" +- Info: "트렌드 분석 결과를 확인하세요" +``` + +### 3.5 Gradient (그라데이션) + +#### AI Feature Gradient + +``` +Primary Gradient: + background: linear-gradient(135deg, #E31E24 0%, #FF4D52 100%); + 사용: AI 기능 강조 카드, 프리미엄 기능 + +Secondary Gradient: + background: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%); + 사용: AI 처리 중 배경, 정보 강조 영역 +``` + +--- + +## 4. 타이포그래피 시스템 + +### 4.1 Font Family + +**Primary: Pretendard** + +```css +font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Helvetica Neue", system-ui, sans-serif; +``` + +**선택 이유:** + +- 한글 가독성이 뛰어남 +- 다양한 Font Weight 지원 (100~900) +- 모던하고 깔끔한 디자인 +- Variable Font 지원으로 성능 최적화 + +### 4.2 Type Scale (Mobile First) + +``` +Display (메인 타이틀) +- Size: 28px +- Weight: 700 (Bold) +- Line Height: 1.3 (36px) +- Letter Spacing: -0.5px +- 사용: 메인 대시보드 타이틀, 랜딩 화면 + +H1 (화면 제목) +- Size: 24px +- Weight: 700 (Bold) +- Line Height: 1.3 (31px) +- Letter Spacing: -0.3px +- 사용: 각 화면의 메인 제목 + +H2 (섹션 제목) +- Size: 20px +- Weight: 700 (Bold) +- Line Height: 1.4 (28px) +- Letter Spacing: -0.2px +- 사용: 카드 그룹, 섹션 구분 + +H3 (카드 제목) +- Size: 18px +- Weight: 600 (SemiBold) +- Line Height: 1.4 (25px) +- Letter Spacing: 0px +- 사용: 카드 제목, 모달 제목 + +Body-Large (큰 본문) +- Size: 16px +- Weight: 400 (Regular) +- Line Height: 1.5 (24px) +- Letter Spacing: 0px +- 사용: 입력 필드, 중요한 본문 + +Body-Medium (기본 본문) +- Size: 14px +- Weight: 400 (Regular) +- Line Height: 1.5 (21px) +- Letter Spacing: 0px +- 사용: 일반 본문, 설명 텍스트 + +Body-Small (작은 본문) +- Size: 12px +- Weight: 400 (Regular) +- Line Height: 1.5 (18px) +- Letter Spacing: 0px +- 사용: 캡션, 보조 정보, 메타 데이터 + +Button (버튼 레이블) +- Size: 16px +- Weight: 600 (SemiBold) +- Line Height: 1.5 (24px) +- Letter Spacing: 0px +- 사용: 모든 버튼 텍스트 +``` + +### 4.3 Font Weights + +``` +Regular (400): 일반 본문, 설명 +Medium (500): 강조하고 싶은 본문 +SemiBold (600): 버튼, 중요 정보 +Bold (700): 제목, 헤딩 +``` + +### 4.4 Responsive Typography + +**Tablet (768px~)** + +``` +Display: 32px (+4px) +H1: 28px (+4px) +H2: 22px (+2px) +H3: 20px (+2px) +Body-L: 18px (+2px) +Body-M: 16px (+2px) +Body-S: 14px (+2px) +Button: 16px (유지) +``` + +**Desktop (1024px~)** + +``` +Display: 36px (+8px) +H1: 32px (+8px) +H2: 24px (+4px) +H3: 20px (+2px) +Body-L: 18px (+2px) +Body-M: 16px (+2px) +Body-S: 14px (+2px) +Button: 16px (유지) +``` + +--- + +## 5. 간격 시스템 (Spacing) + +### 5.1 Base Unit + +**4px Grid System** - 모든 간격은 4의 배수 + +### 5.2 Spacing Scale + +``` +XS (Extra Small): 4px // 매우 작은 요소 간 (아이콘-텍스트) +S (Small): 8px // 관련 요소 간 (레이블-입력, 버튼 내부) +M (Medium): 16px // 섹션 내 요소 간 (카드 내 항목) +L (Large): 24px // 카드 내부 패딩, 섹션 제목 하단 +XL (Extra Large): 32px // 섹션 간 간격 +2XL (2X Large): 48px // 화면 상하단 여백 +``` + +### 5.3 Component Spacing + +#### Button + +``` +Padding (세로 x 가로): +- Large: 16px x 24px (높이 48px) +- Medium: 12px x 20px (높이 44px) +- Small: 8px x 16px (높이 36px) + +Button 간 간격: 12px (S + XS) +``` + +#### Card + +``` +내부 패딩: 24px (L) +카드 간 간격: 16px (M) +``` + +#### Input Field + +``` +내부 패딩: 16px (M) +레이블-입력 간격: 8px (S) +입력 필드 간 간격: 16px (M) +``` + +#### Screen Margins + +``` +Mobile: 20px (양쪽) +Tablet: 40px (양쪽) +Desktop: 80px (양쪽, 최대 1200px container) +``` + +### 5.4 Touch Target + +**WCAG 2.1 AA 준수** + +``` +최소 터치 영역: 44 x 44px +권장 터치 영역: 48 x 48px + +적용 대상: +- 모든 버튼 +- 탭 항목 +- 체크박스, 라디오 버튼 +- 링크가 있는 카드 +``` + +--- + +## 6. 컴포넌트 스타일 + +### 6.1 Button (버튼) + +#### Primary Button + +``` +배경: #E31E24 (KT Red) +텍스트: #FFFFFF (White) +둥근 모서리: 8px +그림자: 0 2px 4px rgba(227, 30, 36, 0.2) + +상태별: +- Default: 배경 #E31E24 +- Hover: 배경 #FF4D52 (10% 밝게) +- Pressed: 배경 #C71820 (10% 어둡게) +- Disabled: 배경 #D9D9D9, 텍스트 #9E9E9E +``` + +#### Secondary Button + +``` +배경: #FFFFFF (White) +텍스트: #E31E24 (KT Red) +테두리: 2px solid #E31E24 +둥근 모서리: 8px + +상태별: +- Default: 테두리 #E31E24 +- Hover: 배경 #FFF5F5 (5% Red tint) +- Pressed: 배경 #FFEBEB (10% Red tint) +- Disabled: 테두리 #D9D9D9, 텍스트 #9E9E9E +``` + +#### Text Button + +``` +배경: 없음 +텍스트: #E31E24 (KT Red) +둥근 모서리: 8px + +상태별: +- Default: 텍스트 #E31E24 +- Hover: 배경 #FFF5F5 +- Pressed: 배경 #FFEBEB +- Disabled: 텍스트 #9E9E9E +``` + +#### Button Sizes + +``` +Large: +- 높이: 48px +- 패딩: 16px x 24px +- 폰트: Button (16px SemiBold) +- 사용: 주요 CTA + +Medium: +- 높이: 44px +- 패딩: 12px x 20px +- 폰트: Body-M (14px SemiBold) +- 사용: 일반 액션 + +Small: +- 높이: 36px +- 패딩: 8px x 16px +- 폰트: Body-S (12px SemiBold) +- 사용: 보조 액션 +``` + +### 6.2 Card (카드) + +#### Default Card + +``` +배경: #FFFFFF (White) +테두리: 1px solid #E0E0E0 +둥근 모서리: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +내부 패딩: 24px + +상태별: +- Default: 테두리 #E0E0E0 +- Hover: 테두리 #E31E24, 그림자 0 4px 12px rgba(227, 30, 36, 0.12) +- Selected: 테두리 2px solid #E31E24 +``` + +#### Event Card (이벤트 카드) + +``` +배경: #FFFFFF +둥근 모서리: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) + +구조: +┌─────────────────────────┐ +│ [이미지 썸네일 16:9] │ +├─────────────────────────┤ +│ H3 제목 │ +│ Body-S 메타 정보 │ +│ [상태 배지] [CTA 버튼] │ +└─────────────────────────┘ +``` + +#### Selection Card (선택형 카드) + +``` +사용: 옵션 선택 화면 (09-콘텐츠미리보기.html) +특징: 카드 전체가 선택 가능한 인터랙티브 영역 + +배경: #FFFFFF +테두리: 3px solid transparent +둥근 모서리: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +내부 패딩: 24px +커서: pointer + +상태별: +- Default: + - 테두리 transparent + - 그림자 0 2px 8px rgba(0, 0, 0, 0.08) + +- Hover: + - transform: translateY(-2px) + - 그림자 0 8px 16px rgba(0, 0, 0, 0.1) + +- Selected: + - 테두리 3px solid #E31E24 + - 그림자 0 4px 12px rgba(227, 30, 36, 0.2) + - 우측 상단 체크 배지 표시 + +구조: +┌─────────────────────────┐ +│ [✓] │ ← 선택 배지 (조건부) +│ [미리보기 이미지 1:1] │ +│ │ +│ H3 옵션 제목 │ +│ Body-S 설명 │ +│ │ +│ [크게보기] │ +└─────────────────────────┘ + +Selected Badge (체크 배지): +- 위치: 우측 상단 (absolute) +- 크기: 32 x 32px +- 배경: #E31E24 +- 아이콘: check (White, 20px) +- 둥근 모서리: 50% (원형) +- Display: none (기본), flex (선택 시) +- z-index: 10 + +Image Preview: +- 비율: 1:1 (aspect-ratio) +- 배경: #F5F5F5 (플레이스홀더) +- 둥근 모서리: 12px +- object-fit: cover + +Radio Button: +- Display: none (숨김) +- 기능: 유지 (폼 제출용) +- 접근성: 키보드 네비게이션 지원 + +전환 애니메이션: +- Duration: 0.3s +- Easing: ease + +주의사항: +- 카드 내부 버튼 클릭 시 이벤트 버블링 방지 필요 + - event.stopPropagation() 사용 +- 카드 클릭과 보조 액션 버튼 클릭 구분 +- 키보드 접근성: Enter/Space로 선택 가능 +``` + +#### Stat Card (지표 카드) + +``` +배경: Gradient 또는 Solid +둥근 모서리: 16px +내부 패딩: 24px + +구조: +┌─────────────────────────┐ +│ [아이콘] Body-S 레이블 │ +│ Display 수치 │ +│ Body-S 변화율 +32% ↑ │ +└─────────────────────────┘ +``` + +### 6.3 Input Field (입력 필드) + +#### Text Input + +``` +배경: #FFFFFF (White) +테두리: 1px solid #D9D9D9 +둥근 모서리: 8px +높이: 48px +내부 패딩: 16px +폰트: Body-L (16px Regular) + +상태별: +- Default: 테두리 #D9D9D9 +- Focus: 테두리 2px solid #0066FF, 그림자 0 0 0 4px rgba(0, 102, 255, 0.1) +- Error: 테두리 2px solid #D32F2F, 그림자 0 0 0 4px rgba(211, 47, 47, 0.1) +- Disabled: 배경 #F5F5F5, 테두리 #E0E0E0, 텍스트 #9E9E9E +- Filled: 배경 유지, 텍스트 #1A1A1A +``` + +#### Textarea + +``` +배경: #FFFFFF +테두리: 1px solid #D9D9D9 +둥근 모서리: 8px +최소 높이: 120px (약 5줄) +내부 패딩: 16px +폰트: Body-M (14px Regular) +Line Height: 1.5 + +Resize: vertical (세로 방향만) +``` + +#### Editable Field (인라인 편집) + +``` +사용: 08-AI이벤트추천.html (제목, 경품 편집) +특징: contenteditable을 활용한 인라인 편집 + +배경: 투명 (기본), #F5F5F5 (hover), #FFFFFF (focus) +테두리: 1px dashed #D9D9D9 +둥근 모서리: 4px +내부 패딩: 4px 8px +폰트: 문맥에 따름 (제목: H3, 경품: Body) +커서: text + +상태별: +- Default: + - 테두리: 1px dashed #D9D9D9 + - 배경: 투명 + +- Hover: + - 테두리: 1px dashed #E31E24 + - 배경: #F5F5F5 + +- Focus: + - outline: none + - 테두리: 1px solid #E31E24 + - 배경: #FFFFFF + +전환 애니메이션: +- Duration: 200ms (Fast) +- Easing: ease-out + +접근성: +- contenteditable="true" +- role="textbox" +- aria-label="편집 가능한 필드명" + +주의사항: +- 빈 값 방지 (최소 1자 이상) +- Enter 키로 편집 완료 (blur) +- maxLength 속성으로 길이 제한 +``` + +#### Placeholder + +``` +색상: #9E9E9E (Gray-500) +폰트: Body-L (16px Regular) +스타일: italic (선택적) +``` + +### 6.4 Checkbox & Radio + +#### Checkbox + +``` +크기: 24 x 24px (터치 영역 44px) +둥근 모서리: 4px +테두리: 2px solid #D9D9D9 + +상태별: +- Unchecked: 배경 White, 테두리 #D9D9D9 +- Checked: 배경 #E31E24, 체크마크 White +- Disabled: 배경 #F5F5F5, 테두리 #E0E0E0 +``` + +#### Radio Button + +``` +크기: 24 x 24px (터치 영역 44px) +둥근 모서리: 50% (원형) +테두리: 2px solid #D9D9D9 + +상태별: +- Unselected: 배경 White, 테두리 #D9D9D9 +- Selected: 테두리 #E31E24, 내부 점 12px #E31E24 +- Disabled: 배경 #F5F5F5, 테두리 #E0E0E0 +``` + +### 6.5 Budget Navigation (예산 선택 탭) + +``` +사용: 08-AI이벤트추천.html +특징: Sticky 네비게이션으로 예산 섹션 이동 + +배경: #F5F5F5 (배경색) +위치: sticky, top 56px (Header 아래) +z-index: 10 +패딩: 16px 0 + +구조: +┌────────────────────────────────┐ +│ [💰 저비용] [💰💰 중비용] [💰💰💰 고비용] │ +└────────────────────────────────┘ + +버튼: +- 크기: Medium (44px 높이) +- 간격: 8px +- flex-1 (균등 분배) + +상태별: +- Default: + - 배경: White + - 텍스트: #4A4A4A + - 테두리: 1px solid #E0E0E0 + +- Active: + - 배경: #E31E24 + - 텍스트: White + - 테두리: none + - 그림자: 0 2px 4px rgba(227, 30, 36, 0.2) + +- Hover (비활성화 탭): + - 배경: #FFF5F5 + - 테두리: 1px solid #E31E24 + +상호작용: +- 클릭 시: 해당 예산 섹션으로 smooth scroll +- Scroll 시: 현재 보이는 섹션에 맞춰 Active 상태 변경 +``` + +### 6.6 Option Card (이벤트 옵션 카드) + +``` +사용: 08-AI이벤트추천.html +특징: 온라인/오프라인 배지 + Editable Field + Radio 선택 + +배경: #FFFFFF +테두리: 1px solid #E0E0E0 +둥근 모서리: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +내부 패딩: 24px + +구조: +┌─────────────────────────┐ +│ [온라인] 배지 [◯] │ +│ │ +│ 제목 (editable) │ +│ 경품명 (editable) │ +│ │ +│ 참여 방법: ... │ +│ │ +│ 📊 예상 참여자: 200명 │ +│ 💰 예상 비용: 30만원 │ +│ 📈 예상 ROI: 180% │ +└─────────────────────────┘ + +온라인/오프라인 배지: +- 크기: 패딩 4px 12px +- 폰트: Body-S (12px SemiBold) +- 둥근 모서리: 12px (pill) +- 온라인: 배경 #E3F2FD (Blue), 텍스트 #0066FF +- 오프라인: 배경 #FCE4EC (Pink), 텍스트 #E31E24 + +Radio 버튼: +- 위치: 우측 상단 +- 크기: 24 x 24px +- Display: visible (기본 보임) + +Editable Field: +- 스타일: 인라인 편집 (상세 내용은 6.3 참조) +- Hover: 점선 테두리로 편집 가능 표시 +- Focus: 실선 테두리로 변경 + +통계 정보: +- 폰트: Body-S (12px Regular) +- 색상: #4A4A4A (Secondary) +- 아이콘: Material Icons (16px) +``` + +### 6.7 Bottom Navigation + +``` +배경: #FFFFFF (White) +높이: 60px +그림자: 0 -2px 8px rgba(0, 0, 0, 0.08) +아이템 개수: 4개 (홈, 이벤트, 분석, 마이) + +아이템 구조: +┌─────────────┐ +│ [아이콘] │ +│ 레이블 │ +└─────────────┘ + +아이콘: +- 크기: 24 x 24px +- 스타일: Outlined (기본), Filled (활성화) + +레이블: +- 폰트: Body-S (12px Regular) + +색상: +- 비활성화: 아이콘/텍스트 #9E9E9E +- 활성화: 아이콘/텍스트 #E31E24 +- 배경: 활성화 시 투명 + +간격: +- 아이콘-레이블: 4px (XS) +``` + +### 6.8 Stepper (단계 표시) + +#### Progress Stepper (AI 이벤트 생성) + +``` +전체 높이: 48px +배경: #F5F5F5 +진행률 바: #E31E24 +둥근 모서리: 24px + +구조: +┌─────────────────────────────┐ +│ ████████░░░░░░░░ 3/7 단계 │ +└─────────────────────────────┘ + +텍스트: +- 폰트: Body-M (14px SemiBold) +- 색상: #1A1A1A +``` + +#### Step Indicator (단계별 표시) + +``` +┌────┐ ┌────┐ ┌────┐ +│ ✓ │─ │ 2 │─ │ 3 │ +└────┘ └────┘ └────┘ +완료 진행중 대기 + +원 크기: 32px +선 두께: 2px +간격: 8px (선 길이 가변) + +색상: +- 완료: 배경 #00C853, 아이콘 White +- 진행중: 배경 #E31E24, 숫자 White +- 대기: 배경 #F5F5F5, 테두리 #D9D9D9, 숫자 #9E9E9E +``` + +--- + +## 7. 반응형 브레이크포인트 + +### 7.1 Breakpoints + +``` +Mobile (기본): +- Range: 320px ~ 767px +- Container: 100% - 40px (양쪽 20px 마진) +- Columns: 4 columns +- Gutter: 16px +- 레이아웃: 1열 스택 + +Tablet: +- Range: 768px ~ 1023px +- Container: 100% - 80px (양쪽 40px 마진) +- Columns: 8 columns +- Gutter: 24px +- 레이아웃: 2열 그리드 + +Desktop: +- Range: 1024px 이상 +- Container: Max 1200px (중앙 정렬) +- Columns: 12 columns +- Gutter: 32px +- 레이아웃: 3열 그리드 + 사이드바 +``` + +### 7.2 CSS Media Queries + +```css +/* Mobile First - 기본 스타일 */ +.component { + /* 320px ~ 767px */ +} + +/* Tablet */ +@media (min-width: 768px) { + .component { + /* Tablet 스타일 */ + } +} + +/* Desktop */ +@media (min-width: 1024px) { + .component { + /* Desktop 스타일 */ + } +} +``` + +### 7.3 Grid System + +#### Mobile Grid (4 Columns) + +``` +┌─────┬─────┬─────┬─────┐ +│ 1 │ 2 │ 3 │ 4 │ +└─────┴─────┴─────┴─────┘ + +사용 예: +- Full Width: span 4 +- Half Width: span 2 +``` + +#### Tablet Grid (8 Columns) + +``` +┌──┬──┬──┬──┬──┬──┬──┬──┐ +│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│ +└──┴──┴──┴──┴──┴──┴──┴──┘ + +사용 예: +- Full Width: span 8 +- Half Width: span 4 (2개 카드) +- 1/3 Width: span 2-3 (3개 카드) +``` + +#### Desktop Grid (12 Columns) + +``` +┌┬┬┬┬┬┬┬┬┬┬┬┬┐ +│││││││││││││ +└┴┴┴┴┴┴┴┴┴┴┴┴┘ + +사용 예: +- Main Content: span 8-9 (왼쪽) +- Sidebar: span 3-4 (오른쪽) +- 3 Column Cards: span 4 each +``` + +--- + +## 8. 서비스 특화 컴포넌트 + +### 8.1 AI 처리 상태 컴포넌트 + +#### Loading Skeleton + +``` +배경: #F5F5F5 +애니메이션: Shimmer (좌→우 반짝임) +Duration: 1.5s infinite + +┌─────────────────────────┐ +│ ▓▓▓▓░░░░░░░░░░░░░░░░░░ │ ← 제목 +│ ░░░░░░░░░░░░░░░░░░░░░░ │ ← 본문 +│ ░░░░░░░░░░░░░░░░░░░░░░ │ +│ ▓▓▓░░░ ▓▓▓░░░ ▓▓▓░░ │ ← 버튼들 +└─────────────────────────┘ + +사용: AI 이미지 생성, 트렌드 분석 등 +``` + +#### Progress Indicator (AI 단계별) + +``` +┌─────────────────────────────┐ +│ 🤖 AI가 트렌드를 분석중입니다 │ +│ │ +│ ████████░░░░░░░░░░░░ 35% │ +│ │ +│ 예상 시간: 약 2초 남음 │ +└─────────────────────────────┘ + +배경: White +테두리: 1px solid #E0E0E0 +진행바: #0066FF (AI Blue) +텍스트: Body-M #4A4A4A +``` + +#### Spinner (간단한 로딩) + +``` +크기: 32px +두께: 3px +색상: #E31E24 (Primary) 또는 #0066FF (AI Blue) +애니메이션: rotate 360deg, 0.8s linear infinite + +사용: 버튼 내부, 작은 액션 로딩 +``` + +### 8.2 AI 결과 카드 + +#### AI 생성 옵션 카드 (선택형) + +``` +┌─────────────────────────────┐ +│ ◯ [이미지 미리보기] │ +│ │ +│ H3 옵션 제목 │ +│ Body-S 설명 │ +│ [재생성 버튼] │ +└─────────────────────────────┘ + +상태별: +- 비선택: 테두리 1px #E0E0E0 +- 선택: 테두리 2px #E31E24, 배경 #FFF5F5 +- 호버: 그림자 0 4px 12px rgba(0,0,0,0.1) + +라디오 버튼: +- 위치: 좌상단 또는 우상단 +- 크기: 24px +``` + +#### AI 추천 배지 + +``` +배경: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%) +텍스트: White +폰트: Body-S (12px SemiBold) +둥근 모서리: 12px (pill 형태) +패딩: 4px 12px +위치: 카드 좌상단 절대 배치 + +텍스트: "AI 추천" 또는 "인기" +``` + +### 8.3 Real-time Dashboard Components + +#### KPI Card (핵심 지표) + +``` +┌─────────────────────────┐ +│ 📊 참여자 수 │ +│ │ +│ 152명 │ +│ +32% ↑ 전주 대비 │ +└─────────────────────────┘ + +배경: White +둥근 모서리: 16px +그림자: 0 2px 8px rgba(0,0,0,0.08) +패딩: 24px + +구조: +- 아이콘 + 레이블: Body-S #4A4A4A +- 수치: Display (28px Bold) #1A1A1A +- 변화율: Body-S, 증가 #00C853, 감소 #D32F2F +``` + +#### Chart Container + +``` +배경: White +둥근 모서리: 12px +그림자: 0 2px 8px rgba(0,0,0,0.08) +패딩: 24px + +차트 색상: +- Primary Line: #E31E24 +- Secondary Line: #0066FF +- Grid: #F5F5F5 +- Axis Label: #9E9E9E +``` + +--- + +## 9. 인터랙션 패턴 + +### 9.1 Bottom Sheet + +``` +배경: White +둥근 모서리: 24px 24px 0 0 (상단만) +최대 높이: 80vh +Handle: 40px x 4px, #D9D9D9, 상단 중앙 +그림자: 0 -4px 12px rgba(0,0,0,0.15) + +애니메이션: +- Open: transform translateY(100% → 0), 300ms ease-out +- Close: transform translateY(0 → 100%), 250ms ease-in + +Backdrop: +- 배경: rgba(0,0,0,0.5) +- 클릭 시: Bottom Sheet 닫힘 +``` + +### 9.2 Toast (알림) + +``` +┌─────────────────────────────┐ +│ ✓ 이벤트가 성공적으로 저장되었습니다 │ +└─────────────────────────────┘ + +배경: #1A1A1A (90% opacity) +텍스트: White +둥근 모서리: 8px +패딩: 16px 24px +위치: 하단 중앙, Bottom Navigation 위 80px +자동 닫힘: 3초 + +애니메이션: +- Show: opacity 0→1, translateY(20px→0), 200ms +- Hide: opacity 1→0, 200ms + +타입별 아이콘: +- Success: ✓ (#00C853) +- Error: ✕ (#D32F2F) +- Info: ⓘ (#0288D1) +``` + +### 9.3 Modal (모달 다이얼로그) + +``` +┌─────────────────────────────┐ +│ H2 제목 [✕] │ +├─────────────────────────────┤ +│ │ +│ Body-M 본문 내용 │ +│ │ +├─────────────────────────────┤ +│ [취소 버튼] [확인 버튼] │ +└─────────────────────────────┘ + +배경: White +둥근 모서리: 16px +최대 너비: 400px (Mobile), 480px (Tablet+) +패딩: 24px +그림자: 0 8px 24px rgba(0,0,0,0.2) + +Backdrop: +- 배경: rgba(0,0,0,0.6) +- 클릭 시: 모달 닫힘 (확인 필요한 경우 제외) + +애니메이션: +- Show: opacity 0→1, scale(0.95→1), 250ms ease-out +- Hide: opacity 1→0, scale(1→0.95), 200ms ease-in +``` + +### 9.4 Pull to Refresh + +``` +상태 표시: +1. Pull Down (당기기): + - 아이콘: ↓ 회전 + - 텍스트: "당겨서 새로고침" + +2. Release to Refresh (놓아서 새로고침): + - 아이콘: ↻ 회전 + - 텍스트: "놓아서 새로고침" + +3. Refreshing (새로고침 중): + - Spinner 표시 + - 텍스트: "새로고침 중..." + +색상: +- 아이콘/텍스트: #9E9E9E +- Spinner: #E31E24 + +위치: 화면 최상단 +높이: 60px +``` + +### 9.5 Swipe Actions (카드 스와이프) + +``` +좌측 스와이프 (삭제): +┌─────────────────────────────┐ +│ ◀ 삭제 │ +└─────────────────────────────┘ +배경: #D32F2F +아이콘: 🗑️ White +텍스트: White Body-M + +우측 스와이프 (편집): +┌─────────────────────────────┐ +│ 편집 ▶ │ +└─────────────────────────────┘ +배경: #0066FF +아이콘: ✏️ White +텍스트: White Body-M + +Threshold: 30% 너비 이상 스와이프 시 액션 트리거 +``` + +### 9.6 Drag and Drop + +``` +드래그 중 상태: +- 원본 카드: opacity 0.5 +- 드래그 카드: 그림자 0 8px 16px rgba(0,0,0,0.2), scale(1.05) +- Drop Zone: 배경 #F5F5F5, 테두리 2px dashed #E31E24 + +드롭 가능 영역: +- 배경: #FFF5F5 (light red tint) +- 테두리: 2px dashed #E31E24 +``` + +--- + +## 10. 애니메이션 가이드라인 + +### 10.1 Duration (지속 시간) + +``` +즉시 (Instant): 0ms // 색상 변화 +매우 빠름 (Very Fast): 100ms // Hover 효과 +빠름 (Fast): 200ms // Toast 등장 +일반 (Normal): 300ms // Modal, Bottom Sheet +느림 (Slow): 500ms // Page Transition +``` + +### 10.2 Easing (가속도) + +``` +Ease-Out (감속): +- cubic-bezier(0, 0, 0.2, 1) +- 사용: 요소 등장 (Modal, Sheet, Toast) + +Ease-In (가속): +- cubic-bezier(0.4, 0, 1, 1) +- 사용: 요소 퇴장 + +Ease-In-Out (가속+감속): +- cubic-bezier(0.4, 0, 0.2, 1) +- 사용: 전환 효과 (Tab 전환) + +Linear (일정): +- linear +- 사용: 무한 회전 (Spinner, Loading) +``` + +### 10.3 주요 애니메이션 + +#### Page Transition + +``` +진입: +- opacity: 0 → 1 +- transform: translateX(20px) → translateX(0) +- duration: 300ms +- easing: ease-out + +퇴장: +- opacity: 1 → 0 +- transform: translateX(0) → translateX(-20px) +- duration: 250ms +- easing: ease-in +``` + +#### Card Hover + +``` +transform: translateY(0) → translateY(-4px) +box-shadow: 증가 (0 2px 8px → 0 8px 16px) +duration: 200ms +easing: ease-out +``` + +#### Button Press + +``` +transform: scale(1) → scale(0.95) +duration: 100ms +easing: ease-out +``` + +--- + +## 11. 아이콘 시스템 + +### 11.1 Icon Style + +**Material Icons Outlined** (기본) +**Material Icons Filled** (활성화 상태) + +``` +스타일 특징: +- Outlined: 선 두께 2px, 심플하고 깔끔 +- Filled: 활성화 시 직관적 구분 +``` + +### 11.2 Icon Sizes + +``` +Small: 16 x 16px // 텍스트 내 인라인 +Medium: 24 x 24px // 버튼, 네비게이션 (기본) +Large: 32 x 32px // 헤더 액션 +XLarge: 48 x 48px // Empty State 일러스트 +``` + +### 11.3 Icon Colors + +``` +Default: #4A4A4A (Gray-700) +Active: #E31E24 (Primary Red) +Disabled: #9E9E9E (Gray-500) +On Color: #FFFFFF (White) - 색상 버튼 위 + +AI Feature: #0066FF (AI Blue) +Success: #00C853 +Warning: #FFA000 +Error: #D32F2F +``` + +### 11.4 주요 아이콘 매핑 + +``` +홈: home +이벤트: event / campaign +분석: analytics / bar_chart +마이: person / account_circle +추가: add / add_circle +알림: notifications +설정: settings +검색: search +필터: filter_list +정렬: sort +공유: share +다운로드: download +업로드: upload +편집: edit +삭제: delete +닫기: close +뒤로: arrow_back +앞으로: arrow_forward +체크: check / check_circle +오류: error / cancel +정보: info +경고: warning +도움말: help / help_outline + +AI 관련: +트렌드: trending_up +경품: card_giftcard +이미지: image / photo +영상: videocam +SNS: share / language +QR: qr_code +달력: calendar_today +시간: schedule +위치: place +참여자: group / people +``` + +--- + +## 12. 접근성 가이드라인 + +### 12.1 색상 대비 (WCAG 2.1 AA) + +``` +텍스트: +- 일반 텍스트 (14px+): 4.5:1 이상 +- 큰 텍스트 (18px+ or 14px+ Bold): 3:1 이상 + +UI 요소: +- 버튼, 입력 필드, 아이콘: 3:1 이상 + +검증된 조합: +✓ #1A1A1A (Black) on #FFFFFF (White): 14.2:1 +✓ #4A4A4A (Gray-700) on #FFFFFF: 8.5:1 +✓ #E31E24 (KT Red) on #FFFFFF: 7.2:1 +✓ #0066FF (AI Blue) on #FFFFFF: 7.8:1 +✓ #FFFFFF (White) on #E31E24 (Red): 7.2:1 +✗ #9E9E9E (Gray-500) on #FFFFFF: 4.6:1 (작은 텍스트 부적합) +``` + +### 12.2 키보드 네비게이션 + +``` +Tab Order: +- 논리적 순서 (상→하, 좌→우) +- 모든 인터랙티브 요소 접근 가능 + +Focus Indicator: +- 테두리: 2px solid #0066FF +- Offset: 2px +- 둥근 모서리: 버튼과 동일 + +Focus Trap: +- Modal, Bottom Sheet 열림 시 내부만 탭 이동 +- ESC 키로 닫기 가능 +``` + +### 12.3 스크린 리더 + +``` +ARIA Labels: +- 모든 버튼에 명확한 레이블 +- 아이콘 버튼: aria-label="설정 열기" +- 이미지: alt="AI가 생성한 홍보 이미지" + +Role & State: +- role="button", role="dialog" +- aria-expanded="true/false" +- aria-selected="true/false" +- aria-disabled="true/false" + +Live Regions: +- Toast: aria-live="polite" +- Error: aria-live="assertive" +``` + +### 12.4 터치 영역 + +``` +최소 크기: 44 x 44px (WCAG 2.1 AA) +권장 크기: 48 x 48px + +충분한 간격: +- 인접 터치 요소 간: 8px 이상 +``` + +### 12.5 대안 제공 + +``` +색상만으로 정보 전달 금지: +✗ 나쁜 예: 빨간색만으로 오류 표시 +✓ 좋은 예: 빨간색 + ✕ 아이콘 + "오류" 텍스트 + +드래그 앤 드롭 대안: +- 버튼 클릭으로도 순서 변경 가능 +- 키보드로 순서 조정 가능 +``` + +--- + +## 13. 성능 최적화 + +### 13.1 이미지 최적화 + +``` +포맷: +- 사진: WebP (fallback: JPG) +- 일러스트: SVG 또는 PNG +- 아이콘: SVG Sprite + +압축: +- JPG: Quality 80-85% +- PNG: TinyPNG 또는 ImageOptim + +Lazy Loading: +- 스크롤 시 로드 (Intersection Observer) +- 중요 이미지: eager loading +``` + +### 13.2 폰트 최적화 + +``` +Font Loading: +- font-display: swap +- Preload 주요 폰트 + +Subsetting: +- 한글: 자주 쓰는 글자만 (Subset) +- Variable Font 사용 (Pretendard Variable) + +WOFF2 우선: +@font-face { + font-family: 'Pretendard'; + src: url('/fonts/Pretendard-Variable.woff2') format('woff2-variations'); + font-display: swap; +} +``` + +### 13.3 애니메이션 성능 + +``` +GPU 가속 사용: +- transform: translate3d(), scale3d() +- opacity +- filter + +피해야 할 속성: +- width, height (Reflow 발생) +- top, left (Reflow 발생) +- background-position + +Will-Change 사용: +will-change: transform, opacity; +(애니메이션 직전에만 적용) +``` + +--- + +## 14. 다크 모드 (향후 지원) + +### 14.1 색상 매핑 (참고용) + +``` +Light Mode → Dark Mode + +배경: +#FFFFFF → #121212 +#F5F5F5 → #1E1E1E + +텍스트: +#1A1A1A → #E0E0E0 +#4A4A4A → #B0B0B0 +#9E9E9E → #707070 + +카드: +#FFFFFF → #1E1E1E +border #E0E0E0 → #2C2C2C + +Primary (유지): +#E31E24 → #E31E24 (동일) + +Secondary: +#0066FF → #4D94FF (밝게 조정) +``` + +--- + +## 15. 변경 이력 + +### Version 1.1 (2025-10-21) + +- 프로토타입 기반 컴포넌트 업데이트 +- Editable Field 컴포넌트 추가 (인라인 편집) +- Budget Navigation 컴포넌트 추가 (Sticky 탭 네비게이션) +- Option Card 컴포넌트 추가 (온라인/오프라인 배지) +- Selection Card 세부사항 보완 (이미지 비율, z-index) +- 이벤트 버블링 방지 가이드 추가 + +### Version 1.0 (2025-10-17) + +- 초안 작성 +- 브랜드 아이덴티티 정의 +- 디자인 원칙 5가지 수립 +- 색상 시스템 (Primary/Secondary/Grayscale/Semantic) 정의 +- 타이포그래피 시스템 (Pretendard, 8단계 스케일) 정의 +- 간격 시스템 (4px 기반, 6단계) 정의 +- 컴포넌트 스타일 (Button/Card/Input/Navigation) 정의 +- 반응형 브레이크포인트 (Mobile/Tablet/Desktop) 정의 +- AI 특화 컴포넌트 (로딩/결과/Stepper) 정의 +- 인터랙션 패턴 (BottomSheet/Toast/Modal/Swipe) 정의 +- 애니메이션 가이드라인 정의 +- 아이콘 시스템 (Material Icons) 정의 +- 접근성 가이드라인 (WCAG 2.1 AA) 정의 +- 성능 최적화 가이드 정의 + +--- + +## 16. 참고 자료 + +- UI/UX 설계서: design/uiux/uiux.md +- 유저스토리: design/userstory.md +- KT 사장님Easy: https://product.kt.com/wDic/soho/marketing.do?itemCode=sajangeasy +- wwit.design 닷슬래시대시: https://wwit.design/2023/09/30/dotslashdash/ +- Material Design Icons: https://fonts.google.com/icons +- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/ +- Pretendard Font: https://github.com/orioncactus/pretendard + +--- + +**문서 끝** diff --git a/design/uiux/uiux-design.md b/design/uiux/uiux-design.md new file mode 100644 index 0000000..06951dd --- /dev/null +++ b/design/uiux/uiux-design.md @@ -0,0 +1,1002 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - UI/UX 설계서 + +## 문서 정보 + +- 작성일: 2025-10-21 +- 버전: 2.0 +- 기반 문서: 유저스토리 v2.0, 프로토타입 분석 +- 설계 원칙: Mobile First, 접근성 우선, 일관성 + +--- + +## 1. 서비스 개요 + +### 1.1 서비스 목적 + +소상공인이 AI의 도움으로 전문 마케터 없이도 효과적인 이벤트를 기획하고 실행할 수 있도록 지원 + +### 1.2 핵심 가치 + +- **혁신성**: AI 기반 자동화로 3분 만에 이벤트 콘텐츠 생성 +- **신뢰성**: KT 브랜드 기반의 안정적인 서비스 +- **접근성**: 초보자도 쉽게 사용할 수 있는 직관적 인터페이스 + +### 1.3 타겟 사용자 + +- **Primary**: 마케팅 전문 지식이 부족한 소상공인 +- **Secondary**: 이벤트 마케팅 경험이 있는 중소기업 담당자 + +--- + +## 2. 화면 구성 + +### 2.1 전체 화면 구조 (총 17개) + +#### 인증 영역 (01~04) + +- **01-로그인.html**: 이메일/비밀번호 로그인 +- **02-회원가입.html**: 신규 사용자 등록 +- **03-프로필.html**: 사용자 프로필 및 매장 정보 관리 +- **04-로그아웃확인.html**: 로그아웃 확인 모달 + +#### 대시보드 영역 (05~06) + +- **05-대시보드.html**: 메인 홈 화면 + - KPI 요약 (진행 중 이벤트, 총 참여자, 평균 ROI) + - 빠른 시작 (새 이벤트, 분석) + - 진행 중인 이벤트 목록 + - 최근 활동 타임라인 +- **06-이벤트목록.html**: 전체 이벤트 목록 및 필터링 + +#### 이벤트 생성 플로우 (07~12, 7단계) + +- **07-이벤트목적선택.html**: 이벤트 목적 선택 +- **08-AI이벤트추천.html**: AI 트렌드 분석 및 이벤트 추천 +- **09-콘텐츠미리보기.html**: SNS 이미지 스타일 선택 +- **10-콘텐츠편집.html**: 텍스트 및 색상 편집 +- **11-배포채널선택.html**: 배포 채널 선택 +- **12-최종승인.html**: 이벤트 최종 검토 및 승인 + +#### 이벤트 관리 및 모니터링 (13~17) + +- **13-이벤트상세.html**: 이벤트 상세 정보 및 실시간 KPI +- **14-참여자목록.html**: 참여자 관리 및 필터링 +- **15-이벤트참여.html**: 사용자 이벤트 참여 화면 (고객용) +- **16-당첨자추첨.html**: 당첨자 추첨 및 관리 +- **17-성과분석.html**: 실시간 대시보드 및 성과 분석 + +### 2.2 화면 흐름도 + +``` +[01-로그인 / 02-회원가입] + ↓ +[05-대시보드] (Bottom Nav: 홈) + ↓ + ├─ [06-이벤트목록] (Bottom Nav: 이벤트) + │ ↓ + │ [13-이벤트상세] → [14-참여자목록] → [16-당첨자추첨] + │ ↓ + │ [17-성과분석] (Bottom Nav: 분석) + │ + └─ [새 이벤트 생성 플로우] (FAB) + ↓ + [07-목적선택] + ↓ + [08-AI추천] + ↓ + [09-콘텐츠미리보기] + ↓ + [10-콘텐츠편집] + ↓ + [11-배포채널선택] + ↓ + [12-최종승인] + ↓ + [13-이벤트상세] + +[03-프로필] (Bottom Nav: 프로필) + ↓ +[04-로그아웃확인] +``` + +--- + +## 3. 공통 컴포넌트 + +### 3.1 Navigation + +#### Bottom Navigation (모든 메인 화면) + +``` +구조: 4개 탭 +- 홈: 05-대시보드.html +- 이벤트: 06-이벤트목록.html +- 분석: 17-성과분석.html +- 프로필: 03-프로필.html + +디자인: +- 높이: 60px +- 배경: White +- 그림자: 0 -2px 8px rgba(0, 0, 0, 0.08) +- 아이콘: Material Icons (24px) +- 활성화: KT Red (#E31E24) +- 비활성화: Gray (#9E9E9E) +``` + +#### Header + +``` +구조: 타이틀 + 뒤로가기 + 프로필 +- 대시보드: 뒤로가기 없음 +- 이벤트 생성 플로우: 프로필 버튼 숨김 + +디자인: +- 높이: 56px +- 배경: White +- 제목: H1 (24px Bold) +- 아이콘: Material Icons (24px) +``` + +#### FAB (Floating Action Button) + +``` +위치: 우측 하단 고정 +기능: 새 이벤트 생성 (07-이벤트목적선택.html) + +디자인: +- 크기: 56 x 56px +- 배경: KT Red (#E31E24) +- 아이콘: add (White, 24px) +- 그림자: 0 4px 12px rgba(227, 30, 36, 0.3) +- Bottom Navigation 위 80px +``` + +### 3.2 Card 컴포넌트 + +#### Default Card + +```css +배경: #FFFFFF +테두리: 1px solid #E0E0E0 +둥근 모서리: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +패딩: 24px +``` + +#### Event Card + +``` +구조: +- 이벤트명 (H3) +- 상태 배지 (진행중/예정/종료) +- 기간 정보 (Body-S) +- 통계 정보 (참여자, ROI 등) + +상태: +- Hover: 테두리 #E31E24, 그림자 증가 +- Selected: 테두리 2px solid #E31E24 +``` + +#### Selection Card (09-콘텐츠미리보기) + +``` +특징: 카드 전체가 선택 가능한 영역 +- Radio 버튼 숨김 (기능은 유지) +- 선택 시 시각적 피드백: + - 테두리: 3px solid #E31E24 + - 그림자: 0 4px 12px rgba(227, 30, 36, 0.2) + - 우측 상단 체크 배지 표시 +- Hover: transform translateY(-2px) +``` + +### 3.3 Form 컴포넌트 + +#### Text Input + +```css +높이: 48px +패딩: 16px +테두리: 1px solid #D9D9D9 +둥근 모서리: 8px +폰트: Body-L (16px Regular) + +Focus: +- 테두리: 2px solid #0066FF +- 그림자: 0 0 0 4px rgba(0, 102, 255, 0.1) + +Error: +- 테두리: 2px solid #D32F2F +- 그림자: 0 0 0 4px rgba(211, 47, 47, 0.1) +``` + +#### Checkbox & Radio + +``` +크기: 24 x 24px (터치 영역 44px) + +Checkbox: +- 둥근 모서리: 4px +- Checked: 배경 #E31E24, 체크마크 White + +Radio: +- 둥근 모서리: 50% (원형) +- Selected: 테두리 #E31E24, 내부 점 12px +``` + +### 3.4 Modal 컴포넌트 + +#### Modal Dialog + +``` +최대 너비: 400px (Mobile), 480px (Tablet+) +패딩: 24px +둥근 모서리: 16px +그림자: 0 8px 24px rgba(0, 0, 0, 0.2) + +Backdrop: rgba(0, 0, 0, 0.6) + +애니메이션: +- Show: opacity 0→1, scale 0.95→1, 250ms +- Hide: opacity 1→0, scale 1→0.95, 200ms +``` + +#### Bottom Sheet + +``` +최대 높이: 80vh +둥근 모서리: 24px 24px 0 0 +Handle: 40px x 4px, #D9D9D9 (상단 중앙) + +애니메이션: +- Open: translateY(100% → 0), 300ms +- Close: translateY(0 → 100%), 250ms +``` + +#### Toast + +``` +위치: 하단 중앙, Bottom Nav 위 80px +배경: #1A1A1A (90% opacity) +텍스트: White +패딩: 16px 24px +둥근 모서리: 8px +자동 닫힘: 3초 + +타입별 아이콘: +- Success: ✓ (#00C853) +- Error: ✕ (#D32F2F) +- Info: ⓘ (#0288D1) +``` + +--- + +## 4. 화면별 상세 설계 + +### 4.1 인증 영역 + +#### 01-로그인.html + +``` +레이아웃: +- 로고 (상단 중앙) +- 로그인 폼 (중앙 정렬) +- 회원가입/비밀번호찾기 링크 (하단) + +컴포넌트: +- Email Input +- Password Input +- "로그인 유지" Checkbox +- Primary Button (로그인) +- Text Button (회원가입, 비밀번호 찾기) + +유저스토리: UFR-USER-020 +``` + +#### 02-회원가입.html + +``` +레이아웃: 세로 스크롤 +섹션: +1. 기본 정보 + - 이름, 전화번호, 이메일, 비밀번호 +2. 매장 정보 + - 매장명, 업종, 주소, 영업시간 +3. 사업자번호 (검증 필요) + +컴포넌트: +- Text Input (각 필드) +- Select Dropdown (업종) +- Primary Button (회원가입) +- 검증 에러 메시지 + +유저스토리: UFR-USER-010 +``` + +#### 03-프로필.html + +``` +레이아웃: +- 프로필 헤더 (상단) +- 편집 폼 (세로 스크롤) +- 저장/로그아웃 버튼 (하단 고정) + +섹션: +1. 사용자 정보 (아이콘 + 이름/이메일) +2. 기본 정보 편집 +3. 매장 정보 편집 +4. 비밀번호 변경 +5. 로그아웃 버튼 + +컴포넌트: +- Text Input (각 필드) +- Primary Button (저장) +- Text Button (로그아웃, Error 색상) + +유저스토리: UFR-USER-030 +Bottom Nav: 프로필 탭 활성화 +``` + +#### 04-로그아웃확인.html + +``` +유형: Modal Dialog + +구조: +- 제목: "로그아웃 하시겠습니까?" +- 본문: 안내 메시지 +- 버튼: 취소 (Secondary) / 확인 (Primary) + +유저스토리: UFR-USER-040 +``` + +### 4.2 대시보드 영역 + +#### 05-대시보드.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. KPI 요약 (그리드) + - 진행 중 이벤트 수 + - 총 참여자 수 + - 평균 ROI + +2. 빠른 시작 (2개 카드) + - 새 이벤트 만들기 + - 분석 보기 + +3. 진행 중인 이벤트 (카드 리스트) + - 최대 5개 표시 + - "전체보기" 링크 + +4. 최근 활동 (타임라인) + +컴포넌트: +- KPI Card (3개, 그리드) +- Action Card (2개, 그리드) +- Event Card (리스트) +- Timeline Item (리스트) +- FAB (새 이벤트) + +유저스토리: UFR-EVENT-010 +Bottom Nav: 홈 탭 활성화 +``` + +#### 06-이벤트목록.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 검색 바 +2. 필터 (상태, 기간) +3. 정렬 (최신순, 참여자순, ROI순) +4. 이벤트 카드 리스트 +5. 페이지네이션 + +컴포넌트: +- Search Input +- Filter Chips +- Dropdown (정렬) +- Event Card (리스트) +- Pagination + +유저스토리: UFR-EVENT-070 +Bottom Nav: 이벤트 탭 활성화 +``` + +### 4.3 이벤트 생성 플로우 + +#### 07-이벤트목적선택.html + +``` +레이아웃: 세로 스크롤 + +섹션: +- 제목: "이벤트 목적을 선택하세요" +- 옵션 카드 (4개) + 1. 신규 고객 유치 + 2. 재방문 유도 + 3. 매출 증대 + 4. 인지도 향상 + +컴포넌트: +- Option Card (4개, Radio 버튼 포함) + - 아이콘 + 제목 + 설명 +- Primary Button (다음) + +상호작용: +- 카드 선택 시 강조 표시 +- 선택 후 다음 버튼 활성화 + +유저스토리: UFR-EVENT-020 +Progress: 1/7 단계 +``` + +#### 08-AI이벤트추천.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. AI 트렌드 분석 결과 (상단 카드) + - 업종 트렌드 (store 아이콘) + - 지역 트렌드 (location_on 아이콘) + - 시즌 트렌드 (wb_sunny 아이콘) + +2. 예산별 추천 이벤트 제목 및 설명 + - "예산별 추천 이벤트" + - "각 예산별 2가지 방식 (온라인 1개, 오프라인 1개)을 추천합니다" + +3. 예산 선택 네비게이션 (Sticky) + - 💰 저비용 / 💰💰 중비용 / 💰💰💰 고비용 + - 탭 형태, 클릭 시 해당 섹션으로 스크롤 + +4. 예산별 추천 이벤트 (각 예산당 2개 카드) + - 저비용 (25~30만원): 온라인/오프라인 각 1개 + - 중비용 (60~70만원): 온라인/오프라인 각 1개 + - 고비용 (200~250만원): 온라인/오프라인 각 1개 + - 각 카드 구성: + - 온라인/오프라인 배지 + - 이벤트 제목 (인라인 편집 가능, editable-field) + - 경품명 (인라인 편집 가능, editable-field) + - 참여 방법 + - 예상 통계 (참여자 수, 비용, ROI) + - Radio 버튼 (카드 내부) + +컴포넌트: +- Trend Analysis Card (3개 트렌드 항목) +- Budget Navigation (Sticky, 3개 버튼) +- Option Card (총 6개, Radio 선택) + - 온라인/오프라인 배지 + - Editable Field (제목, 경품) + - Radio 버튼 (visible) +- Primary Button (다음) + +상호작용: +- 예산 네비게이션 클릭: 해당 섹션으로 smooth scroll +- Editable Field 클릭: 인라인 편집 활성화 +- 제목/경품 hover: 편집 가능 표시 (점선 테두리) +- Radio 선택: 1개만 선택 가능 +- 선택 후 다음 버튼 활성화 + +유저스토리: UFR-EVENT-030, UFR-AI-010 +Progress: 2/7 단계 + +특징: +- 예산별 2가지 방식 제공 (온라인/오프라인) +- 총 6개 옵션 중 1개 선택 +- 인라인 편집으로 즉시 커스터마이징 +``` + +#### 09-콘텐츠미리보기.html + +``` +레이아웃: 세로 스크롤 + +섹션: +- 제목: "SNS 이미지 스타일을 선택하세요" +- 스타일 카드 (5개 그리드) + 1. 심플 - 깔끔하고 읽기 쉬운 디자인 + 2. 모던 - 트렌디하고 세련된 디자인 + 3. 귀여움 - 친근하고 따뜻한 디자인 + 4. 고급스러움 - 프리미엄 느낌의 디자인 + 5. 트렌디 - MZ세대 감성의 디자인 + +컴포넌트: +- Style Selection Card (5개, 카드 선택 UI) + 구조: + - Selected Badge (우측 상단, 조건부) + - 크기: 32x32px + - 배경: KT Red (#E31E24) + - 아이콘: check (White, 20px) + - 둥근 모서리: 50% (원형) + - Image Preview (1:1 비율) + - 배경: Gray-100 (플레이스홀더) + - 둥근 모서리: 12px + - Style Name (H3) + - Description (Body-S, Secondary) + - "크게보기" 버튼 (Secondary Button, Small) + - Radio Input (display: none) + + 스타일: + - 기본: border 3px solid transparent + - Hover: transform translateY(-2px), 그림자 증가 + - Selected: border-color KT Red, 그림자 강조 + - Cursor: pointer + +- Fullscreen Modal + - 배경: rgba(0, 0, 0, 0.95) + - Close 버튼 (우측 상단) + - Image (contain, max 100%) + +- Primary Button (다음) + +상호작용: +- 카드 클릭: Radio 선택 토글 +- Selected 시: 테두리 강조 + 체크 배지 표시 +- "크게보기" 클릭: 전체화면 모달 (이벤트 버블링 방지) +- 모달 닫기: 배경 클릭 or X 버튼 +- AI 생성 시간: 약 5초 (로딩 표시) + +유저스토리: UFR-CONT-010 +Progress: 3/7 단계 + +구현 특징: +- Radio 버튼 숨김 (폼 제출용으로만 사용) +- 카드 전체가 label로 작동 +- 이벤트 버블링 방지 (stopPropagation) +- 키보드 접근성 지원 (Tab, Enter) +- transition: all 0.3s ease +``` + +#### 10-콘텐츠편집.html + +``` +레이아웃: +- Mobile: 세로 스택 (편집폼 → 미리보기) +- Desktop: 좌우 분할 (편집폼 | 미리보기) + +섹션: +1. 텍스트 편집 (좌측) + - 제목 입력 + - 경품 입력 + - 참여 안내 입력 + +2. 미리보기 (우측) + - 실시간 업데이트 + +삭제된 기능: +- 로고 위치 선택 (상단/하단/중앙) +- 로고 크기 조정 + +컴포넌트: +- Text Input (제목) +- Text Input (경품) +- Textarea (참여 안내) +- Preview Card (실시간) +- Primary Button (다음) +- Secondary Button (저장) + +상호작용: +- 입력 시 실시간 미리보기 업데이트 + +유저스토리: UFR-CONT-020 +Progress: 4/7 단계 +``` + +#### 11-배포채널선택.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 채널 선택 (복수 선택) + - 우리동네TV (Checkbox) + - 링고비즈 (Checkbox) + - SNS (Checkbox) + - QR코드 (Checkbox) + +2. 채널별 옵션 (조건부 표시) + - 우리동네TV: 지역 선택 + - SNS: 플랫폼 선택 (Instagram, Naver, Facebook) + - QR코드: 생성 옵션 + +컴포넌트: +- Checkbox List (채널) +- Conditional Options (각 채널) +- Primary Button (다음) + +상호작용: +- 채널 선택 시 해당 옵션 표시 +- 최소 1개 선택 필수 + +유저스토리: UFR-DIST-010 +Progress: 5/7 단계 +``` + +#### 12-최종승인.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 이벤트 요약 + - 제목, 경품, 참여 방법 +2. 이미지 미리보기 +3. 배포 채널 +4. 일정 설정 + - 시작일: 즉시 or 예약 + - 종료일: 선택 + +컴포넌트: +- Summary Card (요약 정보) +- Image Preview +- Channel Badges +- Date Picker (일정) +- Primary Button (승인 및 배포) +- Secondary Button (수정) + +상호작용: +- 수정: 이전 단계로 이동 +- 승인: 이벤트 생성 및 배포 + +유저스토리: UFR-DIST-020 +Progress: 7/7 단계 +``` + +### 4.4 이벤트 관리 및 모니터링 + +#### 13-이벤트상세.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 이벤트 헤더 + - 이미지 썸네일 + - 제목, 상태, 기간 + +2. 실시간 KPI (4개 카드) + - 참여자 수 + - 조회수 + - ROI + - 전환율 + +3. 빠른 액션 + - 참여자 목록 + - 당첨자 관리 + - 성과 분석 + - 이벤트 수정 + +4. 참여 추이 차트 + +컴포넌트: +- Event Header (이미지 + 정보) +- KPI Card (4개, 그리드) +- Action Button (4개, 그리드) +- Chart (참여 추이) + +유저스토리: UFR-EVENT-060 +Bottom Nav: 이벤트 탭 활성화 +``` + +#### 14-참여자목록.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 검색 바 +2. 필터 (채널, 상태) +3. 참여자 카드 리스트 +4. 페이지네이션 +5. 엑셀 다운로드 (Desktop) + +컴포넌트: +- Search Input +- Filter Dropdowns +- Participant Card (리스트) + - 이름, 전화번호, 참여 경로, 일시, 당첨 여부 +- Pagination +- Download Button (Desktop) + +상호작용: +- 카드 클릭: 참여자 상세 모달 + +유저스토리: UFR-PART-020 +``` + +#### 15-이벤트참여.html + +``` +유형: 고객용 화면 (공개 URL) + +레이아웃: 세로 스크롤 + +섹션: +1. 이벤트 이미지 +2. 이벤트 정보 + - 제목, 경품, 참여 방법 +3. 참여 폼 + - 이름, 전화번호 입력 +4. 개인정보 동의 +5. 참여하기 버튼 + +컴포넌트: +- Event Banner (이미지) +- Info Card (정보) +- Text Input (이름, 전화번호) +- Checkbox (동의) +- Primary Button (참여하기) + +상호작용: +- 유효성 검증 +- 중복 참여 방지 +- 참여 완료 모달 + +유저스토리: UFR-PART-010 +``` + +#### 16-당첨자추첨.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 추첨 설정 + - 당첨자 수 입력 + - 자동 추첨 버튼 + +2. 당첨자 목록 (추첨 후) + - 이름, 전화번호, 당첨 경품 + +3. 액션 + - 문자 발송 + - 엑셀 다운로드 + +컴포넌트: +- Number Input (당첨자 수) +- Primary Button (추첨) +- Winner Card (리스트) +- Action Buttons (문자, 다운로드) + +상호작용: +- 추첨: 애니메이션 효과 +- 결과: 즉시 표시 + +유저스토리: UFR-PART-030 +``` + +#### 17-성과분석.html + +``` +레이아웃: 세로 스크롤 + +섹션: +1. 요약 KPI (4개 카드) + - 참여자 수 + - 총 비용 + - 예상 수익 + - ROI + +2. 차트 (2개, 그리드) + - 채널별 성과 (파이 차트) + - 시간대별 참여 추이 (라인 차트) + +3. 상세 분석 (2개, 그리드) + - ROI 상세 (테이블) + - 참여자 프로필 (바 차트) + +컴포넌트: +- Summary Card (4개, 그리드) +- Chart Card (파이, 라인) +- Table (ROI 상세) +- Bar Chart (참여자 프로필) +- Refresh Indicator (실시간 업데이트) + +상호작용: +- Pull to Refresh (모바일) +- 5분마다 자동 갱신 + +유저스토리: UFR-ANAL-010 +Bottom Nav: 분석 탭 활성화 +``` + +--- + +## 5. 반응형 디자인 + +### 5.1 Breakpoints + +``` +Mobile (기본): 320px ~ 767px +Tablet: 768px ~ 1023px +Desktop: 1024px 이상 +``` + +### 5.2 레이아웃 변화 + +#### 대시보드 (05) + +``` +Mobile: KPI 세로 스택 +Tablet: KPI 2x2 그리드, 이벤트 2열 +Desktop: KPI 4열, 이벤트 3열 + 사이드바 +``` + +#### 이벤트 생성 (09, 10) + +``` +Mobile: 세로 스택 +Tablet: 세로 스택 (간격 증가) +Desktop: 좌우 분할 (편집 | 미리보기) +``` + +#### 성과 분석 (17) + +``` +Mobile: 차트 세로 스택 +Tablet: 차트 2x1 그리드 +Desktop: 차트 2x2 그리드 +``` + +--- + +## 6. 인터랙션 패턴 + +### 6.1 Navigation + +- **Bottom Nav**: 탭 클릭 시 즉시 화면 전환 +- **Back Button**: 이전 화면으로 이동 +- **FAB**: 새 이벤트 생성 플로우 시작 + +### 6.2 Form + +- **실시간 검증**: 입력 필드 blur 시 유효성 검사 +- **에러 메시지**: 필드 하단에 빨간색 텍스트 +- **성공 피드백**: Toast 메시지 + +### 6.3 Loading States + +- **AI 처리**: Progress Indicator + 예상 시간 +- **데이터 로딩**: Skeleton Screen +- **버튼 클릭**: Spinner + 비활성화 + +### 6.4 Feedback + +- **성공**: Green Toast + 체크 아이콘 +- **에러**: Red Toast + X 아이콘 +- **정보**: Blue Toast + i 아이콘 + +### 6.5 Gestures (Mobile) + +- **Swipe**: 이벤트 카드 좌우 스와이프 (삭제/편집) +- **Pull to Refresh**: 대시보드, 성과 분석 +- **Long Press**: 컨텍스트 메뉴 표시 + +--- + +## 7. 접근성 고려사항 + +### 7.1 WCAG 2.1 AA 준수 + +- **색상 대비**: 최소 4.5:1 (텍스트), 3:1 (UI 요소) +- **터치 영역**: 최소 44x44px +- **키보드 네비게이션**: 모든 인터랙티브 요소 접근 가능 +- **Focus Indicator**: 명확한 포커스 표시 + +### 7.2 스크린 리더 지원 + +- **ARIA Labels**: 모든 버튼, 링크, 폼 필드 +- **ARIA Roles**: 적절한 역할 지정 +- **Live Regions**: 동적 콘텐츠 업데이트 알림 + +### 7.3 대안 제공 + +- **색상 외 표현**: 아이콘 + 텍스트 조합 +- **키보드 대안**: 드래그 앤 드롭 → 버튼 클릭 +- **음성 대안**: 이미지 alt 텍스트 + +--- + +## 8. 성능 최적화 + +### 8.1 이미지 + +- **포맷**: WebP (fallback: JPG) +- **압축**: Quality 80-85% +- **Lazy Loading**: 스크롤 시 로드 + +### 8.2 폰트 + +- **Font Display**: swap +- **Preload**: 주요 폰트 +- **Subset**: 자주 쓰는 글자만 + +### 8.3 애니메이션 + +- **GPU 가속**: transform, opacity +- **Will-Change**: 애니메이션 직전만 적용 + +--- + +## 9. 에러 처리 + +### 9.1 네트워크 에러 + +``` +표시: Toast (빨간색) +메시지: "네트워크 연결을 확인해주세요" +액션: 재시도 버튼 +``` + +### 9.2 Validation 에러 + +``` +표시: Input 하단 메시지 +색상: Error Red (#D32F2F) +예시: "이메일 형식이 올바르지 않습니다" +``` + +### 9.3 서버 에러 + +``` +표시: Modal Dialog +메시지: "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요." +액션: 확인 버튼 +``` + +### 9.4 Empty State + +``` +표시: 중앙 정렬 메시지 + 아이콘 +예시: +- "아직 이벤트가 없습니다" +- "참여자가 없습니다" +액션: 관련 CTA 버튼 +``` + +--- + +## 10. 디자인 시스템 참조 + +- **스타일 가이드**: design/uiux/style-guide.md +- **색상 시스템**: KT Red (#E31E24), AI Blue (#0066FF) +- **타이포그래피**: Pretendard, 8단계 스케일 +- **간격 시스템**: 4px 기반, 6단계 +- **컴포넌트**: Button, Card, Input, Modal, Toast, Bottom Nav +- **아이콘**: Material Icons Outlined/Filled + +--- + +## 11. 변경 이력 + +### Version 2.1 (2025-10-21) + +- 프로토타입 실제 구현 내용 반영 +- 08-AI이벤트추천: 예산별 2가지 방식(온라인/오프라인) 상세화 +- 08-AI이벤트추천: Budget Navigation (Sticky) 추가 +- 08-AI이벤트추천: Editable Field (인라인 편집) 기능 추가 +- 09-콘텐츠미리보기: Selection Card 세부 구현 상세 기술 +- 09-콘텐츠미리보기: Fullscreen Modal, 이벤트 버블링 방지 추가 + +### Version 2.0 (2025-10-21) + +- 프로토타입 분석 기반 전면 개정 +- 화면 번호 체계 정립 (01~17번) +- 네비게이션 구조 변경 (햄버거 메뉴 제거, Bottom Nav 4탭) +- 이벤트 생성 플로우 상세화 (7단계) +- 카드 선택 UI 패턴 추가 (09-콘텐츠미리보기) +- 로고 위치 선택 삭제 (10-콘텐츠편집) +- 성과 분석 Bottom Nav 추가 (17-성과분석) +- 실시간 KPI 업데이트 추가 (13-이벤트상세) + +### Version 1.0 (2025-10-17) + +- 초안 작성 + +--- + +**문서 끝** diff --git a/design/uiux/uiux.md b/design/uiux/uiux.md new file mode 100644 index 0000000..7207ab8 --- /dev/null +++ b/design/uiux/uiux.md @@ -0,0 +1,2473 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 UI/UX 설계서 + +## 문서 정보 +- **프로젝트명**: KT AI 기반 소상공인 이벤트 자동 생성 서비스 +- **작성일**: 2025-10-21 +- **버전**: 1.0 +- **작성자**: UI/UX Designer + +--- + +## 목차 +1. [프로젝트 개요](#1-프로젝트-개요) +2. [설계 원칙](#2-설계-원칙) +3. [프로토타입 화면 목록](#3-프로토타입-화면-목록) +4. [화면 간 사용자 플로우](#4-화면-간-사용자-플로우) +5. [화면별 상세 설계](#5-화면별-상세-설계) +6. [화면 간 전환 및 네비게이션](#6-화면-간-전환-및-네비게이션) +7. [반응형 설계 전략](#7-반응형-설계-전략) +8. [접근성 보장 방안](#8-접근성-보장-방안) +9. [성능 최적화 방안](#9-성능-최적화-방안) +10. [변경 이력](#10-변경-이력) + +--- + +## 1. 프로젝트 개요 + +### 1.1 서비스 목적 +소상공인 및 중소기업의 이벤트 기획·제작·운영 역량과 시간 부족 문제를 해결하기 위한 AI 기반 이벤트 자동 생성 서비스 + +### 1.2 핵심 가치 제안 +- **AI 트렌드 분석**: 업종/지역/시즌 트렌드 자동 분석 +- **3가지 이벤트 추천**: 목적에 맞는 최적화된 이벤트 기획안 +- **자동 콘텐츠 생성**: SNS 이미지 3가지 스타일 자동 생성 +- **다중 채널 배포**: 우리동네TV, 링고비즈, 지니TV, SNS 동시 배포 +- **실시간 성과 측정**: 통합 대시보드로 투자대비수익률 분석 + +### 1.3 타겟 사용자 +- **주 사용자**: 소상공인 (음식점, 카페, 소매점 운영자) +- **사용 환경**: 모바일 중심 (60%), 데스크톱 (40%) +- **사용 시간**: 매장 운영 중 짧은 시간 활용 (5-10분 내 이벤트 생성) + +### 1.4 마이크로서비스 구성 +1. **User** - 사용자 인증 및 매장정보 관리 +2. **Event** - 이벤트 기획 및 관리 +3. **AI** - AI 기반 트렌드 분석 및 이벤트 추천 +4. **Content** - SNS 콘텐츠 생성 +5. **Distribution** - 다중 채널 배포 관리 +6. **Participation** - 이벤트 참여 및 당첨자 관리 +7. **Analytics** - 실시간 효과 측정 및 통합 대시보드 + +--- + +## 2. 설계 원칙 + +### 2.1 Mobile First 디자인 철학 + +#### 우선순위 중심 설계 (Priority-Driven Design) +- **Above the Fold 최적화**: 스크롤 없이 핵심 정보 확인 +- **Single Column Layout**: 모바일에서 단일 컬럼으로 정보 수직 배치 +- **Progressive Disclosure**: 상세 정보는 탭/아코디언으로 숨김 + +#### 점진적 향상 (Progressive Enhancement) +``` +Mobile (320-767px) → 핵심 기능만 제공 +Tablet (768-1023px) → 부가 정보 추가 +Desktop (1024px+) → 전체 기능 및 상세 분석 +``` + +#### 성능 최적화 (Performance Optimization) +- **First Contentful Paint**: < 1.5s +- **Time to Interactive**: < 3s +- **Lighthouse Score**: > 90 + +### 2.2 유저스토리 기반 설계 +- **불필요한 추가 설계 금지**: 20개 유저스토리와 정확히 매칭 +- **우선순위 반영**: M(Must) > S(Should) > C(Could) > W(Won't) 순서로 개발 + +### 2.3 사용성 원칙 +- **3 Tap Rule**: 모든 주요 기능은 3번 탭 내 도달 가능 +- **터치 영역**: 최소 44x44px (애플 권장) +- **가독성**: 최소 글꼴 크기 14px (모바일), 16px (데스크톱) +- **색상 대비**: WCAG AA 등급 이상 (4.5:1) + +--- + +## 3. 프로토타입 화면 목록 + +### 총 17개 화면 + +| No | 화면명 | 유저스토리 | 우선순위 | 마이크로서비스 | Mobile 중요도 | +|----|--------|----------|---------|--------------|-------------| +| **인증 및 사용자 관리** | +| 01 | 로그인 | UFR-USER-020 | M/8 | User | ⭐⭐⭐ | +| 02 | 회원가입 | UFR-USER-010 | M/21 | User | ⭐⭐⭐ | +| 03 | 프로필편집 | UFR-USER-030 | C/8 | User | ⭐ | +| 04 | 로그아웃확인 | UFR-USER-040 | S/3 | User | ⭐ | +| **메인 및 대시보드** | +| 05 | 대시보드 | UFR-EVENT-010 | S/13 | Event | ⭐⭐⭐ | +| 06 | 이벤트목록 | UFR-EVENT-070 | S/13 | Event | ⭐⭐ | +| **이벤트 생성 플로우** | +| 07 | 이벤트목적선택 | UFR-EVENT-020 | M/5 | Event | ⭐⭐⭐ | +| 08 | AI이벤트추천 | UFR-EVENT-030 | M/34 | AI | ⭐⭐⭐ | +| 09 | SNS이미지생성 | UFR-CONT-010 | M/21 | Content | ⭐⭐⭐ | +| 10 | 콘텐츠편집 | UFR-CONT-020 | S/13 | Content | ⭐⭐ | +| 11 | 배포채널선택 | UFR-EVENT-040 | M/13 | Event | ⭐⭐ | +| 12 | 최종승인 | UFR-EVENT-050 | M/13 | Event | ⭐⭐⭐ | +| **이벤트 관리** | +| 13 | 이벤트상세 | UFR-EVENT-060 | S/13 | Event | ⭐⭐⭐ | +| 14 | 참여자목록 | UFR-PART-020 | S/8 | Participation | ⭐ | +| **참여자 관리** | +| 15 | 이벤트참여 | UFR-PART-010 | M/13 | Participation | ⭐⭐⭐ | +| 16 | 당첨자추첨 | UFR-PART-030 | M/13 | Participation | ⭐ | +| **성과 분석** | +| 17 | 실시간대시보드 | UFR-ANAL-010 | M/34 | Analytics | ⭐⭐ | + +--- + +## 4. 화면 간 사용자 플로우 + +```mermaid +graph TD + Start([시작]) --> Login{로그인 여부} + + %% 최초 사용자 플로우 + Login -->|미로그인| SignUp[02-회원가입] + SignUp --> Login01[01-로그인] + Login01 --> Dashboard[05-대시보드] + + Login -->|로그인됨| Dashboard + + %% 메인 대시보드 액션 + Dashboard --> EventList[06-이벤트목록] + Dashboard --> EventDetail[13-이벤트상세] + Dashboard --> Profile[03-프로필편집] + Dashboard --> CreateEvent[07-이벤트목적선택] + Dashboard --> Logout[04-로그아웃확인] + + %% 이벤트 생성 플로우 (핵심) + CreateEvent --> AIRecommend[08-AI이벤트추천] + AIRecommend --> ImageGen[09-SNS이미지생성] + ImageGen --> ContentEdit[10-콘텐츠편집] + ContentEdit --> ChannelSelect[11-배포채널선택] + ChannelSelect --> FinalApproval[12-최종승인] + FinalApproval --> Dashboard + + %% 이벤트 관리 플로우 + EventDetail --> ParticipantList[14-참여자목록] + EventDetail --> DrawWinner[16-당첨자추첨] + EventDetail --> Analytics[17-실시간대시보드] + + EventList --> EventDetail + + %% 고객 참여 플로우 (별도) + External([외부 채널
SNS/TV/연결음]) --> Participate[15-이벤트참여] + Participate --> ThankYou([참여 완료]) + + %% 프로필 편집 + Profile --> Dashboard + + %% 로그아웃 + Logout --> Login01 + + style Dashboard fill:#4A90E2,color:#fff + style CreateEvent fill:#7ED321,color:#fff + style AIRecommend fill:#7ED321,color:#fff + style FinalApproval fill:#7ED321,color:#fff + style Analytics fill:#F5A623,color:#fff + style Participate fill:#BD10E0,color:#fff +``` + +--- + +## 5. 화면별 상세 설계 + +### 5.1 인증 및 사용자 관리 + +#### 01-로그인 + +**개요** +- **목적**: 기존 사용자의 안전한 서비스 접근 +- **관련 유저스토리**: UFR-USER-020 +- **비즈니스 중요도**: M/8 (필수, 낮은 복잡도) + +**주요 기능** +- 전화번호/비밀번호 입력 +- 로그인 유지 옵션 +- 비밀번호 찾기 +- 회원가입 링크 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ │ +│ [서비스 로고] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 전화번호 입력 │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 비밀번호 입력 │ │ +│ └─────────────────┘ │ +│ │ +│ ☐ 로그인 유지 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 로그인 버튼 │ │ +│ └─────────────────┘ │ +│ │ +│ 비밀번호 찾기 | 회원가입 │ +│ │ +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ │ +│ [서비스 로고 + 설명] │ +│ │ +│ ┌──────────────────────┐ │ +│ │ 전화번호 입력 │ │ +│ └──────────────────────┘ │ +│ ┌──────────────────────┐ │ +│ │ 비밀번호 입력 │ │ +│ └──────────────────────┘ │ +│ │ +│ ☐ 로그인 유지 │ +│ │ +│ ┌──────────────────────┐ │ +│ │ 로그인 버튼 │ │ +│ └──────────────────────┘ │ +│ │ +│ 비밀번호 찾기 | 회원가입 │ +│ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **포커스 이동**: Enter 키로 다음 입력 필드 이동 +2. **입력 검증**: 실시간 형식 검증 (전화번호 형식, 비밀번호 최소 길이) +3. **비밀번호 표시**: 눈 아이콘 클릭으로 비밀번호 보기/숨기기 +4. **로그인 실패**: 오류 메시지 입력 필드 하단에 빨간색으로 표시 +5. **로딩 상태**: 버튼 비활성화 + 스피너 표시 + +**반응형 처리** +- Mobile: 세로 전체 화면, 버튼 하단 고정 +- Tablet: 중앙 카드 형태 (최대 너비 480px) +- Desktop: 좌측 브랜딩 + 우측 로그인 폼 (50:50 레이아웃) + +**접근성** +- Label과 Input 연결 (for/id 속성) +- 오류 메시지는 aria-live="assertive" +- Tab 키 순서: 전화번호 → 비밀번호 → 로그인 유지 → 로그인 버튼 + +--- + +#### 02-회원가입 + +**개요** +- **목적**: 신규 소상공인 사용자 온보딩 +- **관련 유저스토리**: UFR-USER-010 +- **비즈니스 중요도**: M/21 (필수, 중간 복잡도) + +**주요 기능** +- 다단계 폼 (3단계) + - Step 1: 기본 정보 (이름, 전화번호, 이메일, 비밀번호) + - Step 2: 매장 정보 (매장명, 업종, 주소, 영업시간) + - Step 3: 사업자번호 검증 +- 진행 인디케이터 +- 단계별 유효성 검증 +- 사업자번호 국세청 DB 연동 검증 + +**UI 구성요소** + +*Mobile (320-767px) - Step 1* +``` +┌─────────────────────┐ +│ ← 회원가입 │ +├─────────────────────┤ +│ ●──○──○ (진행바) │ +│ │ +│ 기본 정보 입력 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 이름 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 전화번호 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 이메일 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 비밀번호 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 비밀번호 확인 * │ │ +│ └─────────────────┘ │ +│ │ +│ 비밀번호는 8자 이상, │ +│ 영문/숫자/특수문자 │ +│ 포함 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 다음 단계 │ │ +│ └─────────────────┘ │ +└─────────────────────┘ +``` + +*Mobile (320-767px) - Step 2* +``` +┌─────────────────────┐ +│ ← 회원가입 │ +├─────────────────────┤ +│ ○──●──○ (진행바) │ +│ │ +│ 매장 정보 입력 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 매장명 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 업종 선택 * │ │ (드롭다운) +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 주소 검색 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 상세 주소 │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 영업시간 │ │ (선택) +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 다음 단계 │ │ +│ └─────────────────┘ │ +└─────────────────────┘ +``` + +*Mobile (320-767px) - Step 3* +``` +┌─────────────────────┐ +│ ← 회원가입 │ +├─────────────────────┤ +│ ○──○──● (진행바) │ +│ │ +│ 사업자번호 검증 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 사업자번호 * │ │ +│ │ 000-00-00000 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 검증하기 │ │ +│ └─────────────────┘ │ +│ │ +│ [검증 결과 영역] │ +│ ✓ 정상 사업자 │ +│ 업체명: 왕갈비통닭 │ +│ 대표자: 홍길동 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 회원가입 완료 │ │ +│ └─────────────────┘ │ +└─────────────────────┘ +``` + +**인터랙션** +1. **진행바**: 현재 단계 표시, 클릭으로 이전 단계 이동 가능 +2. **필수 필드**: 별표(*) 표시, 미입력 시 다음 버튼 비활성화 +3. **실시간 검증**: + - 전화번호: 010-0000-0000 형식 자동 포맷 + - 이메일: @ 포함 여부 확인 + - 비밀번호: 8자 이상, 영문/숫자/특수문자 포함 여부 표시 +4. **주소 검색**: Daum 주소 API 팝업 또는 Bottom Sheet +5. **사업자번호 검증**: + - 로딩 인디케이터 (3초 이내) + - 성공: 녹색 체크 + 업체 정보 표시 + - 실패: 빨간색 X + 오류 메시지 +6. **뒤로 가기**: 이전 입력 값 유지 + +**반응형 처리** +- Mobile: 전체 화면, 단계별 스크롤 +- Tablet: 중앙 카드 (최대 너비 600px) +- Desktop: 좌측 진행바 + 우측 폼 (30:70 레이아웃) + +**접근성** +- 진행바는 aria-label="회원가입 진행 단계" +- 필수 필드는 aria-required="true" +- 검증 결과는 aria-live="polite" + +--- + +#### 03-프로필편집 + +**개요** +- **목적**: 사용자 정보 수정 +- **관련 유저스토리**: UFR-USER-030 +- **비즈니스 중요도**: C/8 (선택, 낮은 복잡도) + +**주요 기능** +- 기본 정보 수정 (이름, 전화번호, 이메일) +- 매장 정보 수정 (매장명, 업종, 주소, 영업시간) +- 비밀번호 변경 (현재 비밀번호 확인 필수) +- 변경사항 저장 확인 다이얼로그 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 프로필 편집 👤│ ← Header 추가 +├─────────────────────┤ +│ │ +│ ▼ 기본 정보 │ +│ 이름: [홍길동 ] │ +│ 전화번호: [010-...] │ +│ 이메일: [hong@...]│ +│ │ +│ ▼ 매장 정보 │ +│ 매장명: [왕갈비...]│ +│ 업종: [음식점 ] │ +│ 주소: [수원시...] │ +│ 영업시간: [10:00~]│ +│ │ +│ ▼ 비밀번호 변경 │ +│ 현재 비밀번호: [ ] │ +│ 새 비밀번호: [ ] │ +│ 비밀번호 확인: [ ] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 저장하기 │ │ +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom 추가 +└─────────────────────┘ +``` + +**인터랙션** +1. **아코디언**: 각 섹션 클릭으로 접기/펼치기 +2. **전화번호 변경**: 재인증 다이얼로그 표시 (SMS 인증) +3. **저장 확인**: "변경사항을 저장하시겠습니까?" 다이얼로그 +4. **취소 확인**: 변경사항이 있을 경우 "저장하지 않고 나가시겠습니까?" 다이얼로그 +5. **성공 토스트**: "프로필이 성공적으로 업데이트되었습니다" (2초) + +**반응형 처리** +- Mobile: 전체 화면, 아코디언 형태 +- Tablet/Desktop: 중앙 카드 (최대 너비 720px), 저장 버튼 하단 고정 + +--- + +#### 04-로그아웃확인 + +**개요** +- **목적**: 안전한 로그아웃 +- **관련 유저스토리**: UFR-USER-040 +- **비즈니스 중요도**: S/3 (권장, 낮은 복잡도) + +**주요 기능** +- 로그아웃 확인 다이얼로그 +- 세션 종료 +- 로그인 화면 이동 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ │ +│ [어두운 배경] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 로그아웃 │ │ +│ ├─────────────────┤ │ +│ │ │ │ +│ │ 로그아웃 하시겠 │ │ +│ │ 습니까? │ │ +│ │ │ │ +│ ├─────────────────┤ │ +│ │ ┌─────┐┌─────┐│ │ +│ │ │취소 ││확인 ││ │ +│ │ └─────┘└─────┘│ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────┘ +``` + +**인터랙션** +1. **다이얼로그 표시**: 프로필 메뉴 → 로그아웃 선택 +2. **확인**: 세션 종료 + 로그인 화면 이동 (애니메이션) +3. **취소**: 다이얼로그 닫기, 이전 화면 유지 +4. **배경 클릭**: 취소와 동일 + +**반응형 처리** +- Mobile: Bottom Sheet 형태 +- Tablet/Desktop: 중앙 Modal Dialog (최대 너비 400px) + +--- + +### 5.2 메인 및 대시보드 + +#### 05-대시보드 + +**개요** +- **목적**: 메인 허브, 이벤트 현황 한눈에 파악 +- **관련 유저스토리**: UFR-EVENT-010 +- **비즈니스 중요도**: S/13 (권장, 중간 복잡도) + +**주요 기능** +- 진행중 이벤트 3개 표시 (모바일) +- 예정/종료 이벤트 개수 +- "새 이벤트 만들기" 버튼 (FAB) +- Bottom Navigation (홈, 이벤트, 분석, 프로필) + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 대시보드 👤 │ ← Header +├─────────────────────┤ +│ │ +│ 안녕하세요, 홍길동님!│ +│ │ +│ ┌─────────────────┐ │ +│ │ 진행중 3 | 예정 1│ │ +│ │ 종료 5 | 총 9개 │ │ +│ └─────────────────┘ │ +│ │ +│ 진행중 이벤트 │ +│ ┌─────────────────┐ │ +│ │ 신규고객 이벤트 │ │ +│ │ 참여자: 128명 │ │ +│ │ D-5 종료까지 │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 재방문 이벤트 │ │ +│ │ 참여자: 56명 │ │ +│ │ D-12 종료까지 │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 매출증대 이벤트 │ │ +│ │ 참여자: 89명 │ │ +│ │ D-20 종료까지 │ │ +│ └─────────────────┘ │ +│ │ +│ 더보기 > │ +│ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필 │ ← Nav Bottom +└─────────────────────┘ + [+] ← FAB +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ [로고] 대시보드 [프로필] │ +├─────────────────────────────────────┤ +│ [사이드바] 안녕하세요, 홍길동님! │ +│ ┌────────────────────────┐│ +│ • 대시보드 │ 진행중 5 | 예정 2 | 종료 10 ││ +│ • 이벤트 └────────────────────────┘│ +│ • 분석 │ +│ • 프로필 진행중 이벤트 │ +│ ┌──────┐┌──────┐┌──────┐│ +│ │이벤트1││이벤트2││이벤트3││ +│ │참여128││참여56││참여89││ +│ │D-5 ││D-12 ││D-20 ││ +│ └──────┘└──────┘└──────┘│ +│ ┌──────┐┌──────┐ │ +│ │이벤트4││이벤트5│ │ +│ │... ││... │ │ +│ └──────┘└──────┘ │ +│ │ +│ 예정 이벤트 │ +│ ┌──────┐┌──────┐ │ +│ │이벤트1││이벤트2│ │ +│ └──────┘└──────┘ │ +│ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **이벤트 카드 클릭**: 이벤트 상세 화면 이동 +2. **FAB 클릭**: 이벤트 목적 선택 화면 이동 +3. **더보기**: 이벤트 목록 화면 이동 +4. **Bottom Navigation**: 메뉴 전환 (애니메이션) + +**반응형 처리** +- Mobile: 카드 1열, Bottom Navigation +- Tablet: 카드 2열, Bottom Navigation +- Desktop: 카드 3열, Side Navigation + +--- + +#### 06-이벤트목록 + +**개요** +- **목적**: 전체 이벤트 목록 조회 및 관리 +- **관련 유저스토리**: UFR-EVENT-070 +- **비즈니스 중요도**: S/13 (권장, 중간 복잡도) + +**주요 기능** +- 전체 이벤트 목록 테이블 +- 상태별 필터 (전체/진행중/예정/종료) +- 기간별 필터 (최근 1개월/3개월/6개월/1년/전체) +- 이벤트명 검색 +- 정렬 기능 (최신순/참여자순/투자대비수익률순) + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 이벤트 목록 👤│ ← Header +├─────────────────────┤ +│ [검색창] │ +│ 🔍 이벤트명 검색... │ +│ │ +│ 📂 필터 │ +│ [전체▼] [최근1개월▼]│ +│ │ +│ 정렬: [최신순 ▼] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 신규고객 이벤트 │ │ +│ │ 진행중 | D-5 │ │ +│ │ 참여 128명 │ │ +│ │ 투자대비수익률: 450%│ │ +│ │ 2025-11-01~15 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 재방문 이벤트 │ │ +│ │ 진행중 | D-12 │ │ +│ │ 참여 56명 │ │ +│ │ 투자대비수익률: 320%│ │ +│ │ 2025-11-05~20 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 매출증대 이벤트 │ │ +│ │ 종료 │ │ +│ │ 참여 234명 │ │ +│ │ 투자대비수익률: 580%│ │ +│ │ 2025-10-15~31 │ │ +│ └─────────────────┘ │ +│ │ +│ [더보기...] │ +│ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ [로고] 이벤트 목록 [프로필] │ +├─────────────────────────────────────┤ +│ [사이드바] 🔍 [검색창] 📂 필터│ +│ 이벤트명 검색... [전체▼] │ +│ • 대시보드 [최근1개월▼]│ +│ • 이벤트 정렬: [최신순 ▼] │ +│ • 분석 ────────────────────────│ +│ • 프로필 │이벤트명│기간│상태│참여자│투자대비수익률│ +│ ├──────┼────┼──┼────┼────┤ +│ │신규고객│11/1~│진행│128명│450% │ +│ │이벤트 │11/15│중 │ │ │ +│ ├──────┼────┼──┼────┼────┤ +│ │재방문 │11/5~│진행│56명 │320% │ +│ │이벤트 │11/20│중 │ │ │ +│ ├──────┼────┼──┼────┼────┤ +│ │매출증대│10/15│종료│234명│580% │ +│ │이벤트 │~10/31│ │ │ │ +│ ├──────┼────┼──┼────┼────┤ +│ │... │... │... │... │... │ +│ └──────┴────┴──┴────┴────┘ +│ │ +│ [1] 2 3 4 5 ... 10 > │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **검색**: 입력 중 실시간 검색 결과 업데이트 +2. **필터 적용**: 상태/기간 선택 시 즉시 목록 갱신 +3. **정렬**: 드롭다운 선택 시 정렬 기준 변경 +4. **카드/행 클릭**: 이벤트 상세 화면으로 이동 +5. **페이지네이션**: 페이지당 20개 표시 + +**반응형 처리** +- Mobile: 카드 형태 1열 표시 +- Tablet: 카드 형태 2열 표시 +- Desktop: 테이블 형태 표시 + +--- + +### 5.3 이벤트 생성 플로우 + +#### 07-이벤트목적선택 + +**개요** +- **목적**: 이벤트 목적 선택으로 AI 추천 최적화 +- **관련 유저스토리**: UFR-EVENT-020 +- **비즈니스 중요도**: M/5 (필수, 낮은 복잡도) + +**주요 기능** +- 4가지 이벤트 목적 선택 +- 각 목적별 설명 및 예상 효과 제공 +- 목적 선택 후 AI 추천 자동 진행 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 이벤트 목적 선택 👤│ ← Header +├─────────────────────┤ +│ │ +│ 이벤트 목적을 선택해 │ +│ 주세요 │ +│ │ +│ ┌─────────────────┐ │ +│ │ ○ 신규 고객 유치 │ │ +│ │ ────────────────│ │ +│ │ 새로운 고객 확보 │ │ +│ │ 예상: 참여율 높음│ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ ○ 재방문 유도 │ │ +│ │ ────────────────│ │ +│ │ 기존 고객 재방문 │ │ +│ │ 예상: 충성도 향상│ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ ○ 매출 증대 │ │ +│ │ ────────────────│ │ +│ │ 단기 매출 향상 │ │ +│ │ 예상: 즉각적 효과│ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ ○ 인지도 향상 │ │ +│ │ ────────────────│ │ +│ │ 브랜드 인지도 제고│ │ +│ │ 예상: 장기적 효과│ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 다음 단계 │ │ ← 선택 후 활성화 +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필 │ ← Nav Bottom +└─────────────────────┘ +``` + +*Tablet/Desktop (768px+)* +``` +┌─────────────────────────────────────┐ +│ ← 이벤트 목적 선택 │ +├─────────────────────────────────────┤ +│ │ +│ 이벤트 목적을 선택해주세요 │ +│ │ +│ ┌──────────┐┌──────────┐ │ +│ │○신규고객 ││○재방문 │ │ +│ │ 유치 ││ 유도 │ │ +│ │──────────││──────────│ │ +│ │새로운 고객││기존 고객 │ │ +│ │확보 ││재방문 │ │ +│ │참여율 높음││충성도 향상│ │ +│ └──────────┘└──────────┘ │ +│ │ +│ ┌──────────┐┌──────────┐ │ +│ │○매출 ││○인지도 │ │ +│ │ 증대 ││ 향상 │ │ +│ │──────────││──────────│ │ +│ │단기 매출 ││브랜드 │ │ +│ │향상 ││인지도 제고│ │ +│ │즉각적효과││장기적효과│ │ +│ └──────────┘└──────────┘ │ +│ │ +│ ┌──────────┐ │ +│ │ 다음 단계 │ │ +│ └──────────┘ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **목적 선택**: 라디오 버튼, 1개만 선택 가능 +2. **카드 클릭**: 전체 카드 영역 클릭으로 선택 +3. **다음 단계**: 선택 후 버튼 활성화, AI 이벤트 추천 화면 이동 +4. **애니메이션**: 선택 시 카드 하이라이트 효과 + +**반응형 처리** +- Mobile: 카드 1열 세로 배치 +- Tablet/Desktop: 카드 2×2 그리드 배치 + +--- + +#### 08-AI이벤트추천 + +**개요** +- **목적**: AI 트렌드 분석 + 3가지 이벤트 추천 +- **관련 유저스토리**: UFR-EVENT-030 (통합) +- **비즈니스 중요도**: M/34 (필수, 최고 복잡도) + +**주요 기능** +- 트렌드 분석 결과 표시 (업종/지역/시즌) +- 3가지 예산 수준별 이벤트 추천 (저비용/중비용/고비용) +- 각 예산별 온/오프라인 방식 2가지 추천 (온라인 1개, 오프라인 1개) +- 제목/경품 간편 수정 (인라인 편집) +- 로딩 인디케이터 (10초 이내) + +**UI 구성요소** + +*Mobile (320-767px) - 로딩 상태* +``` +┌─────────────────────┐ +│ ☰ AI 이벤트 추천 👤│ ← Header 추가 +├─────────────────────┤ +│ │ +│ │ +│ [AI 아이콘 애니메이션]│ +│ │ +│ AI가 트렌드를 분석하고│ +│ 최적의 이벤트를 │ +│ 추천하고 있어요... │ +│ │ +│ [진행 바 50%] │ +│ │ +│ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom 추가 +└─────────────────────┘ +``` + +*Mobile (320-767px) - 완료 상태* +``` +┌─────────────────────┐ +│ ☰ AI 이벤트 추천 👤│ ← Header +├─────────────────────┤ +│ ▼ 트렌드 분석 결과 │ +│ (접기/펼치기) │ +│ │ +│ 예산별 추천 이벤트 │ +│ (각 예산별 2가지 방식)│ +│ │ +│ ┌─────────────────┐ │ +│ │ 💰 옵션 1: 저비용│ │ +│ │ ────────────────│ │ +│ │ 🌐 온라인 방식 │ │ +│ │ [SNS 팔로우..✏] │ │ ← 제목 인라인 편집 +│ │ 경품: [커피 쿠폰✏]│ │ ← 경품 인라인 편집 +│ │ 참여: SNS 팔로우 │ │ +│ │ 예상: 180명 │ │ +│ │ 비용: 25만원 │ │ +│ │ 투자대비수익률: 520%│ │ +│ │ ○ 선택 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 🏪 오프라인 방식 │ │ +│ │ [전화번호 등록..✏]│ │ +│ │ 경품: [커피 쿠폰✏]│ │ +│ │ 참여: 전화번호 │ │ +│ │ 예상: 150명 │ │ +│ │ 비용: 30만원 │ │ +│ │ 투자대비수익률: 450%│ │ +│ │ ○ 선택 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 💳 옵션 2: 중비용│ │ +│ │ ────────────────│ │ +│ │ 🌐 온라인 방식 │ │ +│ │ [인스타 댓글..✏]│ │ +│ │ 경품: [상품권1만✏]│ │ +│ │ 참여: SNS 댓글 │ │ +│ │ 예상: 250명 │ │ +│ │ 비용: 48만원 │ │ +│ │ 투자대비수익률: 380%│ │ +│ │ ○ 선택 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 🏪 오프라인 방식 │ │ +│ │ [재방문 이벤트✏]│ │ +│ │ 경품: [상품권1만✏]│ │ +│ │ 참여: 영수증제출 │ │ +│ │ 예상: 200명 │ │ +│ │ 비용: 50만원 │ │ +│ │ 투자대비수익률: 320%│ │ +│ │ ○ 선택 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 💎 옵션 3: 고비용│ │ +│ │ ────────────────│ │ +│ │ 🌐 온라인 방식 │ │ +│ │ [유튜브 구독..✏]│ │ +│ │ 경품: [상품권5만✏]│ │ +│ │ 참여: 유튜브구독 │ │ +│ │ 예상: 400명 │ │ +│ │ 비용: 95만원 │ │ +│ │ 투자대비수익률: 280%│ │ +│ │ ○ 선택 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 🏪 오프라인 방식 │ │ +│ │ [VIP초대 이벤트✏]│ │ +│ │ 경품: [고급상품✏]│ │ +│ │ 참여: VIP 초대장│ │ +│ │ 예상: 300명 │ │ +│ │ 비용: 100만원 │ │ +│ │ 투자대비수익률: 250%│ │ +│ │ ○ 선택 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 다음 단계 │ │ ← 선택 후 활성화 +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom +└─────────────────────┘ +``` + +*트렌드 분석 결과 펼침 상태* +``` +┌─────────────────────┐ +│ ▼ 트렌드 분석 결과 │ +│ ────────────────────│ +│ │ +│ 📊 업종 트렌드 │ +│ • 최근 성공 유형: │ +│ 할인 쿠폰 (참여율 │ +│ 35% ↑) │ +│ • 선호 경품: │ +│ 커피/상품권 │ +│ • 효과적 참여방법: │ +│ 전화번호 > SNS │ +│ │ +│ 📍 지역 특성 │ +│ • 지역 성공률: │ +│ 수원 지역 28% ↑ │ +│ • 지역 고객 특성: │ +│ 20-30대 여성 주력│ +│ │ +│ 🗓️ 시즌 특성 │ +│ • 추천 이벤트: │ +│ 봄 신메뉴 프로모션│ +│ • 특별 시즌: │ +│ 화이트데이 (3/14) │ +│ │ +└─────────────────────┘ +``` + +**인터랙션** +1. **로딩**: + - AI 아이콘 애니메이션 (회전) + - 진행 바 0% → 100% (10초) + - 완료 시 부드러운 전환 +2. **트렌드 분석 접기/펼치기**: 아코디언 형태 +3. **제목/경품 편집**: + - 연필 아이콘 클릭 → 인라인 편집 모드 + - 최대 글자수 표시 (제목 50자, 경품 30자) + - Enter/저장 시 변경사항 저장 +4. **옵션 선택**: 라디오 버튼, 1개만 선택 가능 +5. **다음 단계**: 선택 후 버튼 활성화, 콘텐츠 생성 화면 이동 + +**반응형 처리** +- Mobile: 세로 스크롤, 카드 1열 +- Tablet: 카드 1열 유지, 너비 증가 +- Desktop: 좌측 트렌드 분석 (30%) + 우측 추천안 (70%) + +--- + +#### 09-SNS이미지생성 + +**개요** +- **목적**: 3가지 스타일 SNS 이미지 자동 생성 +- **관련 유저스토리**: UFR-CONT-010 +- **비즈니스 중요도**: M/21 (필수, 중간 복잡도) + +**주요 기능** +- 이벤트 정보 기반 이미지 생성 +- 3가지 스타일 제공 (심플/화려/트렌디) +- 생성 중 로딩 (5초 이내) +- 이미지 풀스크린 미리보기 + +**UI 구성요소** + +*Mobile (320-767px) - 생성 중* +``` +┌─────────────────────┐ +│ ☰ SNS 이미지 생성 👤│ ← Header 추가 +├─────────────────────┤ +│ │ +│ [AI 이미지 생성 중] │ +│ │ +│ 딥러닝 모델이 │ +│ 이벤트에 어울리는 │ +│ 이미지를 생성하고 │ +│ 있어요... │ +│ │ +│ [스피너 애니메이션] │ +│ │ +│ 예상 시간: 5초 │ +│ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom 추가 +└─────────────────────┘ +``` + +*Mobile (320-767px) - 완료* +``` +┌─────────────────────┐ +│ ☰ SNS 이미지 생성 👤│ ← Header +├─────────────────────┤ +│ 스타일 1: 심플 │ +│ ┌─────────────────┐ │ +│ │ │ │ +│ │ [이미지 미리보기]│ │ +│ │ │ │ +│ │ [이벤트 제목] │ │ +│ │ [경품 정보] │ │ +│ │ │ │ +│ └─────────────────┘ │ +│ ○ 선택 | 🔍 크게보기 │ +│ │ +│ 스타일 2: 화려 │ +│ ┌─────────────────┐ │ +│ │ [이미지...] │ │ +│ └─────────────────┘ │ +│ ○ 선택 | 🔍 크게보기 │ +│ │ +│ 스타일 3: 트렌디 │ +│ ┌─────────────────┐ │ +│ │ [이미지...] │ │ +│ └─────────────────┘ │ +│ ○ 선택 | 🔍 크게보기 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 건너뛰기 | 다음 │ │ +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom +└─────────────────────┘ +``` + +**인터랙션** +1. **생성 로딩**: + - Skeleton Screen (이미지 영역) + - 스피너 + 진행 메시지 +2. **이미지 선택**: 라디오 버튼, 1개 선택 +3. **크게보기**: 풀스크린 모달, 핀치 줌 가능 +4. **건너뛰기**: 이미지 없이 다음 단계 (콘텐츠 편집 스킵) +5. **다음**: 선택한 이미지로 콘텐츠 편집 화면 이동 + +**반응형 처리** +- Mobile: 이미지 1열, 세로 스크롤 +- Tablet: 이미지 2열 (1+2 또는 3열) +- Desktop: 이미지 3열 가로 배치 + +--- + +#### 10-콘텐츠편집 + +**개요** +- **목적**: 생성된 SNS 이미지 커스터마이징 +- **관련 유저스토리**: UFR-CONT-020 +- **비즈니스 중요도**: S/13 (권장, 중간 복잡도) + +**주요 기능** +- 텍스트 수정 (제목, 경품, 참여안내) +- 색상 조정 (배경색, 텍스트색, 강조색) +- 로고 위치 조정 +- 실시간 미리보기 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 콘텐츠 편집 👤│ ← Header +├─────────────────────┤ +│ 미리보기 │ +│ ┌─────────────────┐ │ +│ │ │ │ +│ │ [실시간 미리보기]│ │ +│ │ │ │ +│ │ [수정된 내용 │ │ +│ │ 즉시 반영] │ │ +│ │ │ │ +│ └─────────────────┘ │ +│ │ +│ 📝 텍스트 편집 │ +│ 제목: │ +│ [신규고객 이벤트...] │ +│ │ +│ 경품: │ +│ [커피 쿠폰 100매...]│ +│ │ +│ 참여안내: │ +│ [전화번호 입력...] │ +│ │ +│ 🎨 색상 조정 │ +│ 배경색: [■ 선택] │ +│ 텍스트: [■ 선택] │ +│ 강조색: [■ 선택] │ +│ │ +│ 🏢 로고 위치 │ +│ [◀ 위치이동 ▶] │ +│ [- 크기조절 +] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 저장 | 다음 단계 │ │ +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필 │ ← Nav Bottom +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ ← 콘텐츠 편집 [프로필]│ +├─────────────────────────────────────┤ +│ [사이드바] ┌────────┐ ┌──────────┐│ +│ │미리보기 │ │편집 패널 ││ +│ • 대시보드 │ │ │ ││ +│ • 이벤트 │ │ │📝 텍스트 ││ +│ • 분석 │[실시간 │ │제목: ││ +│ • 프로필 │ 미리보기]│ │[입력창] ││ +│ │ │ │ ││ +│ │ │ │경품: ││ +│ │ │ │[입력창] ││ +│ │ │ │ ││ +│ │ │ │🎨 색상 ││ +│ │ │ │배경: [■] ││ +│ │ │ │텍스트:[■] ││ +│ │ │ │강조: [■] ││ +│ │ │ │ ││ +│ │ │ │🏢 로고 ││ +│ │ │ │위치/크기 ││ +│ └────────┘ └──────────┘│ +│ │ +│ ┌────────┐ ┌──────────┐ │ +│ │ 저장 │ │다음 단계 │ │ +│ └────────┘ └──────────┘ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **텍스트 편집**: 입력 시 실시간 미리보기 업데이트 +2. **색상 선택**: 컬러 피커 팝업, 선택 시 즉시 반영 +3. **로고 위치**: 드래그 앤 드롭 또는 화살표 버튼 +4. **저장**: 편집 내용 저장, 현재 화면 유지 +5. **다음 단계**: 배포 채널 선택 화면으로 이동 + +**반응형 처리** +- Mobile: 세로 배치 (미리보기 → 편집 옵션) +- Tablet: 세로 배치 유지, 입력 영역 확대 +- Desktop: 좌우 분할 (미리보기 60% + 편집 패널 40%) + +--- + +#### 11-배포채널선택 + +**개요** +- **목적**: 다중 채널 선택 및 배포 설정 +- **관련 유저스토리**: UFR-EVENT-040 +- **비즈니스 중요도**: M/13 (필수, 중간 복잡도) + +**주요 기능** +- 4가지 배포 채널 선택 (우리동네TV, 링고비즈, 지니TV, SNS) +- 채널별 예상 노출 수 표시 +- 채널별 비용 안내 +- 총 예상 비용 합계 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 배포 채널 선택 👤│ ← Header +├─────────────────────┤ +│ │ +│ 배포 채널을 선택해 │ +│ 주세요 (최소 1개) │ +│ │ +│ ┌─────────────────┐ │ +│ │ ☑ 우리동네TV │ │ +│ │ ────────────────│ │ +│ │ 반경: [500m ▼] │ │ +│ │ 시간: [저녁 ▼] │ │ +│ │ 예상: 5만명 노출 │ │ +│ │ 비용: 8만원 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ ☑ 링고비즈 │ │ +│ │ ────────────────│ │ +│ │ 매장 전화번호: │ │ +│ │ 010-1234-5678 │ │ +│ │ 연결음 업데이트 │ │ +│ │ 비용: 무료 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ ☐ 지니TV 광고 │ │ +│ │ ────────────────│ │ +│ │ 지역: [수원 ▼] │ │ +│ │ 시간: [전체 ▼] │ │ +│ │ 예산: [입력...] │ │ +│ │ 예상: 계산중... │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ ☑ SNS │ │ +│ │ ────────────────│ │ +│ │ ☑ Instagram │ │ +│ │ ☑ Naver Blog │ │ +│ │ ☐ Kakao Channel │ │ +│ │ 예약: [즉시 ▼] │ │ +│ │ 비용: 무료 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 총 예상 비용: │ │ +│ │ 8만원 │ │ +│ │ 총 예상 노출: │ │ +│ │ 5만명+ │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ 다음 단계 │ │ +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필 │ ← Nav Bottom +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ ← 배포 채널 선택 [프로필]│ +├─────────────────────────────────────┤ +│ [사이드바] │ +│ 배포 채널을 선택해주세요 │ +│ • 대시보드 (최소 1개 이상) │ +│ • 이벤트 ─────────────────────────│ +│ • 분석 ┌───────┐┌───────┐ │ +│ • 프로필 │☑우리동네││☑링고비즈│ │ +│ │ TV ││ │ │ +│ │───────││───────│ │ +│ │반경: ││전화번호││ │ +│ │[500m▼]││자동연동││ │ +│ │시간: ││ │ │ +│ │[저녁▼]││연결음 ││ │ +│ │5만명 ││업데이트││ │ +│ │8만원 ││무료 ││ │ +│ └───────┘└───────┘ │ +│ │ +│ ┌───────┐┌───────┐ │ +│ │☐지니TV││☑ SNS │ │ +│ │ 광고 ││ │ │ +│ │───────││───────│ │ +│ │지역: ││☑Insta │ │ +│ │[수원▼]││☑Naver │ │ +│ │시간: ││☐Kakao │ │ +│ │[전체▼]││예약:즉시│ │ +│ │예산: ││무료 │ │ +│ │[입력...]││ │ │ +│ └───────┘└───────┘ │ +│ │ +│ ┌────────────────┐ │ +│ │총 예상 비용: 8만원│ │ +│ │총 예상 노출: 5만명+│ │ +│ └────────────────┘ │ +│ │ +│ ┌──────────┐ │ +│ │다음 단계 │ │ +│ └──────────┘ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **채널 선택**: 체크박스, 다중 선택 가능 +2. **옵션 설정**: 각 채널별 세부 옵션 입력 +3. **실시간 계산**: 선택/옵션 변경 시 비용/노출 수 자동 업데이트 +4. **다음 단계**: 최소 1개 선택 시 버튼 활성화, 최종 승인 화면 이동 + +**반응형 처리** +- Mobile: 카드 1열 세로 배치 +- Tablet: 카드 2열 배치 +- Desktop: 카드 2×2 그리드 배치 + +--- + +#### 12-최종승인 + +**개요** +- **목적**: 모든 설정 최종 확인 및 승인 +- **관련 유저스토리**: UFR-EVENT-050 +- **비즈니스 중요도**: M/13 (필수, 중간 복잡도) + +**주요 기능** +- 이벤트 정보 요약 (제목, 경품, 기간) +- SNS 이미지 미리보기 +- 배포 채널 목록 +- 수정 버튼 (각 섹션별 이전 단계 이동) +- 승인 및 배포 버튼 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 최종 승인 👤│ ← Header 추가 +├─────────────────────┤ +│ │ +│ ✓ 이벤트 정보 [수정]│ +│ ────────────────────│ +│ • 제목: 신규고객... │ +│ • 경품: 커피 쿠폰 │ +│ • 기간: 2025-11-01 ~ │ +│ 2025-11-15 │ +│ • 참여방법: 전화번호 │ +│ │ +│ ✓ SNS 이미지 [수정]│ +│ ────────────────────│ +│ ┌─────────────────┐ │ +│ │ [선택한 이미지] │ │ +│ │ (스타일 1: 심플) │ │ +│ └─────────────────┘ │ +│ │ +│ ✓ 배포 채널 [수정]│ +│ ────────────────────│ +│ • 우리동네TV (예상 5만명)│ +│ • 링고비즈 (예상 3만명) │ +│ • SNS (네이버, 카카오) │ +│ │ +│ ✓ 예상 성과 │ +│ ────────────────────│ +│ • 총 참여자: 150명 │ +│ • 총 비용: 30만원 │ +│ • 투자대비수익률: 450%│ +│ │ +│ ⚠️ 주의사항 │ +│ 배포 후에는 이벤트 제목,│ +│ 경품, 기간을 변경할 수 │ +│ 없습니다. │ +│ │ +│ ┌─────────────────┐ │ +│ │ 승인 및 배포 │ │ +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom 추가 +└─────────────────────┘ +``` + +**인터랙션** +1. **수정 버튼**: 각 섹션별 이전 단계 화면 이동 +2. **승인 확인**: "이벤트를 승인하고 배포하시겠습니까?" 다이얼로그 +3. **승인 완료**: + - 로딩 인디케이터 (배포 중...) + - 성공 토스트: "이벤트가 성공적으로 배포되었습니다" + - 대시보드 화면 이동 + 새 이벤트 카드 추가 + +**반응형 처리** +- Mobile: 전체 화면, 세로 스크롤 +- Tablet/Desktop: 중앙 카드 (최대 너비 800px), 좌우 여백 + +--- + +#### 13-이벤트상세 + +**개요** +- **목적**: 진행 중인 이벤트 상세 정보 조회 +- **관련 유저스토리**: UFR-EVENT-060 +- **비즈니스 중요도**: S/13 (권장, 중간 복잡도) + +**주요 기능** +- 이벤트 기본 정보 표시 +- 실시간 통계 (참여자, 노출수, 조회수, 공유수) +- 배포 채널 현황 +- 최근 참여자 10명 +- 이벤트 관리 액션 (수정, 종료) + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 이벤트 상세 👤│ ← Header +├─────────────────────┤ +│ 신규고객 유치 이벤트 │ +│ 진행중 | D-5 │ +│ │ +│ 📊 실시간 통계 │ +│ ┌────────┐┌────────┐│ +│ │참여자수 ││노출수 ││ +│ │ 128명 ││5.2만회 ││ +│ └────────┘└────────┘│ +│ ┌────────┐┌────────┐│ +│ │조회수 ││공유수 ││ +│ │ 3.8만회││ 156회 ││ +│ └────────┘└────────┘│ +│ │ +│ 📋 기본 정보 │ +│ 이벤트 기간: │ +│ 2025-11-01~11-15 │ +│ │ +│ 경품: │ +│ 커피 쿠폰 100매 │ +│ │ +│ 참여 방법: │ +│ 전화번호 입력 │ +│ │ +│ 📺 배포 채널 현황 │ +│ ✅ 우리동네TV │ +│ 노출: 4.8만회 │ +│ ✅ 링고비즈 │ +│ 연결음 업데이트됨 │ +│ ✅ SNS │ +│ Instagram: 220회 │ +│ Naver: 180회 │ +│ │ +│ 👥 최근 참여자 │ +│ • 김** (010-****-1234)│ +│ • 이** (010-****-5678)│ +│ • 박** (010-****-9012)│ +│ ... │ +│ [전체 참여자 보기 >] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 효과측정 대시보드│ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 이벤트 수정 │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 이벤트 종료 │ │ +│ └─────────────────┘ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필 │ ← Nav Bottom +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ ← 이벤트 상세 [프로필]│ +├─────────────────────────────────────┤ +│ [사이드바] 신규고객 유치 이벤트 │ +│ 진행중 | D-5 │ +│ • 대시보드 ─────────────────────────│ +│ • 이벤트 📊 실시간 통계 │ +│ • 분석 ┌──────┐┌──────┐┌──────┐│ +│ • 프로필 │참여자 ││노출수 ││조회수 ││ +│ │128명 ││5.2만회││3.8만회││ +│ └──────┘└──────┘└──────┘│ +│ ┌──────┐ │ +│ │공유수 │ │ +│ │156회 │ │ +│ └──────┘ │ +│ │ +│ ┌────────────┐┌──────────┐│ +│ │📋 기본 정보 ││📺 배포현황││ +│ │ ││ ││ +│ │기간: ││✅우리동네TV││ +│ │11/1~11/15 ││ 4.8만회 ││ +│ │ ││ ││ +│ │경품: ││✅링고비즈││ +│ │커피쿠폰100 ││ 업데이트││ +│ │ ││ ││ +│ │참여: ││✅SNS ││ +│ │전화번호 ││ Insta:220││ +│ │ ││ Naver:180││ +│ └────────────┘└──────────┘│ +│ │ +│ 👥 최근 참여자 [전체보기 >] │ +│ 김** (010-****-1234) │ +│ 이** (010-****-5678) │ +│ 박** (010-****-9012) │ +│ ... │ +│ │ +│ ┌──────┐┌──────┐┌──────┐ │ +│ │효과측정││수정 ││종료 │ │ +│ └──────┘└──────┘└──────┘ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **실시간 통계**: 5분 간격 자동 갱신 +2. **전체 참여자 보기**: 참여자 목록 화면으로 이동 +3. **효과측정 대시보드**: 실시간 대시보드 화면으로 이동 +4. **이벤트 수정**: 제한적 수정 가능 (배포 후 제목/경품 수정 불가) +5. **이벤트 종료**: 확인 다이얼로그 후 즉시 종료 + +**반응형 처리** +- Mobile: 섹션별 세로 배치 +- Tablet: 2열 배치 (기본정보 + 배포현황) +- Desktop: 그리드 배치 (통계 상단, 정보 2열, 참여자 하단) + +--- + +#### 14-참여자목록 + +**개요** +- **목적**: 이벤트 참여자 목록 조회 및 관리 +- **관련 유저스토리**: UFR-PART-020 +- **비즈니스 중요도**: S/8 (권장, 낮은 복잡도) + +**주요 기능** +- 참여자 테이블 (응모번호, 이름, 전화번호, 참여경로, 참여일시, 당첨여부) +- 참여 경로별 필터 +- 당첨 여부 필터 +- 이름/전화번호 검색 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 참여자 목록 👤│ ← Header +├─────────────────────┤ +│ [검색창] │ +│ 🔍 이름/전화번호... │ +│ │ +│ 📂 필터 │ +│ [전체경로▼] [전체▼] │ +│ │ +│ 총 128명 참여 │ +│ │ +│ ┌─────────────────┐ │ +│ │ #0001 │ │ +│ │ 김** │ │ +│ │ 010-****-1234 │ │ +│ │ SNS (Instagram) │ │ +│ │ 2025-11-02 14:23│ │ +│ │ 당첨 대기 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ #0002 │ │ +│ │ 이** │ │ +│ │ 010-****-5678 │ │ +│ │ 우리동네TV │ │ +│ │ 2025-11-02 15:45│ │ +│ │ 당첨 대기 │ │ +│ └─────────────────┘ │ +│ │ +│ ┌─────────────────┐ │ +│ │ #0003 │ │ +│ │ 박** │ │ +│ │ 010-****-9012 │ │ +│ │ 링고비즈 │ │ +│ │ 2025-11-02 16:12│ │ +│ │ 당첨 대기 │ │ +│ └─────────────────┘ │ +│ │ +│ [더보기...] │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필 │ ← Nav Bottom +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ ← 참여자 목록 [프로필]│ +├─────────────────────────────────────┤ +│ [사이드바] 🔍 [검색창] 📂 필터 │ +│ 이름/전화번호... [전체경로▼]│ +│ • 대시보드 [전체▼] │ +│ • 이벤트 총 128명 참여 │ +│ • 분석 ──────────────────────────│ +│ • 프로필 │번호│이름│전화번호│경로│일시│당첨│ +│ ├────┼──┼────┼──┼──┼──┤ +│ │0001│김**│010-****│SNS│11/2│대기│ +│ │ │ │-1234 │ │14:23│ │ +│ ├────┼──┼────┼──┼──┼──┤ +│ │0002│이**│010-****│우리│11/2│대기│ +│ │ │ │-5678 │동네│15:45│ │ +│ ├────┼──┼────┼──┼──┼──┤ +│ │0003│박**│010-****│링고│11/2│대기│ +│ │ │ │-9012 │비즈│16:12│ │ +│ ├────┼──┼────┼──┼──┼──┤ +│ │... │...│... │... │... │...│ +│ └────┴──┴────┴──┴──┴──┘ +│ │ +│ [1] 2 3 4 5 ... 7 > │ +│ │ +│ ┌──────────────┐ │ +│ │엑셀 다운로드 │ │ +│ └──────────────┘ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **검색**: 입력 중 실시간 검색 결과 업데이트 +2. **필터 적용**: 경로/당첨여부 선택 시 즉시 목록 갱신 +3. **카드/행 클릭**: 참여자 상세 정보 모달 표시 +4. **엑셀 다운로드**: 현재 필터 기준 참여자 목록 다운로드 +5. **페이지네이션**: 페이지당 20개 표시 + +**반응형 처리** +- Mobile: 카드 형태 1열 표시 +- Tablet: 카드 형태 2열 표시 +- Desktop: 테이블 형태 표시 + +--- + +### 5.4 참여자 관리 + +#### 15-이벤트참여 + +**개요** +- **목적**: 고객이 이벤트에 참여하는 화면 (모바일 최적화 최우선) +- **관련 유저스토리**: UFR-PART-010 +- **비즈니스 중요도**: M/13 (필수, 중간 복잡도) + +**주요 기능** +- 이벤트 정보 표시 (제목, 경품, 기간) +- SNS 이미지 표시 +- 참여 폼 (이름, 전화번호) +- 개인정보 동의 체크박스 +- 참여 완료 애니메이션 + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ │ +│ ┌─────────────────┐ │ +│ │ │ │ +│ │ [이벤트 이미지] │ │ +│ │ │ │ +│ └─────────────────┘ │ +│ │ +│ 🎉 신규고객 유치 이벤트│ +│ │ +│ 경품: ☕ 커피 쿠폰 │ +│ 기간: 2025-11-01 ~ │ +│ 2025-11-15 │ +│ │ +│ ────────────────────│ +│ │ +│ 참여하기 │ +│ │ +│ ┌─────────────────┐ │ +│ │ 이름 * │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │ 전화번호 * │ │ +│ │ 010-0000-0000 │ │ +│ └─────────────────┘ │ +│ │ +│ ☐ 개인정보 수집 및 │ +│ 이용에 동의합니다 │ +│ [전문보기] │ +│ │ +│ ┌─────────────────┐ │ +│ │ 참여하기 │ │ +│ └─────────────────┘ │ +│ │ +│ 참여자: 128명 │ +│ │ +└─────────────────────┘ +``` + +*참여 완료 상태* +``` +┌─────────────────────┐ +│ │ +│ ┌────┐ │ +│ │ ✓ │ │ +│ └────┘ │ +│ │ +│ 참여가 완료되었습니다! │ +│ │ +│ 홍길동님의 행운을 │ +│ 기원합니다! │ +│ │ +│ ────────────────── │ +│ │ +│ 당첨자 발표 │ +│ 2025-11-16 (월) │ +│ │ +│ ┌─────────────────┐│ +│ │ 확인 ││ +│ └─────────────────┘│ +│ │ +└─────────────────────┘ +``` + +**인터랙션** +1. **폼 검증**: + - 이름: 2자 이상 + - 전화번호: 010-0000-0000 형식 자동 포맷 + - 개인정보 동의: 필수 체크 +2. **전문보기**: Bottom Sheet로 개인정보 처리방침 표시 +3. **참여하기**: + - 중복 참여 체크 + - 로딩 (1초) + - 애니메이션 (체크 아이콘 + 축하 메시지) +4. **확인**: 참여 완료 화면 닫기 + +**반응형 처리** +- Mobile: 전체 화면 (최적화) +- Tablet: 중앙 카드 (최대 너비 480px) +- Desktop: 중앙 카드 (최대 너비 600px) + +**접근성** +- 이미지 alt 텍스트: "신규고객 유치 이벤트 포스터" +- 참여 버튼: aria-label="이벤트에 참여하기" +- 개인정보 동의: 필수 체크 aria-required="true" + +--- + +#### 16-당첨자추첨 + +**개요** +- **목적**: 공정한 당첨자 자동 선정 +- **관련 유저스토리**: UFR-PART-030 +- **비즈니스 중요도**: M/13 (필수, 중간 복잡도) + +**주요 기능** +- 당첨 인원 설정 +- 매장 방문 고객 가산점 옵션 +- 자동 추첨 실행 +- 당첨자 목록 표시 +- 재추첨 기능 +- 추첨 이력 보관 + +**UI 구성요소** + +**Mobile (320-767px)** +``` +┌─────────────────────────────────┐ +│ ☰ 당첨자 추첨 👤 │ Header +├─────────────────────────────────┤ +│ │ +│ 📋 이벤트 정보 │ +│ ┌─────────────────────────────┐ │ +│ │ 이벤트명: 신규고객 할인 이벤트 │ │ +│ │ 총 참여자: 127명 │ │ +│ │ 추첨 상태: 추첨 전 │ │ +│ └─────────────────────────────┘ │ +│ │ +│ 🎯 추첨 설정 │ +│ ┌─────────────────────────────┐ │ +│ │ 당첨 인원 │ │ +│ │ ┌───────┐ ┌──┐ ┌───────┐ │ │ +│ │ │ - │ │ 5 │ │ + │ │ │ +│ │ └───────┘ └──┘ └───────┘ │ │ +│ │ │ │ +│ │ ☑ 매장 방문 고객 가산점 │ │ +│ │ (가중치: 1.5배) │ │ +│ │ │ │ +│ │ ℹ️ 추첨 방식: 난수 기반 무작위 │ │ +│ │ 모든 추첨 과정은 자동 기록됩니다 │ │ +│ └─────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ 🎲 추첨 시작 │ │ Primary Action +│ └─────────────────────────────┘ │ +│ │ +│ 📜 추첨 이력 (최근 3건) │ +│ ┌─────────────────────────────┐ │ +│ │ 2024-01-15 14:30 │ │ +│ │ 당첨자 5명 | 👁️ 상세보기 │ │ +│ ├─────────────────────────────┤ │ +│ │ 2024-01-15 14:25 (재추첨) │ │ +│ │ 당첨자 5명 | 👁️ 상세보기 │ │ +│ └─────────────────────────────┘ │ +│ │ +├─────────────────────────────────┤ +│ 홈 이벤트 분석 프로필 │ Nav Bottom +└─────────────────────────────────┘ + +[추첨 후 화면] +┌─────────────────────────────────┐ +│ ☰ 당첨자 추첨 👤 │ +├─────────────────────────────────┤ +│ │ +│ 🎉 추첨 완료! │ +│ 총 127명 중 5명 당첨 │ +│ │ +│ 🏆 당첨자 목록 │ +│ ┌─────────────────────────────┐ │ +│ │ 1위 (응모번호: #00042) │ │ +│ │ 김** (010-****-1234) │ │ +│ │ 참여: 우리동네TV 🌟 │ │ +│ ├─────────────────────────────┤ │ +│ │ 2위 (응모번호: #00089) │ │ +│ │ 이** (010-****-5678) │ │ +│ │ 참여: SNS │ │ +│ ├─────────────────────────────┤ │ +│ │ 3위 (응모번호: #00103) │ │ +│ │ 박** (010-****-9012) │ │ +│ │ 참여: 링고비즈 🌟 │ │ +│ ├─────────────────────────────┤ │ +│ │ 4위 (응모번호: #00012) │ │ +│ │ 최** (010-****-3456) │ │ +│ │ 참여: SNS │ │ +│ ├─────────────────────────────┤ │ +│ │ 5위 (응모번호: #00067) │ │ +│ │ 정** (010-****-7890) │ │ +│ │ 참여: 우리동네TV │ │ +│ └─────────────────────────────┘ │ +│ │ +│ 🌟 매장 방문 고객 가산점 적용 │ +│ │ +│ ┌───────────┐ ┌───────────┐ │ +│ │ 📥 엑셀다운 │ │ 🔄 재추첨 │ │ +│ └───────────┘ └───────────┘ │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ 당첨자에게 알림 전송 │ │ Primary +│ └─────────────────────────────┘ │ +│ │ +├─────────────────────────────────┤ +│ 홈 이벤트 분석 프로필 │ +└─────────────────────────────────┘ +``` + +**Tablet (768-1023px)** +``` +┌────────────────────────────────────────────┐ +│ ☰ 당첨자 추첨 👤 │ +├────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ 📋 이벤트 정보 │ │ 🎯 추첨 설정 │ │ +│ │ │ │ │ │ +│ │ 이벤트명: │ │ 당첨 인원 │ │ +│ │ 신규고객 할인 │ │ ┌─┐ ┌─┐ ┌─┐ │ │ +│ │ │ │ │-│ │5│ │+│ │ │ +│ │ 총 참여자: │ │ └─┘ └─┘ └─┘ │ │ +│ │ 127명 │ │ │ │ +│ │ │ │ ☑ 매장 방문 │ │ +│ │ 추첨 상태: │ │ 가산점 1.5배 │ │ +│ │ 추첨 전 │ │ │ │ +│ │ │ │ ℹ️ 난수 기반 무작위 │ │ +│ │ │ │ 추첨 과정 자동 기록 │ │ +│ └──────────────────┘ │ │ │ +│ │ ┌──────────────┐ │ │ +│ │ │ 🎲 추첨 시작 │ │ │ +│ │ └──────────────┘ │ │ +│ └──────────────────┘ │ +│ │ +│ 📜 추첨 이력 │ +│ ┌──────────────────────────────────────┐ │ +│ │ 날짜 당첨자 상태 상세 │ │ +│ ├──────────────────────────────────────┤ │ +│ │ 2024-01-15 5명 완료 👁️ │ │ +│ │ 2024-01-15 5명 재추첨 👁️ │ │ +│ └──────────────────────────────────────┘ │ +│ │ +├────────────────────────────────────────────┤ +│ 홈 이벤트 분석 프로필 │ +└────────────────────────────────────────────┘ +``` + +**Desktop (1024px+)** +``` +┌──────────────────────────────────────────────────────────┐ +│ ☰ 당첨자 추첨 👤 │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────┐ ┌───────────────────────────┐ │ +│ │ 📋 이벤트 정보 │ │ 🎯 추첨 설정 │ │ +│ │ │ │ │ │ +│ │ 이벤트명: │ │ 당첨 인원 │ │ +│ │ 신규고객 할인 이벤트 │ │ ┌───┐ ┌────┐ ┌───┐ │ │ +│ │ │ │ │ - │ │ 5 │ │ + │ │ │ +│ │ 이벤트 기간: │ │ └───┘ └────┘ └───┘ │ │ +│ │ 2024-01-01 ~ 01-14 │ │ │ │ +│ │ │ │ ☑ 매장 방문 고객 가산점 │ │ +│ │ 총 참여자: 127명 │ │ (가중치: 1.5배) │ │ +│ │ - 우리동네TV: 45명 │ │ │ │ +│ │ - 링고비즈: 32명 │ │ ℹ️ 추첨 방식 │ │ +│ │ - 지니TV: 28명 │ │ • 난수 기반 무작위 │ │ +│ │ - SNS: 22명 │ │ • 추첨 과정 자동 기록 │ │ +│ │ │ │ • 공정성 보장 │ │ +│ │ 추첨 상태: 추첨 전 │ │ │ │ +│ │ │ │ ┌─────────────────────┐ │ │ +│ └───────────────────────┘ │ │ 🎲 추첨 시작 │ │ │ +│ │ └─────────────────────┘ │ │ +│ └───────────────────────────┘ │ +│ │ +│ 📜 추첨 이력 │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ 추첨 일시 당첨자 재추첨 추첨방식 상세 │ │ +│ ├────────────────────────────────────────────────────┤ │ +│ │ 2024-01-15 14:30 5명 - 무작위 👁️ │ │ +│ │ 2024-01-15 14:25 5명 Y 무작위 👁️ │ │ +│ │ 2024-01-15 14:20 5명 - 무작위 👁️ │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────┘ + +[추첨 후 화면 - Desktop] +┌──────────────────────────────────────────────────────────┐ +│ ☰ 당첨자 추첨 결과 👤 │ +├──────────────────────────────────────────────────────────┤ +│ │ +│ 🎉 추첨 완료! 총 127명 중 5명 당첨 │ +│ │ +│ 🏆 당첨자 목록 ┌──────────────────────┐ │ +│ ┌────────────────────────────┐ │ 📊 통계 │ │ +│ │ 순위 응모번호 이름 전화번호 참여경로│ │ │ │ +│ ├────────────────────────────┤ │ 채널별 당첨 분포: │ │ +│ │ 1위 #00042 김** 010-****-1234 우리동네TV 🌟│ │ 우리동네TV: 2명 │ │ +│ │ 2위 #00089 이** 010-****-5678 SNS │ │ 링고비즈: 1명 │ │ +│ │ 3위 #00103 박** 010-****-9012 링고비즈 🌟│ │ SNS: 2명 │ │ +│ │ 4위 #00012 최** 010-****-3456 SNS │ │ │ │ +│ │ 5위 #00067 정** 010-****-7890 우리동네TV │ │ 가산점 적용: 2명 │ │ +│ └────────────────────────────┘ │ │ │ +│ │ 추첨 일시: │ │ +│ 🌟 매장 방문 고객 가산점 적용 │ 2024-01-15 14:30 │ │ +│ └──────────────────────┘ │ +│ │ +│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ +│ │ 📥 엑셀다운 │ │ 🔄 재추첨 │ │ 당첨자에게 알림 전송 │ │ +│ └────────────┘ └────────────┘ └────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────┘ +``` + +**인터랙션** + +**추첨 전 설정** +- 당첨 인원: +/- 버튼으로 조정 (1~100명) +- 가산점 옵션: 체크박스 토글 +- 추첨 시작: 확인 다이얼로그 표시 → "총 127명 중 5명을 추첨하시겠습니까?" + +**추첨 실행** +- 로딩 애니메이션: 3초간 슬롯머신 효과 +- 진행 상황: "추첨 중... (난수 생성 중)" → "당첨자 선정 중..." → "완료!" +- 추첨 완료: 당첨자 목록 페이드인 애니메이션 + +**당첨자 목록** +- 순위별 하이라이트: 1위(금색), 2위(은색), 3위(동색) +- 가산점 적용 표시: 🌟 아이콘으로 매장 방문 고객 구분 +- 개인정보 마스킹: 이름(김**), 전화번호(010-****-1234) + +**재추첨** +- 재추첨 버튼: 경고 다이얼로그 표시 + - "재추첨 시 현재 당첨자 정보가 변경됩니다. 계속하시겠습니까?" + - "이전 추첨 이력은 보관됩니다" +- 확인 시: 새로운 추첨 실행, 이전 결과는 이력에 "(재추첨)" 표시 + +**알림 전송** +- 알림 전송 버튼: 확인 다이얼로그 표시 + - "5명의 당첨자에게 SMS 알림을 전송하시겠습니까?" + - "예상 비용: 500원 (100원/건)" +- 전송 완료: 토스트 메시지 "알림이 전송되었습니다" + +**엑셀 다운로드** +- 파일명: `당첨자목록_[이벤트명]_[추첨일시].xlsx` +- 포함 정보: 순위, 응모번호, 이름, 전화번호, 참여경로, 가산점여부, 추첨일시 + +**추첨 이력 상세보기** +- 클릭 시: 모달로 해당 추첨의 당첨자 목록 표시 +- 재추첨 여부, 추첨 설정 정보 포함 + +**반응형 처리** + +**Mobile (320-767px)** +- 1열 레이아웃: 이벤트 정보 → 추첨 설정 → 추첨 버튼 → 이력 +- 당첨자 목록: 카드형 (1명씩) +- 추첨 설정: 전체 너비 사용 +- 버튼: 스택형 배치 (세로 정렬) + +**Tablet (768-1023px)** +- 2열 레이아웃: 이벤트 정보 | 추첨 설정 +- 당첨자 목록: 테이블 형식 (간소화) +- 추첨 이력: 테이블 형식 +- 버튼: 수평 배치 + +**Desktop (1024px+)** +- 최적화된 테이블 레이아웃 +- 당첨자 목록: 전체 정보 표시 +- 우측에 통계 패널 추가 +- 추첨 이력: 전체 정보 표시 +- 버튼: 수평 배치, 우측 정렬 + +**터치/마우스 최적화** +- Mobile: 터치 영역 최소 44px +- Desktop: 호버 효과, 툴팁 표시 +- 재추첨/알림 버튼: 경고색 (주의 필요) + +**성능 최적화** +- 추첨 실행: 서버 사이드 처리 (공정성 보장) +- 로딩 상태: 스켈레톤 UI 표시 +- 당첨자 목록: 페이징 (50명 이상 시) +- 추첨 이력: 최근 10건만 표시, "더보기" 옵션 + +**접근성** +- 추첨 시작 버튼: aria-label="추첨 시작, 127명 중 5명 선정" +- 재추첨 버튼: aria-label="재추첨, 주의 필요" +- 당첨자 정보: 스크린리더용 전체 정보 제공 +- 키보드 네비게이션: Tab, Enter로 모든 기능 접근 가능 + +--- + +### 5.5 성과 분석 + +#### 17-실시간대시보드 + +**개요** +- **목적**: 이벤트 성과 실시간 측정 및 분석 +- **관련 유저스토리**: UFR-ANAL-010 +- **비즈니스 중요도**: M/34 (필수, 최고 복잡도) + +**주요 기능** +- 요약 카드 4개 (참여자, 비용, 수익, 투자대비수익률) +- 채널별 성과 분석 (파이 차트) +- 시간대별 참여 추이 (라인 차트) +- 투자대비수익률 상세 분석 (테이블) +- 참여자 프로필 분석 (막대 차트) +- 실시간 업데이트 (5분 간격) + +**UI 구성요소** + +*Mobile (320-767px)* +``` +┌─────────────────────┐ +│ ☰ 실시간 대시보드 👤│ ← Header 추가 +├─────────────────────┤ +│ │ +│ 📊 요약 (실시간) │ +│ ┌────────┐┌────────┐│ +│ │참여자수 ││총 비용 ││ +│ │ 128명 ││ 30만원 ││ +│ └────────┘└────────┘│ +│ ┌────────┐┌────────┐│ +│ │예상수익 ││투자대비││ +│ │ 135만원││수익률 ││ +│ │ ││ 450% ││ +│ └────────┘└────────┘│ +│ │ +│ 📺 채널별 성과 │ +│ ┌─────────────────┐ │ +│ │ [파이 차트] │ │ +│ │ 우리동네TV: 45% │ │ +│ │ 링고비즈: 30% │ │ +│ │ SNS: 25% │ │ +│ └─────────────────┘ │ +│ │ +│ 📈 시간대별 참여 추이 │ +│ ┌─────────────────┐ │ +│ │ [라인 차트] │ │ +│ │ 피크: 오후 2-4시 │ │ +│ └─────────────────┘ │ +│ │ +│ 💰 투자대비수익률 상세│ +│ ┌─────────────────┐ │ +│ │총 비용: 30만원 │ │ +│ │• 경품: 25만원 │ │ +│ │• 채널: 5만원 │ │ +│ │ │ │ +│ │예상 수익: 135만원│ │ +│ │• 매출 증가: 100만│ │ +│ │• 신규 LTV: 35만 │ │ +│ │ │ │ +│ │투자대비수익률: │ │ +│ │(135-30)/30×100 │ │ +│ │= 450% │ │ +│ └─────────────────┘ │ +│ │ +│ 👥 참여자 프로필 │ +│ ┌─────────────────┐ │ +│ │[막대 차트] │ │ +│ │연령: 20대 35% │ │ +│ │ 30대 40% │ │ +│ │ 40대 25% │ │ +│ │성별: 여 60% │ │ +│ │ 남 40% │ │ +│ └─────────────────┘ │ +│ │ +├─────────────────────┤ +│ 홈 이벤트 분석 프로필│ ← Nav Bottom 추가 +└─────────────────────┘ +``` + +*Desktop (1024px+)* +``` +┌─────────────────────────────────────┐ +│ ← 실시간 대시보드 │ +├─────────────────────────────────────┤ +│ 📊 요약 (실시간 업데이트) │ +│ ┌─────┐┌─────┐┌─────┐┌─────┐ │ +│ │참여자││비용 ││수익 ││투자대││ │ +│ │128명││30만 ││135만││비수익││ │ +│ │ ││ ││ ││률450%││ │ +│ └─────┘└─────┘└─────┘└─────┘ │ +│ │ +│ ┌──────────────────┐┌──────────────┐│ +│ │📺 채널별 성과 ││📈 시간대별 추이││ +│ │ ││ ││ +│ │ [파이 차트] ││ [라인 차트] ││ +│ │ ││ ││ +│ │ • 우리동네TV 45% ││ 피크: 오후2-4 ││ +│ │ • 링고비즈 30% ││ ││ +│ │ • SNS 25% ││ ││ +│ └──────────────────┘└──────────────┘│ +│ │ +│ ┌──────────────────┐┌──────────────┐│ +│ │💰 투자대비수익률 ││👥 참여자 프로필││ +│ │ ││ ││ +│ │ [상세 테이블] ││ [차트] ││ +│ │ ││ ││ +│ └──────────────────┘└──────────────┘│ +│ │ +└─────────────────────────────────────┘ +``` + +**인터랙션** +1. **실시간 업데이트**: + - 5분 간격 자동 폴링 + - 변경 사항 하이라이트 (숫자 카운터 애니메이션) +2. **차트 인터랙션**: + - 파이 차트: 호버/클릭 시 상세 정보 툴팁 + - 라인 차트: 호버 시 시간대별 정확한 수치 표시 +3. **새로고침**: Pull to Refresh (모바일) +4. **상세보기**: 각 섹션 클릭 시 확장/축소 + +**반응형 처리** +- Mobile: 세로 스크롤, 카드 1열, 단순한 차트 (막대/파이) +- Tablet: 카드 2열, 차트 확장 +- Desktop: 카드 4열, 복잡한 차트 (라인/영역), 2열 레이아웃 + +--- + +## 6. 화면 간 전환 및 네비게이션 + +### 6.1 네비게이션 구조 + +#### Mobile (Bottom Navigation) +``` +┌─────────────────────┐ +│ │ +│ [화면 콘텐츠] │ +│ │ +├─────────────────────┤ +│[홈][이벤트][분석][프로필]│ +└─────────────────────┘ +``` + +**메뉴 항목**: +- **홈**: 대시보드 (05-대시보드) +- **이벤트**: 이벤트 목록 (06-이벤트목록) +- **분석**: 실시간 대시보드 (17-실시간대시보드) +- **프로필**: 프로필 편집 (03-프로필편집) + +#### Desktop (Side Navigation) +``` +┌─────────────────────────────────────┐ +│ [로고] [프로필] │ +├──────┬──────────────────────────────┤ +│ 메뉴 │ [화면 콘텐츠] │ +│ │ │ +│• 홈 │ │ +│• 이벤트│ │ +│• 분석 │ │ +│• 프로필│ │ +│ │ │ +└──────┴──────────────────────────────┘ +``` + +### 6.2 화면 전환 애니메이션 + +**Forward Navigation** (다음 단계): +- Slide Left (왼쪽으로 슬라이드) +- Duration: 300ms +- Easing: ease-out + +**Backward Navigation** (이전 단계): +- Slide Right (오른쪽으로 슬라이드) +- Duration: 300ms +- Easing: ease-out + +**Tab Navigation** (탭 전환): +- Fade In/Out +- Duration: 200ms +- Easing: ease-in-out + +**Modal/Dialog**: +- Fade In + Scale Up (등장) +- Fade Out + Scale Down (퇴장) +- Duration: 250ms +- Easing: cubic-bezier(0.4, 0, 0.2, 1) + +### 6.3 Floating Action Button (FAB) + +**위치**: 우측 하단 +**기능**: "새 이벤트 만들기" +**동작**: 07-이벤트목적선택 화면 이동 + +``` + ┌─────┐ + │ + │ + └─────┘ +``` + +--- + +## 7. 반응형 설계 전략 + +### 7.1 브레이크포인트 + +```css +/* Mobile First Breakpoints */ + +/* Mobile: 320px ~ 767px (기본 스타일) */ +@media (min-width: 768px) { + /* Tablet: 768px ~ 1023px */ +} + +@media (min-width: 1024px) { + /* Desktop: 1024px ~ 1439px */ +} + +@media (min-width: 1440px) { + /* Large Desktop: 1440px ~ */ + /* 최대 너비 1280px, 중앙 정렬 */ +} +``` + +### 7.2 레이아웃 전략 + +#### Grid System + +**Mobile**: +- 1열 레이아웃 +- 좌우 여백: 16px +- 요소 간 간격: 16px + +**Tablet**: +- 2열 레이아웃 (50:50 또는 30:70) +- 좌우 여백: 24px +- 요소 간 간격: 24px + +**Desktop**: +- 3열 레이아웃 (33:33:33 또는 20:60:20) +- 좌우 여백: 32px +- 요소 간 간격: 32px +- 최대 너비: 1280px (중앙 정렬) + +### 7.3 타이포그래피 + +```css +/* Mobile */ +h1: 24px / 1.2 / bold +h2: 20px / 1.3 / bold +h3: 18px / 1.4 / semibold +body: 14px / 1.5 / regular +small: 12px / 1.5 / regular + +/* Desktop */ +h1: 32px / 1.2 / bold +h2: 28px / 1.3 / bold +h3: 24px / 1.4 / semibold +body: 16px / 1.5 / regular +small: 14px / 1.5 / regular +``` + +### 7.4 터치 영역 + +**Mobile**: +- 최소 터치 영역: 44x44px (애플 권장) +- 버튼 높이: 48px +- 입력 필드 높이: 48px + +**Desktop**: +- 최소 클릭 영역: 32x32px +- 버튼 높이: 40px +- 입력 필드 높이: 40px + +--- + +## 8. 접근성 보장 방안 + +### 8.1 WCAG 2.1 AA 준수 + +**색상 대비**: +- 일반 텍스트: 4.5:1 +- 큰 텍스트 (18px 이상): 3:1 +- UI 컴포넌트: 3:1 + +**키보드 네비게이션**: +- 모든 기능 Tab 키로 접근 가능 +- Focus Indicator 명확히 표시 (2px 파란색 테두리) +- Escape 키로 모달/다이얼로그 닫기 + +**스크린 리더**: +- 모든 이미지 alt 텍스트 제공 +- 폼 Label과 Input 연결 (for/id) +- ARIA 속성 활용: + - aria-label + - aria-describedby + - aria-live (동적 콘텐츠) + - aria-required (필수 입력) + +### 8.2 접근성 체크리스트 + +**이미지**: +- [ ] 모든 이미지 alt 속성 제공 +- [ ] 장식용 이미지 alt="" 처리 + +**폼**: +- [ ] Label과 Input 연결 +- [ ] 필수 필드 표시 (*, aria-required) +- [ ] 오류 메시지 스크린 리더 읽기 + +**네비게이션**: +- [ ] 키보드만으로 모든 기능 접근 가능 +- [ ] Focus 순서 논리적 +- [ ] Skip Navigation 링크 제공 + +**콘텐츠**: +- [ ] 제목 계층 구조 (h1 > h2 > h3) +- [ ] 색상만으로 정보 전달하지 않음 +- [ ] 충분한 색상 대비 + +--- + +## 9. 성능 최적화 방안 + +### 9.1 이미지 최적화 + +**Lazy Loading**: +```html +이벤트 포스터 +``` + +**Responsive Images**: +```html +이벤트 포스터 +``` + +**WebP 포맷**: +- 압축률 30% 향상 +- Fallback: JPEG/PNG + +**이미지 CDN**: +- Cloudflare/AWS CloudFront +- 자동 리사이즈 및 포맷 변환 + +### 9.2 코드 최적화 + +**Code Splitting**: +```javascript +// 페이지별 번들 분리 +const Dashboard = lazy(() => import('./Dashboard')); +const EventCreate = lazy(() => import('./EventCreate')); +``` + +**Tree Shaking**: +- 불필요한 코드 제거 +- ES6 Module 사용 + +**Minification**: +- JS/CSS 압축 +- Gzip/Brotli 압축 + +### 9.3 로딩 전략 + +**Skeleton Screen**: +``` +┌─────────────────────┐ +│ ┌─────────────────┐ │ +│ │░░░░░░░░░░░░░░░░ │ │ ← Skeleton Card +│ │░░░░░░░░░░░░░░░░ │ │ +│ └─────────────────┘ │ +│ ┌─────────────────┐ │ +│ │░░░░░░░░░░░░░░░░ │ │ +│ └─────────────────┘ │ +└─────────────────────┘ +``` + +**Critical CSS**: +- Above the Fold CSS 인라인 삽입 +- 나머지 CSS 비동기 로드 + +**Defer/Async Script**: +```html + + +``` + +### 9.4 데이터 최적화 + +**Pagination**: +- 목록은 20개씩 페이징 +- Infinite Scroll (모바일) + +**Debouncing**: +```javascript +// 검색 입력 시 500ms 대기 후 API 호출 +const debouncedSearch = debounce(search, 500); +``` + +**LocalStorage**: +- 임시 저장 (이벤트 생성 진행 상태) +- 오프라인 대응 + +### 9.5 성능 목표 + +**Core Web Vitals**: +- LCP (Largest Contentful Paint): < 2.5s +- FID (First Input Delay): < 100ms +- CLS (Cumulative Layout Shift): < 0.1 + +**추가 지표**: +- First Contentful Paint: < 1.5s +- Time to Interactive: < 3s +- Lighthouse Score: > 90 +- 이미지 로딩: < 1s (per image) +- API 응답: < 500ms (평균) + +--- + +## 10. 변경 이력 + +| 버전 | 날짜 | 변경 내용 | 작성자 | +|------|------|----------|--------| +| 1.0 | 2025-10-21 | 최초 작성 | UI/UX Designer | + +--- + +## 부록 + +### A. 디자인 시스템 (추후 작성 예정) +- 컬러 팔레트 +- 타이포그래피 시스템 +- 스페이싱 시스템 +- 아이콘 라이브러리 +- 컴포넌트 라이브러리 + +### B. 프로토타입 링크 (추후 추가 예정) +- Figma 프로토타입 링크 +- HTML 프로토타입 링크 + +### C. 사용성 테스트 결과 (추후 추가 예정) +- 테스트 참가자 정보 +- 주요 발견 사항 +- 개선 권장 사항 + +--- + +**문서 끝** diff --git a/design/userstory-table.md b/design/userstory-table.md new file mode 100644 index 0000000..76e8930 --- /dev/null +++ b/design/userstory-table.md @@ -0,0 +1,344 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 유저스토리 목록 + +## 전체 유저스토리 요약 + +| 서비스 | ID | 유저스토리 | 우선순위 | 복잡도 | +|--------|-----|-----------|----------|--------| +| **User** | UFR-USER-010 | [회원가입] 소상공인으로서 간편하게 회원가입하고 싶다 | M | 21 | +| | UFR-USER-020 | [로그인] 소상공인으로서 간편하게 로그인하고 싶다 | M | 8 | +| | UFR-USER-030 | [프로필관리] 소상공인으로서 프로필 정보를 편집하고 싶다 | C | 8 | +| | UFR-USER-040 | [로그아웃] 소상공인으로서 안전하게 로그아웃하고 싶다 | S | 3 | +| **Event** | UFR-EVENT-010 | [대시보드] 소상공인으로서 대시보드에서 이벤트를 한눈에 확인하고 싶다 | S | 13 | +| | UFR-EVENT-020 | [이벤트목적선택] 소상공인으로서 이벤트 목적을 먼저 선택하고 싶다 | M | 5 | +| | UFR-EVENT-030 | [AI이벤트추천] 소상공인으로서 AI 트렌드 분석 결과와 함께 3가지 이벤트 추천을 확인하고 싶다 | M | 34 | +| | UFR-EVENT-040 | [배포채널선택] 소상공인으로서 다중 배포 채널을 선택하고 싶다 | M | 13 | +| | UFR-EVENT-050 | [최종승인] 소상공인으로서 모든 설정을 최종 확인하고 승인하고 싶다 | M | 13 | +| | UFR-EVENT-060 | [이벤트상세조회] 소상공인으로서 이벤트 상세 정보를 조회하고 싶다 | S | 13 | +| | UFR-EVENT-070 | [이벤트목록관리] 소상공인으로서 전체 이벤트 목록을 조회하고 필터링/검색하고 싶다 | S | 13 | +| **AI** | UFR-AI-010 | [AI트렌드분석및이벤트추천] AI 시스템으로서 업종/지역/시즌 트렌드를 분석하고 3가지 최적화된 이벤트 기획안을 생성하고 싶다 | M | 34 | +| **Content** | UFR-CONT-010 | [SNS이미지생성] 소상공인으로서 AI가 자동으로 3가지 스타일의 이미지를 생성하기를 원한다 | M | 21 | +| | UFR-CONT-020 | [콘텐츠편집] 소상공인으로서 생성된 이미지를 간단한 편집 기능으로 조정하고 싶다 | S | 13 | +| **Distribution** | UFR-DIST-010 | [다중채널배포] 소상공인으로서 원클릭으로 다중 채널 배포를 실행하고 싶다 | M | 21 | +| | UFR-DIST-020 | [배포상태모니터링] 소상공인으로서 배포 상태를 실시간으로 모니터링하고 싶다 | S | 13 | +| **Participation** | UFR-PART-010 | [이벤트참여] 고객으로서 최소한의 정보만 입력하고 이벤트에 참여하고 싶다 | M | 13 | +| | UFR-PART-020 | [참여자목록조회] 소상공인으로서 참여자 목록을 조회하고 싶다 | S | 8 | +| | UFR-PART-030 | [당첨자추첨] 소상공인으로서 자동 추첨 기능을 사용하고 싶다 | M | 13 | +| **Analytics** | UFR-ANAL-010 | [실시간통합대시보드] 소상공인으로서 모든 지표가 통합된 실시간 대시보드를 보고 싶다 | M | 34 | + +--- + +## 우선순위별 통계 + +| 우선순위 | 개수 | 비율 | +|---------|------|------| +| M (필수) | 13 | 65.0% | +| S (선택) | 6 | 30.0% | +| C (조건부) | 1 | 5.0% | +| **총계** | **20** | **100%** | + +--- + +## 서비스별 통계 + +| 서비스 | 유저스토리 수 | 평균 복잡도 | 필수(M) | 선택(S) | 조건부(C) | +|--------|-------------|-----------|---------|---------|----------| +| User | 4 | 10.0 | 2 | 1 | 1 | +| Event | 7 | 14.9 | 4 | 3 | 0 | +| AI | 1 | 34.0 | 1 | 0 | 0 | +| Content | 2 | 17.0 | 1 | 1 | 0 | +| Distribution | 2 | 17.0 | 1 | 1 | 0 | +| Participation | 3 | 11.3 | 2 | 1 | 0 | +| Analytics | 1 | 34.0 | 1 | 0 | 0 | +| **총계** | **20** | **16.9** | **12** | **7** | **1** | + +--- + +## 복잡도별 통계 + +| 복잡도 범위 | 개수 | 유저스토리 | +|-----------|------|-----------| +| 1-5 (낮음) | 2 | UFR-USER-040, UFR-EVENT-020 | +| 6-13 (중간) | 11 | UFR-USER-020, UFR-USER-030, UFR-EVENT-010, UFR-EVENT-040, UFR-EVENT-050, UFR-EVENT-060, UFR-EVENT-070, UFR-CONT-020, UFR-DIST-020, UFR-PART-010, UFR-PART-020, UFR-PART-030 | +| 14-21 (높음) | 4 | UFR-USER-010, UFR-CONT-010, UFR-DIST-010 | +| 22+ (매우 높음) | 3 | UFR-EVENT-030, UFR-AI-010, UFR-ANAL-010 | + +--- + +## 주요 기능별 상세 목록 + +### 1. User 서비스 (회원 인증 및 매장 정보 관리) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | +|----|------|----------|--------|-------------| +| UFR-USER-010 | 회원가입 | M | 21 | 이름, 전화번호, 이메일, 비밀번호, 매장명, 업종, 주소, 사업자번호 검증 | +| UFR-USER-020 | 로그인 | M | 8 | 전화번호/비밀번호 입력, 로그인 유지 옵션, 대시보드 이동 | +| UFR-USER-030 | 프로필관리 | C | 8 | 기본 정보, 매장 정보, 비밀번호 변경 | +| UFR-USER-040 | 로그아웃 | S | 3 | 안전한 세션 종료, 확인 다이얼로그 | + +**검증 로직:** +- 사업자번호 형식 검증 (XXX-XX-XXXXX) +- 사업자번호 유효성 확인 및 휴폐업 여부 확인 (국세청 API) +- 매장명과 사업자 정보 일치 확인 + +**주요 흐름:** +1. UFR-USER-020 (로그인) → UFR-EVENT-010 (대시보드) +2. 신규 사용자: 회원가입(UFR-USER-010) → 대시보드(UFR-EVENT-010) +3. 대시보드에서 "새 이벤트 만들기" → UFR-EVENT-020 + +--- + +### 2. Event 서비스 (이벤트 관리) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | +|----|------|----------|--------|-------------| +| UFR-EVENT-010 | 대시보드 | S | 13 | 진행중/예정/종료 이벤트 목록, 통계, 새 이벤트 만들기 버튼 | +| UFR-EVENT-020 | 이벤트목적선택 | M | 5 | 신규고객 유치/재방문 유도/매출 증대/인지도 향상 선택 | +| UFR-EVENT-030 | AI이벤트추천 | M | 34 | 트렌드 분석 결과, 3가지 이벤트 추천, 제목/경품 간단 수정 | +| UFR-EVENT-040 | 배포채널선택 | M | 13 | 우리동네TV, 링고비즈, 지니TV, SNS 선택, 예상 노출 수 | +| UFR-EVENT-050 | 최종승인 | M | 13 | 이벤트 정보 확인, 승인 처리, 배포 시작 | +| UFR-EVENT-060 | 이벤트상세조회 | S | 13 | 기본 정보, 실시간 통계, 배포 현황, 참여자 목록 | +| UFR-EVENT-070 | 이벤트목록관리 | S | 13 | 전체 이벤트 목록, 필터링, 검색, 정렬 | + +**이벤트 생성 플로우:** +1. 이벤트 목적 선택 (UFR-EVENT-020) +2. AI 트렌드 분석 및 이벤트 추천 (UFR-EVENT-030) +3. 콘텐츠 생성 (UFR-CONT-010) +4. 배포채널 선택 (UFR-EVENT-040) +5. 최종승인 (UFR-EVENT-050) + +--- + +### 3. AI 서비스 (AI 분석 및 추천) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | AI 모델 | +|----|------|----------|--------|-------------|---------| +| UFR-AI-010 | AI트렌드분석및이벤트추천 | M | 34 | 업종/지역/시즌 트렌드 분석, 3가지 이벤트 기획안 생성, 10초 이내 완료 | Claude API / GPT-4 API | + +**분석 항목:** +- 업종 트렌드: 최근 성공 이벤트, 고객 선호 경품, 효과적인 참여 방법 +- 지역 특성: 지역별 이벤트 성공률, 고객 특성 +- 시즌 특성: 계절/시기별 추천 이벤트 + +**추천 기준:** +- 옵션 1: 저비용, 높은 참여율 중심 +- 옵션 2: 중비용, 균형잡힌 투자 대비 수익률 +- 옵션 3: 고비용, 높은 매출 증대 효과 + +**성능 목표:** 전체 기획 과정 10초 이내 완료 (병렬 처리) + +--- + +### 4. Content 서비스 (콘텐츠 생성) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | AI 모델 | +|----|------|----------|--------|-------------|---------| +| UFR-CONT-010 | SNS이미지생성 | M | 21 | 3가지 스타일 (심플/화려/트렌디), 플랫폼별 최적화 | Stable Diffusion / DALL-E | +| UFR-CONT-020 | 콘텐츠편집 | S | 13 | 텍스트 수정, 색상 조정, 로고 위치 변경 | - | + +**플랫폼별 최적화:** +- Instagram: 1080x1080 +- Naver Blog: 800x600 +- Kakao Channel: 800x800 + +**성능 목표:** +- 이미지 생성: 30초 이내 +- 실시간 미리보기 제공 + +--- + +### 5. Distribution 서비스 (다중 채널 배포) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | 연동 API | +|----|------|----------|--------|-------------|---------| +| UFR-DIST-010 | 다중채널배포 | M | 21 | 우리동네TV, 링고비즈, 지니TV, SNS 동시 배포, 1분 이내 완료 | 우리동네TV, 링고비즈, 지니TV, SNS APIs | +| UFR-DIST-020 | 배포상태모니터링 | S | 13 | 채널별 배포 상태, 진행률, 오류 메시지, 재시도 기능 | - | + +**배포 채널:** +1. 우리동네TV (반경 500m/1km, 송출 시간대) +2. 링고비즈 (매장 전화 연결음 업데이트) +3. 지니TV (타겟 지역, 노출 시간대, 예산) +4. SNS (Instagram, Naver Blog, Kakao Channel) + +**성능 목표:** 전체 배포 과정 1분 이내 완료 + +--- + +### 6. Participation 서비스 (이벤트 참여 및 접수 관리) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | +|----|------|----------|--------|-------------| +| UFR-PART-010 | 이벤트참여 | M | 13 | 이름, 전화번호 입력, 참여 경로 추적, 응모번호 발급, 중복 방지 | +| UFR-PART-020 | 참여자목록조회 | S | 8 | 참여자 테이블, 필터링, 검색, 개인정보 마스킹 | +| UFR-PART-030 | 당첨자추첨 | M | 13 | 자동 추첨, 매장 방문 고객 가산점, 재추첨 기능 | + +**정책:** +- 1인 1회 참여 제한 (전화번호 기반) +- 개인정보 보호 규정 준수 +- 매장 방문 고객 가산점 부여 (선택 가능) + +--- + +### 7. Analytics 서비스 (실시간 효과 측정 및 분석) + +| ID | 기능 | 우선순위 | 복잡도 | 핵심 요구사항 | 데이터 소스 | +|----|------|----------|--------|-------------|------------| +| UFR-ANAL-010 | 실시간통합대시보드 | M | 34 | 참여자 수, 노출 수, 투자 대비 수익률, 매출 증가율, 채널별 성과, 5분 간격 업데이트 | KT 채널 API, POS, SNS API | + +**대시보드 구성:** +1. 상단 요약 카드: 총 참여자 수, 총 노출 수, 예상 투자 대비 수익률, 매출 증가율 +2. 채널별 성과 분석: 막대 그래프, 원 그래프 +3. 시간대별 참여 추이: 라인 차트 +4. 투자 대비 수익률 상세 분석: 총 비용, 예상 수익, 손익분기점 +5. 참여자 프로필 분석: 연령대별, 성별, 지역별 분포 +6. 비교 분석: 업종 평균, 이전 이벤트 대비 + +**데이터 수집:** +- 실시간 데이터 수집 (5분 간격) +- WebSocket 또는 SSE를 통한 실시간 업데이트 +- 데이터 캐싱 (Redis) + +--- + +## 기술 스택 요약 + +### AI/ML 모델 +| 모델 | 용도 | 관련 유저스토리 | +|------|------|----------------| +| Claude API / GPT-4 API | 트렌드 분석, 이벤트 추천, 홍보 문구 생성 | UFR-AI-010 | +| Stable Diffusion / DALL-E | 이미지 생성 | UFR-CONT-010 | + +### 외부 API 연동 +| API | 용도 | 관련 유저스토리 | +|-----|------|----------------| +| 국세청 API | 사업자번호 검증 | UFR-USER-010 | +| 우리동네TV API | 지역 타겟팅 영상 송출 | UFR-DIST-010 | +| 링고비즈 API | 연결음 업데이트 | UFR-DIST-010 | +| 지니TV API | TV 광고 배포 | UFR-DIST-010 | +| Instagram API | SNS 자동 포스팅, 성과 데이터 수집 | UFR-DIST-010, UFR-ANAL-010 | +| Naver API | 블로그 자동 포스팅 | UFR-DIST-010 | +| Kakao API | 카카오 채널 자동 포스팅, 성과 데이터 수집 | UFR-DIST-010, UFR-ANAL-010 | + +--- + +## 마이크로서비스 아키텍처 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ API Gateway │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ +┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ +│ User Service │ │ Event Service │ │ AI Service │ +│ (UFR-USER) │ │ (UFR-EVENT) │ │ (UFR-AI) │ +└────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + │ │ │ +┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐ +│ Content │ │ Distribution │ │ Participation │ +│ (UFR-CONT) │ │ (UFR-DIST) │ │ (UFR-PART) │ +└────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └─────────────────────┼─────────────────────┘ + │ + ┌─────────▼─────────┐ + │ Analytics │ + │ (UFR-ANAL) │ + └───────────────────┘ +``` + +--- + +## 서비스 간 의존성 + +- User → Event: 사용자 인증 정보 제공 +- Event → AI: 트렌드 분석 및 이벤트 추천 요청 +- Event → Content: 콘텐츠 생성 요청 +- Event → Distribution: 배포 요청 +- Distribution → Content: 생성된 콘텐츠 사용 +- Participation → Analytics: 참여자 데이터 제공 +- Distribution → Analytics: 노출 데이터 제공 + +--- + +## 개발 우선순위 로드맵 + +### Phase 1: MVP (8주) +**목표:** 기본 이벤트 기획 및 배포 기능 + +| 순서 | 서비스 | 유저스토리 | 비고 | +|------|--------|-----------|------| +| 1 | User | UFR-USER-010, UFR-USER-020 | 회원가입, 로그인 | +| 2 | Event | UFR-EVENT-020, UFR-EVENT-030, UFR-EVENT-050 | 목적 선택, AI 추천, 승인 | +| 3 | AI | UFR-AI-010 | 트렌드 분석 및 이벤트 추천 | +| 4 | Content | UFR-CONT-010 | 이미지 생성 | +| 5 | Distribution | UFR-DIST-010 | 다중 채널 배포 | +| 6 | Participation | UFR-PART-010, UFR-PART-030 | 참여 신청, 자동 추첨 | + +### Phase 2: 고도화 (6주) +**목표:** 관리 및 분석 기능 추가 + +| 순서 | 서비스 | 유저스토리 | 비고 | +|------|--------|-----------|------| +| 7 | Event | UFR-EVENT-010, UFR-EVENT-040, UFR-EVENT-060, UFR-EVENT-070 | 대시보드, 채널 선택, 상세 조회, 목록 관리 | +| 8 | Analytics | UFR-ANAL-010 | 실시간 통합 대시보드 | +| 9 | Distribution | UFR-DIST-020 | 배포 상태 모니터링 | +| 10 | Participation | UFR-PART-020 | 참여자 목록 조회 | + +### Phase 3: 완성 (2주) +**목표:** 선택 기능 추가 + +| 순서 | 서비스 | 유저스토리 | 비고 | +|------|--------|-----------|------| +| 11 | User | UFR-USER-030, UFR-USER-040 | 프로필 관리, 로그아웃 | +| 12 | Content | UFR-CONT-020 | 콘텐츠 편집 | + +--- + +## 주요 기술적 리스크 및 해결방안 + +### 1. AI 응답 시간 (10초 목표) +- **리스크**: AI API 응답이 목표 시간을 초과할 수 있음 +- **해결방안**: + - 프롬프트 최적화로 응답 시간 단축 + - 트렌드 분석과 이벤트 추천 병렬 처리 + - 비동기 처리 + 폴링 방식 + - 부분 응답 스트리밍 + +### 2. 다중 외부 API 의존성 +- **리스크**: API 장애 시 서비스 중단 +- **해결방안**: + - 각 API별 폴백 전략 수립 + - 캐싱 적극 활용 + - 써킷 브레이커 패턴 적용 + - 채널별 독립 처리 + +### 3. 실시간 대시보드 성능 +- **리스크**: 다중 데이터 소스 통합으로 인한 성능 저하 +- **해결방안**: + - Redis 캐싱 + - 5분 간격 데이터 폴링 + - WebSocket/SSE를 통한 효율적인 업데이트 + - 차트 데이터 메모이제이션 + +### 4. 이미지 생성 비용 및 시간 +- **리스크**: 이미지 생성 API 비용 및 생성 시간 +- **해결방안**: + - 생성된 이미지 캐싱 + - CDN 활용 + - 비동기 생성 + 진행 상황 표시 + +--- + +## 예상 개발 기간 + +- User 서비스: 2주 +- Event 서비스: 3주 +- AI 서비스: 4주 (프롬프트 엔지니어링 포함) +- Content 서비스: 3주 +- Distribution 서비스: 4주 (다중 API 연동) +- Participation 서비스: 2주 +- Analytics 서비스: 4주 (통합 대시보드) + +**총 예상 기간**: 12-16주 (병렬 개발 시) diff --git a/design/userstory.md b/design/userstory.md new file mode 100644 index 0000000..1528798 --- /dev/null +++ b/design/userstory.md @@ -0,0 +1,998 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 유저스토리 ver2.0 + +- [KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 유저스토리 ver2.0](#kt-ai-기반-소상공인-이벤트-자동-생성-서비스---유저스토리-ver20) + - [마이크로서비스 구성](#마이크로서비스-구성) + - [유저스토리](#유저스토리) + - [기술 검토 결과](#기술-검토-결과) + +--- + +## 마이크로서비스 구성 +1. **User** - 사용자 인증 및 매장정보 관리 +2. **Event** - 이벤트 기획 및 관리 +3. **AI** - AI 기반 트렌드 분석 및 이벤트 추천 +4. **Content** - SNS 콘텐츠 생성 +5. **Distribution** - 다중 채널 배포 관리 +6. **Participation** - 이벤트 참여 및 당첨자 관리 +7. **Analytics** - 실시간 효과 측정 및 통합 대시보드 + +--- + +## 유저스토리 +``` +1. User 서비스 +1) 사용자 인증 및 매장정보 관리 +UFR-USER-010: [회원가입] 소상공인으로서 | 나는 이벤트 마케팅 서비스를 이용하기 위해 | 간편하게 회원가입하고 싶다. +- 시나리오: 신규 회원가입 + 미로그인 상태에서 회원가입 화면에 접근한 상황에서 | 필수 정보를 모두 입력하고 회원가입 버튼을 클릭하면 | 사업자번호 검증을 거쳐 회원가입이 완료된다. + + [입력 요구사항] + - 기본 정보 + - 이름: 2자 이상 (한글/영문) + - 전화번호: 휴대폰 번호 형식 + - 이메일: 이메일 형식 준수 + - 비밀번호: 8자 이상 (영문/숫자/특수문자 포함) + + [매장 정보 입력] + - 매장명: 필수 + - 업종: 선택 (예: 음식점, 카페, 소매점 등) + - 주소: 필수 + - 영업시간: 선택 + - 사업자번호: 필수 + - 국세청 DB 조회를 통한 검증 + - 휴폐업 여부 확인 + - 검증 실패 시 재입력 요청 + + [처리 결과] + - 성공: "회원가입이 완료되었습니다" 메시지 → 대시보드 화면 + - 실패: 구체적인 오류 메시지 표시 + +- M/21 + +--- + +UFR-USER-020: [로그인] 소상공인으로서 | 나는 이벤트를 관리하기 위해 | 간편하게 로그인하고 싶다. +- 시나리오: 사용자 로그인 + 미로그인 상태에서 로그인 화면에 접근한 상황에서 | 전화번호와 비밀번호를 입력하고 로그인 버튼을 클릭하면 | 인증이 완료되고 대시보드로 이동한다. + + [입력 요구사항] + - 전화번호: 등록된 전화번호 입력 + - 비밀번호: 해당 계정의 비밀번호 입력 + - 로그인 유지: 선택 옵션 제공 + + [인증 처리] + - 성공: 대시보드로 즉시 이동 + - 실패: "전화번호 또는 비밀번호를 확인해주세요" 메시지 + +- M/8 + +--- + +UFR-USER-030: [프로필관리] 소상공인으로서 | 나는 내 정보를 최신 상태로 유지하기 위해 | 프로필 정보를 편집하고 싶다. +- 시나리오: 프로필 정보 수정 + Bottom Navigation의 "프로필" 탭을 클릭하여 프로필 편집 화면에 접근한 상황에서 | 변경할 정보를 수정하고 저장 버튼을 클릭하면 | 변경사항이 저장되고 확인 메시지가 표시된다. + + [수정 가능 항목] + - 기본 정보 + - 이름: 실명 변경 가능 + - 전화번호: 변경 시 재인증 필요 + - 이메일: 이메일 변경 가능 + - 매장 정보 + - 매장명 + - 업종 + - 주소 + - 영업시간 + - 비밀번호 + - 현재 비밀번호 확인 필수 + - 새 비밀번호 규칙: 8자 이상 (영문/숫자/특수문자 포함) + + [저장 처리] + - 저장 전 확인: "변경사항을 저장하시겠습니까?" + - 저장 완료: "프로필이 성공적으로 업데이트되었습니다" + - 취소 선택: "변경사항이 저장되지 않습니다. 계속하시겠습니까?" + +- C/8 + +--- + +UFR-USER-040: [로그아웃] 소상공인으로서 | 나는 보안을 위해 | 서비스 사용 후 안전하게 로그아웃하고 싶다. +- 시나리오: 안전한 로그아웃 + Bottom Navigation의 "프로필" 탭에서 로그아웃 버튼을 선택한 상황에서 | 로그아웃 확인 다이얼로그에서 확인을 클릭하면 | 세션이 종료되고 로그인 화면으로 이동한다. + + [로그아웃 요구사항] + - 확인 다이얼로그 + - 메시지: "로그아웃 하시겠습니까?" + - 버튼: 취소/확인 + + [처리 결과] + - 확인 선택: "안전하게 로그아웃되었습니다" 메시지 → 로그인 화면 + - 취소 선택: 다이얼로그 닫힘, 현재 화면 유지 + +- S/3 + +--- + +2. Event 서비스 +1) 이벤트 관리 +UFR-EVENT-010: [대시보드] 소상공인으로서 | 나는 내 이벤트를 효율적으로 관리하기 위해 | 대시보드에서 진행중/예정/종료된 이벤트를 한눈에 확인하고 싶다. +- 시나리오: 대시보드에서 이벤트 목록 확인 + 로그인한 상태에서 Bottom Navigation의 "홈" 탭을 클릭하거나, 최초 로그인 시 대시보드에 접근한 상황에서 | 이벤트 목록 영역을 확인하면 | 내 이벤트들이 상태별로 구분되어 표시된다. + + [표시 요구사항] + - 이벤트 상태별 섹션 구성 + - "진행중": 현재 진행 중인 이벤트 + - "예정": 배포 대기 중인 이벤트 + - "종료": 완료된 이벤트 + - 이벤트 카드 정보 + - 이벤트명 + - 이벤트 기간 + - 진행 상태 뱃지 + - 간단한 통계 (참여자 수, 조회수 등) + + [상호작용 요구사항] + - 이벤트 카드 클릭: 해당 이벤트 상세 화면으로 이동 + - 섹션당 최대 5개 카드 표시 (최신 순) + - "전체보기" 링크: 전체 이벤트 목록 화면으로 이동 + - "새 이벤트 만들기" 버튼: 이벤트 생성 프로세스 시작 + + [빈 상태 처리] + - 이벤트가 없을 경우: "첫 이벤트를 만들어보세요" 안내 및 생성 버튼 + +- S/13 + +--- + +UFR-EVENT-020: [이벤트목적선택] 소상공인으로서 | 나는 효과적인 이벤트를 기획하기 위해 | 이벤트 목적을 먼저 선택하고 싶다. +- 시나리오: 이벤트 목적 선택 + 대시보드에서 "새 이벤트 만들기" 버튼을 클릭한 상황에서 | 이벤트 목적 선택 화면에서 하나의 목적을 선택하면 | AI 트렌드 분석 및 이벤트 추천 화면으로 이동한다. + + [목적 옵션] + - 신규 고객 유치: 새로운 고객 확보 + - 재방문 유도: 기존 고객 재방문 촉진 + - 매출 증대: 단기 매출 향상 + - 인지도 향상: 브랜드/매장 인지도 제고 + + [목적별 설명 제공] + - 각 목적에 대한 간단한 설명 + - 예상 효과 예시 + + [다음 단계] + - 목적 선택 완료 시 자동으로 AI 트렌드 분석 및 이벤트 추천 화면으로 이동 + +- M/5 + +--- + +UFR-EVENT-030: [AI이벤트추천] 소상공인으로서 | 나는 최신 트렌드를 반영하고 선택지를 비교하기 위해 | AI 트렌드 분석 결과와 함께 3가지 이벤트 추천을 확인하고 싶다. +- 시나리오: AI 트렌드 분석 및 이벤트 추천 확인 + 이벤트 목적을 선택한 상황에서 | AI 이벤트 추천 화면에 접근하면 | 트렌드 분석 결과와 함께 3가지 이벤트 기획안이 표시된다. + + [화면 구성] + 1. 트렌드 분석 결과 (상단) + - 업종 트렌드 + - 최근 성공한 이벤트 유형 + - 고객 선호 경품 분석 + - 효과적인 참여 방법 + - 지역 특성 + - 지역별 이벤트 성공률 + - 지역 고객 특성 + - 시즌 특성 + - 계절/시기별 추천 이벤트 + - 특별 이벤트 시즌 (명절, 기념일 등) + + 2. 추천 이벤트 3가지 (하단) + 각 카드에 다음 정보 포함: + - 이벤트 제목 (수정 가능) + - 추천 경품 (수정 가능) + - 참여 방법 + - 예상 참여자 수 + - 예상 비용 + - 예상 투자 대비 수익률 + + [추천 기준] + - 이벤트 목적 기반 최적화 + - 업종 트렌드 반영 + - 예산 대비 효과 극대화 + - 3가지 옵션 차별화: + - 옵션 1: 저비용, 높은 참여율 중심 + - 옵션 2: 중비용, 균형잡힌 투자 대비 수익률 + - 옵션 3: 고비용, 높은 매출 증대 효과 + + [간단한 커스텀 기능] + - 이벤트 제목: 각 카드에서 직접 수정 가능 (최대 50자) + - 경품명: 각 카드에서 직접 수정 가능 + + [이벤트 선택] + - 3가지 중 1개 선택 (라디오 버튼) + - 선택한 카드의 커스텀된 정보가 저장됨 + - 선택 완료 후 "다음" 버튼 활성화 + + [프로세스 시간] + - 트렌드 분석 + 추천 생성: 10초 이내 + - 진행 상황 표시 (로딩 인디케이터) + + [다음 단계] + - 이벤트 선택 후 콘텐츠 생성 화면으로 이동 + +- M/34 + +- 기술 태스크 + - AI 트렌드 분석 API + - 빅데이터 분석: 과거 성공 이벤트, 업종별 성공률, 지역 특성, 시즌 패턴 + - 트렌드 리포트 생성: JSON 형태 반환 + - 분석 완료 시간: 5초 이내 + - AI 이벤트 추천 API + - Claude API / GPT-4 API 연동 + - 기획안 생성 로직: + - 경품 추천 (예산 대비 매력도, 고객 선호도) + - 참여 방법 설계 (난이도별 차별화) + - 홍보 문구 생성 + - 3가지 옵션 차별화 생성 + - 예상 성과 계산 (참여자 수, 비용, 투자 대비 수익률) + - 생성 완료 시간: 5초 이내 + - 프론트엔드 통합 + - 트렌드 분석 결과 표시 + - 3가지 추천 카드 UI + - 실시간 제목/경품 편집 기능 + - 선택 상태 관리 + - 성능 최적화 + - 트렌드 분석과 추천 생성 병렬 처리 + - 캐싱 전략 + - 비동기 처리 + +--- + +UFR-EVENT-040: [배포채널선택] 소상공인으로서 | 나는 효과적인 이벤트 홍보를 위해 | 다중 배포 채널을 선택하고 싶다. +- 시나리오: 배포 채널 선택 + 이벤트 추천을 완료하고 콘텐츠 생성이 완료된 상황에서 | 배포 채널 선택 화면에서 원하는 채널을 선택하면 | 각 채널의 예상 노출 수가 표시된다. + + [배포 채널 옵션] + 1. 우리동네TV + - 반경 선택: 500m / 1km + - 송출 시간대: 평일 저녁 / 주말 점심 등 + - 예상 노출 수 표시 + + 2. 링고비즈 (연결음) + - 매장 전화번호 자동 연동 + - 연결음 업데이트 안내 + + 3. 지니TV 광고 + - 타겟 지역 선택 + - 노출 시간대 선택 + - 예산 입력 + - 예상 노출량 표시 + + 4. SNS + - Instagram (체크박스) + - Naver Blog (체크박스) + - Kakao Channel (체크박스) + - 예약 시간 설정 + + [채널별 비용 안내] + - 각 채널별 예상 비용 표시 + - 총 예상 비용 합계 + + [다음 단계] + - 최소 1개 이상 채널 선택 필수 + - "다음" 버튼: 최종 승인 화면으로 이동 + +- M/13 + +--- + +UFR-EVENT-050: [최종승인] 소상공인으로서 | 나는 이벤트를 배포하기 전에 | 모든 설정을 최종 확인하고 승인하고 싶다. +- 시나리오: 이벤트 최종 승인 + 배포 채널을 선택한 상황에서 | 최종 승인 화면에서 모든 정보를 확인하고 승인 버튼을 클릭하면 | 이벤트가 생성되고 배포가 시작된다. + + [최종 확인 정보] + 1. 이벤트 정보 + - 이벤트명 + - 이벤트 기간 + - 이벤트 목적 + + 2. 경품 정보 + - 경품명 + - 경품 수량 + + 3. 참여 방법 + - 참여 조건 요약 + + 4. 배포 채널 + - 선택한 채널 목록 + - 채널별 예상 노출 수 + + 5. 예상 비용 및 효과 + - 총 예상 비용 + - 예상 참여자 수 + - 예상 투자 대비 수익률 + + [승인 프로세스] + - "승인 및 배포" 버튼 클릭 + - 확인 다이얼로그: "이벤트를 승인하고 배포하시겠습니까?" + - 승인 시 이벤트 생성 및 콘텐츠 생성 시작 + + [처리 결과] + - 성공: "이벤트가 생성되었습니다. 배포를 시작합니다." 메시지 + - 자동으로 대시보드로 이동 + - 배포 진행 상황 알림 + +- M/13 + +--- + +UFR-EVENT-060: [이벤트상세조회] 소상공인으로서 | 나는 진행 중인 이벤트의 현황을 파악하기 위해 | 이벤트 상세 정보를 조회하고 싶다. +- 시나리오: 이벤트 상세 정보 조회 + 대시보드에서 이벤트 카드를 클릭한 상황에서 | 이벤트 상세 화면에 접근하면 | 이벤트의 모든 정보와 실시간 통계가 표시된다. + + [상세 정보 구성] + 1. 기본 정보 + - 이벤트명 + - 이벤트 기간 + - 진행 상태 + - 경품 정보 + - 참여 방법 + + 2. 실시간 통계 (Analytics 서비스 연동) + - 참여자 수 + - 노출 수 + - 조회 수 + - 공유 수 + + 3. 배포 채널 현황 + - 채널별 배포 상태 + - 채널별 성과 요약 + + 4. 참여자 목록 + - 최근 참여자 10명 + - "전체 참여자 보기" 링크 + + [액션 버튼] + - "효과 측정 대시보드": Analytics 대시보드로 이동 + - "이벤트 수정": 진행 중인 이벤트 수정 (제한적) + - "이벤트 종료": 예정보다 빨리 종료 + +- S/13 + +--- + +UFR-EVENT-070: [이벤트목록관리] 소상공인으로서 | 나는 모든 이벤트를 관리하기 위해 | 전체 이벤트 목록을 조회하고 필터링/검색하고 싶다. +- 시나리오: 전체 이벤트 목록 조회 + Bottom Navigation의 "이벤트" 탭을 클릭하거나, 대시보드에서 "전체보기" 링크를 클릭한 상황에서 | 이벤트 목록 화면에 접근하면 | 모든 이벤트가 테이블 형태로 표시된다. + + [목록 표시 요구사항] + - 테이블 컬럼 + - 이벤트명 + - 이벤트 기간 + - 상태 (진행중/예정/종료) + - 참여자 수 + - 투자 대비 수익률 + - 생성일 + - 페이지네이션 (페이지당 20개) + + [필터 옵션] + - 상태별 필터: 전체/진행중/예정/종료 + - 기간별 필터: 최근 1개월/3개월/6개월/1년/전체 + + [검색 기능] + - 검색창: 이벤트명 검색 + - 실시간 검색 결과 + + [정렬 옵션] + - 최신순 (기본값) + - 참여자 많은 순 + - 투자 대비 수익률 높은 순 + + [이벤트 관리] + - 행 클릭: 이벤트 상세 화면 + - 다중 선택: 일괄 삭제 기능 + +- S/13 + +--- + +3. AI 서비스 +1) AI 분석 및 추천 +UFR-AI-010: [AI트렌드분석및이벤트추천] AI 시스템으로서 | 나는 소상공인에게 최적의 이벤트를 제안하기 위해 | 업종/지역/시즌 트렌드를 분석하고 3가지 최적화된 이벤트 기획안을 생성하고 싶다. +- 시나리오: 트렌드 분석 및 이벤트 추천 수행 + 소상공인이 이벤트 목적을 선택한 상황에서 | AI 시스템이 요청을 받으면 | 트렌드 분석과 이벤트 추천을 병렬로 수행하여 결과를 반환한다. + + [분석 데이터 소스] + - 과거 성공 이벤트 데이터 + - 업종별 이벤트 성공률 + - 지역별 고객 특성 + - 시즌별 트렌드 패턴 + + [트렌드 분석 항목] + 1. 업종 트렌드 + - 최근 3개월 성공 이벤트 유형 + - 고객 선호 경품 Top 5 + - 효과적인 참여 방법 분석 + + 2. 지역 특성 + - 해당 지역 이벤트 성공률 + - 지역 고객 연령대/성별 분포 + + 3. 시즌 특성 + - 계절별 추천 이벤트 + - 특별 시즌 (명절, 기념일) 반영 + + [이벤트 추천 로직] + 1. 경품 추천 + - 예산 대비 매력도 최대화 + - 타겟 고객 선호도 반영 + - KT 멤버십 포인트 활용 가능 여부 + + 2. 참여 방법 설계 + - 간단한 참여 방법 (저 난이도) + - 재방문 유도 요소 (중 난이도) + - 바이럴 확산 장치 (고 난이도) + + 3. 홍보 문구 생성 + - 이벤트 개요 기반 문구 5개 생성 + - SNS 해시태그 자동 생성 + + [3가지 옵션 차별화] + - 옵션 1: 저비용, 높은 참여율 중심 + - 옵션 2: 중비용, 균형잡힌 투자 대비 수익률 + - 옵션 3: 고비용, 높은 매출 증대 효과 + + [예상 성과 계산] + - 예상 참여자 수 + - 예상 비용 + - 예상 투자 대비 수익률 + + [결과 출력] + - 트렌드 분석 결과 JSON 형태 반환 + - 3가지 이벤트 기획안 JSON 배열 반환 + - 전체 완료 시간: 10초 이내 (병렬 처리) + +- M/34 + +- 기술 태스크 + - AI 트렌드 분석 엔진 + - 빅데이터 분석 시스템 연동 + - 업종/지역/시즌별 데이터 집계 + - 트렌드 패턴 인식 알고리즘 + - AI 이벤트 추천 엔진 + - Claude API / GPT-4 API 연동 + - 프롬프트 엔지니어링 최적화 + - 응답 파싱 및 구조화 + - 병렬 처리 아키텍처 + - 트렌드 분석과 이벤트 추천 동시 실행 + - 결과 통합 로직 + - 성능 최적화 + - 캐싱 전략 (Redis) + - 비동기 처리 + - 응답 시간 모니터링 + +--- + +4. Content 서비스 +1) 콘텐츠 생성 +UFR-CONT-010: [SNS이미지생성] 소상공인으로서 | 나는 SNS에 게시할 이벤트 이미지를 쉽게 만들기 위해 | AI가 자동으로 3가지 스타일의 이미지를 생성하기를 원한다. +- 시나리오: SNS 이미지 자동 생성 + 이벤트 추천을 완료한 상황에서 | SNS 이미지 생성 화면에 접근하면 | AI가 3가지 스타일의 SNS 이미지를 자동 생성한다. + + [이미지 생성 입력] + - 이벤트 제목 + - 경품 정보 + - 브랜드 컬러 (프로필에서 가져옴) + - 로고 이미지 (업로드된 경우) + + [3가지 스타일 카드 선택] + 1. 심플 스타일 + - 깔끔한 디자인 + - 텍스트 중심 + - 읽기 쉬운 구성 + - 카드 형태로 제공 (선택 가능) + + 2. 화려한 스타일 + - 눈에 띄는 디자인 + - 이미지 강조 + - 풍부한 색상 + - 카드 형태로 제공 (선택 가능) + + 3. 트렌디 스타일 + - 최신 트렌드 반영 + - SNS 최적화 + - MZ세대 타겟 + - 카드 형태로 제공 (선택 가능) + + [플랫폼별 최적화] + - Instagram: 1080x1080 + - Naver Blog: 800x600 + - Kakao Channel: 800x800 + + [생성 프로세스] + - 생성 진행 표시 (스피너 애니메이션) + - "딥러닝 모델이 이벤트에 어울리는 이미지를 생성하고 있어요..." 안내 + - 예상 소요 시간: 5초 이내 + - 생성 완료 시 3가지 스타일 카드 표시 + + [이미지 선택 및 미리보기] + - 3가지 스타일 중 1개 선택 가능 (카드 선택 방식) + - 각 카드 클릭 시 풀스크린 미리보기 제공 + - "다시 생성" 옵션 (최대 3회) + - 선택 완료 시 "다음" 버튼 활성화 + +- M/21 + +--- + +UFR-CONT-020: [콘텐츠편집] 소상공인으로서 | 나는 생성된 이미지를 내 스타일에 맞게 조정하기 위해 | 간단한 편집 기능을 사용하고 싶다. +- 시나리오: 생성된 콘텐츠 편집 + SNS 이미지 스타일을 선택한 상황에서 | 콘텐츠 편집 화면에서 텍스트나 색상을 수정하면 | 실시간으로 미리보기가 업데이트된다. + + [편집 가능 항목] + - 텍스트 수정 + - 제목 텍스트 (인라인 편집) + - 경품 정보 텍스트 (인라인 편집) + - 참여 안내 텍스트 (인라인 편집) + - 색상 조정 + - 배경색 (컬러 피커) + - 텍스트 색상 (컬러 피커) + - 강조 색상 (컬러 피커) + + [편집 제약사항] + - 레이아웃 변경 불가 + - 로고 위치/크기 조절 불가 + - 이미지 교체 불가 (이전 화면의 "다시 생성"으로만 가능) + + [실시간 미리보기] + - 수정사항 즉시 반영 + - 플랫폼별 미리보기 전환 가능 (Instagram/Naver/Kakao) + - 모바일 상단에 미리보기 영역 표시 + + [저장 및 완료] + - "저장" 버튼: 편집 내용 임시 저장 + - "다음" 버튼: 배포 채널 선택 화면으로 이동 + +- S/13 + +--- + +5. Distribution 서비스 +1) 배포 관리 +UFR-DIST-010: [다중채널배포] 소상공인으로서 | 나는 여러 채널에 동시에 이벤트를 홍보하기 위해 | 원클릭으로 다중 채널 배포를 실행하고 싶다. +- 시나리오: 다중 채널 동시 배포 + 최종 승인이 완료된 상황에서 | Distribution 서비스가 배포 요청을 받으면 | 선택된 모든 채널에 동시 배포를 시작한다. + + [배포 채널 처리] + 1. 우리동네TV + - 우리동네TV API 연동 + - 15초 영상 업로드 (Content 서비스에서 생성된 이미지 활용) + - 송출 시간대 및 반경 설정 전달 + - 배포 완료 시 배포 ID 수신 + + 2. 링고비즈 (연결음) + - 링고비즈 API 연동 + - 매장 전화번호로 연결음 업데이트 + - 업데이트 완료 시각 수신 + + 3. 지니TV 광고 + - 지니TV 광고 API 연동 + - 타겟 지역, 노출 시간대, 예산 정보 전달 + - 광고 ID 및 노출 스케줄 수신 + + 4. SNS 자동 포스팅 + - Instagram, Naver Blog, Kakao Channel 병렬 처리 + - 플랫폼별 최적화된 이미지 사용 + - 예약 시간 설정 반영 + - 각 플랫폼 포스팅 완료 확인 + + [배포 진행 상황] + - 실시간 진행 상황 알림 + - 채널별 배포 상태 표시 + - 예상 완료 시간: 1분 이내 + + [배포 실패 처리] + - 채널별 독립적 처리 (하나 실패해도 다른 채널 계속 진행) + - 실패 시 자동 재시도 (최대 3회) + - 최종 실패 시 소상공인에게 알림 + + [배포 완료 처리] + - 배포 이력 DB 저장 + - 배포 채널 목록 및 예상 도달 수 반환 + - 소상공인에게 배포 완료 알림 (앱 푸시/이메일) + +- M/21 + +--- + +UFR-DIST-020: [배포상태모니터링] 소상공인으로서 | 나는 배포 진행 상황을 실시간으로 파악하기 위해 | 배포 상태를 모니터링하고 싶다. +- 시나리오: 배포 상태 실시간 확인 + 배포가 시작된 상황에서 | 이벤트 상세 화면의 배포 상태 섹션을 확인하면 | 채널별 배포 진행 상황이 실시간으로 표시된다. + + [배포 상태 표시] + - 채널별 상태 + - 대기중: 회색 + - 진행중: 파란색 (진행률 표시) + - 완료: 초록색 (체크 표시) + - 실패: 빨간색 (오류 메시지) + + [배포 정보] + - 우리동네TV: 배포 ID, 예상 노출 수 + - 링고비즈: 업데이트 완료 시각 + - 지니TV: 광고 ID, 노출 스케줄 + - SNS: 포스팅 URL 링크 + + [재시도/취소] + - 실패한 채널: "재시도" 버튼 + - 진행중인 배포: "취소" 버튼 (일부 채널만 가능) + +- S/13 + +--- + +6. Participation 서비스 +1) 참여자 관리 +UFR-PART-010: [이벤트참여] 고객으로서 | 나는 간편하게 이벤트에 참여하기 위해 | 최소한의 정보만 입력하고 싶다. +- 시나리오: 고객 이벤트 참여 + 고객이 이벤트를 발견한 상황에서 | 이벤트 참여 화면에서 필수 정보를 입력하고 참여 버튼을 클릭하면 | 참여 접수가 완료된다. + + [이벤트 발견 경로] + - 우리동네TV + - SNS (Instagram, Blog, Kakao) + - 매장 방문 (링고비즈 연결음 안내) + + [참여 정보 입력] + - 이름: 필수 (2자 이상) + - 전화번호: 필수 (휴대폰 번호) + - 참여 경로: 자동 감지 또는 선택 + + [중복 참여 체크] + - 전화번호 기반 중복 확인 + - 1인 1회 참여 제한 + - 중복 시: "이미 참여하신 이벤트입니다" 안내 + + [참여 접수 완료] + - 응모 번호 발급 + - 당첨 발표일 안내 + - 참여 완료 화면 표시 + + [개인정보 처리] + - 개인정보 수집/이용 동의 필수 + - 마케팅 활용 동의 선택 + +- M/13 + +--- + +UFR-PART-020: [참여자목록조회] 소상공인으로서 | 나는 누가 참여했는지 파악하기 위해 | 참여자 목록을 조회하고 싶다. +- 시나리오: 참여자 목록 확인 + 이벤트 상세 화면에서 "참여자 목록" 탭을 클릭한 상황에서 | 참여자 목록 화면에 접근하면 | 모든 참여자 정보가 테이블로 표시된다. + + [목록 표시] + - 테이블 컬럼 + - 번호 (응모 번호) + - 이름 + - 전화번호 (일부 마스킹) + - 참여 경로 + - 참여 일시 + - 당첨 여부 + - 페이지네이션 + + [필터 옵션] + - 참여 경로별 필터 + - 당첨 여부 필터 (전체/당첨/미당첨) + + [검색 기능] + - 이름 또는 전화번호로 검색 + + [액션] + - 개별 참여자 상세 정보 보기 + +- S/8 + +--- + +UFR-PART-030: [당첨자추첨] 소상공인으로서 | 나는 공정한 당첨자 선정을 위해 | 자동 추첨 기능을 사용하고 싶다. +- 시나리오: 당첨자 자동 추첨 + 이벤트 종료일이 도래한 상황에서 | 소상공인이 "당첨자 추첨" 버튼을 클릭하면 | 시스템이 자동으로 당첨자를 추첨한다. + + [추첨 방식] + - 난수 기반 무작위 추첨 + - 매장 방문 고객 가산점 옵션 (선택 가능) + - 추첨 과정 로그 자동 기록 + + [당첨 인원 설정] + - 경품 수량 기반 자동 설정 + - 수동 조정 가능 + + [추첨 결과] + - 당첨자 목록 표시 + - 당첨자 정보: 이름, 전화번호, 응모번호 + + [재추첨] + - "재추첨" 버튼: 당첨자 재선정 (추첨 전 확인) + - 이전 추첨 이력 보관 + +- M/13 + +--- + +7. Analytics 서비스 +1) 효과 측정 +UFR-ANAL-010: [성과분석] 소상공인으로서 | 나는 이벤트 성과를 한눈에 파악하기 위해 | 모든 지표가 통합된 실시간 성과분석 대시보드를 보고 싶다. +- 시나리오: 성과분석 대시보드 조회 + Bottom Navigation의 "분석" 탭을 클릭하거나, 이벤트 상세 화면에서 "성과분석" 버튼을 클릭한 상황에서 | 성과분석 대시보드 화면에 접근하면 | 모든 핵심 지표가 하나의 화면에 실시간으로 표시된다. + + [대시보드 구성] + + 1. 상단 요약 카드 (4개) + - 총 참여자 수 + - 현재 참여자 수 + - 목표 대비 달성률 (%) + - 총 노출 수 + - 채널별 노출 합계 + - 전일 대비 증감률 + - 예상 투자 대비 수익률 + - 실시간 투자 대비 수익률 계산 + - 업종 평균 대비 비교 + - 매출 증가율 + - POS 연동 데이터 기반 + - 이벤트 전후 비교 (%) + + 2. 채널별 성과 분석 (차트) + - 막대 그래프: 채널별 참여자 수 + - 원 그래프: 채널별 참여 비율 + - 채널 목록: + - 우리동네TV + - 링고비즈 + - 지니TV + - Instagram + - Naver Blog + - Kakao Channel + - 각 채널별 세부 지표: + - 노출 수 + - 참여자 수 + - 전환율 (%) + - 비용 대비 효율 (CPA) + + 3. 시간대별 참여 추이 (라인 차트) + - X축: 시간 (일별/시간별 전환 가능) + - Y축: 참여자 수 + - 실시간 업데이트 (5분 간격) + - 피크 시간대 하이라이트 + + 4. 투자 대비 수익률 상세 분석 (표) + - 총 비용 산출 + - 경품 비용 + - 채널별 플랫폼 비용 + - 기타 비용 + - 예상 수익 산출 + - 매출 증가액 + - 신규 고객 LTV + - 투자 대비 수익률 계산식 표시 + - 투자 대비 수익률 = (수익 - 비용) / 비용 × 100 + - 손익분기점 표시 + + 5. 참여자 프로필 분석 (차트) + - 연령대별 분포 + - 성별 분포 + - 지역별 분포 + - 참여 시간대 분석 + + 6. 비교 분석 (표) + - 업종 평균과 비교 + - 참여율 + - 투자 대비 수익률 + - 전환율 + - 내 이전 이벤트와 비교 + - 성과 개선도 + - 최고/최저 기록 대비 + + [실시간 업데이트] + - 모든 지표 5분 간격 자동 갱신 + - 새로운 참여자 알림 (선택 옵션) + - 수동 새로고침 버튼 + + [데이터 소스] + - Participation 서비스: 참여자 데이터 + - Distribution 서비스: 채널별 노출 수 + - POS 시스템: 매출 데이터 (연동 시) + - 외부 API: 우리동네TV, 지니TV, SNS 통계 + + [모바일 최적화] + - 반응형 디자인 + - 스크롤 가능한 레이아웃 + - 핵심 지표 우선 표시 + +- M/34 + +- 기술 태스크 + - 실시간 데이터 수집 + - 5분 간격 데이터 폴링 + - WebSocket 또는 SSE를 통한 실시간 업데이트 + - 데이터 캐싱 (Redis) + - 다중 API 통합 + - 우리동네TV API + - 링고비즈 API + - 지니TV API + - Instagram API + - Naver API + - Kakao API + - POS 시스템 연동 + - 매출 데이터 수집 + - 이벤트 전후 비교 로직 + - 차트 라이브러리 활용 + - Chart.js / Recharts 등 + - 반응형 차트 구현 + - 성능 최적화 + - 차트 데이터 메모이제이션 + - 무한 스크롤 대신 페이지네이션 + - 이미지/데이터 lazy loading + +--- + +## 기술 검토 결과 +**[마이크로서비스별 검토]** + +1) User 서비스 +모든 유저스토리 **✅ 실현 가능** +- 표준적인 인증/인가 패턴 +- JWT 토큰, Redis 캐싱 등 검증된 기술 활용 +- 사업자번호 검증 API (국세청) 연동 + +2) Event 서비스 +대부분 **✅ 실현 가능** +- 이벤트 CRUD 기능 +- AI 서비스와의 연동 +- LocalStorage 자동 저장 구현 + +3) AI 서비스 +**⚠️ 높은 기술적 복잡도** + +- UFR-AI-010: **주요 리스크 포인트** + - Claude API 또는 GPT-4 API 연동 + - 트렌드 분석 + 이벤트 추천 병렬 처리 + - 응답 시간 관리 (10초 목표) + - 프롬프트 엔지니어링 최적화 + - **해결방안**: 캐싱, 병렬 처리, 비동기 처리 + +4) Content 서비스 +**⚠️ 중간 복잡도** + +- UFR-CONT-010: **이미지 생성 API 연동** + - Stable Diffusion 또는 DALL-E API + - 플랫폼별 이미지 크기 최적화 + - **해결방안**: 이미지 캐싱, CDN 활용 + +5) Distribution 서비스 +**⚠️ 다중 API 통합 복잡도** + +- UFR-DIST-010: **외부 API 다중 연동** + - 우리동네TV API + - 링고비즈 API + - 지니TV API + - SNS API (Instagram, Naver, Kakao) + - **해결방안**: 채널별 독립 처리, 실패 시 재시도, 써킷 브레이커 패턴 + +6) Participation 서비스 +대부분 **✅ 실현 가능** +- 표준적인 CRUD 기능 +- 추첨 알고리즘 구현 + +7) Analytics 서비스 +**⚠️ 높은 통합 복잡도** + +- UFR-ANAL-010: **통합 대시보드 구현** + - 다중 데이터 소스 통합 + - 실시간 데이터 수집 및 표시 + - 복잡한 차트 및 그래프 + - **해결방안**: 데이터 캐싱 (Redis), WebSocket/SSE, 차트 라이브러리 활용 + +**[주요 기술 스택 권장사항]** + +**백엔드:** +- Node.js/Spring Boot (마이크로서비스) +- Redis (캐싱, 세션, 실시간 데이터) +- PostgreSQL/MongoDB +- RabbitMQ/Kafka (비동기 처리) + +**프론트엔드:** +- React/Next.js +- TypeScript +- Zustand/Redux (상태관리) +- React Query (API 상태관리) +- Chart.js/Recharts (차트 라이브러리) + +**인프라:** +- Docker/Kubernetes +- API Gateway +- Circuit Breaker +- 로드 밸런서 + +**외부 API:** +- AI: Claude API / GPT-4 API +- 이미지 생성: Stable Diffusion / DALL-E +- 사업자 검증: 국세청 API +- 배포 채널: 우리동네TV, 링고비즈, 지니TV, SNS APIs + +**[주요 기술적 리스크 및 해결방안]** + +**1. AI 응답 시간 (10초 목표)** +- 리스크: AI API 응답이 목표 시간을 초과할 수 있음 +- 해결방안: + - 프롬프트 최적화로 응답 시간 단축 + - 트렌드 분석과 이벤트 추천 병렬 처리 + - 비동기 처리 + 폴링 방식 + - 부분 응답 스트리밍 + +**2. 다중 외부 API 의존성** +- 리스크: API 장애 시 서비스 중단 +- 해결방안: + - 각 API별 폴백 전략 수립 + - 캐싱 적극 활용 + - 써킷 브레이커 패턴 적용 + - 채널별 독립 처리 + +**3. 실시간 대시보드 성능** +- 리스크: 다중 데이터 소스 통합으로 인한 성능 저하 +- 해결방안: + - Redis 캐싱 + - 5분 간격 데이터 폴링 + - WebSocket/SSE를 통한 효율적인 업데이트 + - 차트 데이터 메모이제이션 + +**4. 이미지 생성 비용 및 시간** +- 리스크: 이미지 생성 API 비용 및 생성 시간 +- 해결방안: + - 생성된 이미지 캐싱 + - CDN 활용 + - 비동기 생성 + 진행 상황 표시 + +**[간소화된 플로우 검증]** + +✅ **요청사항 반영 확인** +1. ✅ KT인증시스템연동 제외됨 +2. ✅ 신규가입 시 무료체험 기능 삭제됨 +3. ✅ 5회연속실패시 계정잠금기능 삭제됨 +4. ✅ 이벤트커스텀 화면 통합됨 (이벤트추천조회 화면에서 간단히 제목/경품 수정 가능) +5. ✅ UFR-AI-010과 UFR-AI-020 통합됨 (트렌드 분석 + 이벤트 추천 한 번에 처리) +6. ✅ UFR-PART-040 삭제됨 (당첨알림발송) +7. ✅ UFR-ANAL-020 삭제됨 (분석리포트생성) +8. ✅ PDF / Excel 내보내기/다운로드 기능 삭제됨 + +**[이벤트 생성 플로우 (최종)]** +1. 이벤트 목적 선택 (07-이벤트목적선택) +2. AI 트렌드 분석 및 이벤트 추천 (08-AI이벤트추천) + - 3가지 추천, 제목/경품 간단 수정 가능 +3. SNS 이미지 생성 (09-SNS이미지생성) + - 3가지 스타일 카드 중 선택 +4. 콘텐츠 편집 (10-콘텐츠편집) + - 텍스트, 색상 편집 +5. 배포채널 선택 (11-배포채널선택) +6. 최종승인 (12-최종승인) + +**[서비스 간 의존성]** +- User → Event: 사용자 인증 정보 제공 +- Event → AI: 트렌드 분석 및 이벤트 추천 요청 +- Event → Content: 콘텐츠 생성 요청 +- Event → Distribution: 배포 요청 +- Distribution → Content: 생성된 콘텐츠 사용 +- Participation → Analytics: 참여자 데이터 제공 +- Distribution → Analytics: 노출 데이터 제공 + +**[유저스토리 통계]** +- User 서비스: 4개 +- Event 서비스: 7개 +- AI 서비스: 1개 (통합) +- Content 서비스: 2개 +- Distribution 서비스: 2개 +- Participation 서비스: 3개 +- Analytics 서비스: 1개 + +**총 20개 유저스토리** + +**[예상 개발 기간]** +- User 서비스: 2주 +- Event 서비스: 3주 +- AI 서비스: 4주 (프롬프트 엔지니어링 포함) +- Content 서비스: 3주 +- Distribution 서비스: 4주 (다중 API 연동) +- Participation 서비스: 2주 +- Analytics 서비스: 4주 (통합 대시보드) + +**총 예상 기간**: 12-16주 (병렬 개발 시) +``` diff --git a/design/구현방안-AI이벤트설계.md b/design/구현방안-AI이벤트설계.md new file mode 100644 index 0000000..44f394d --- /dev/null +++ b/design/구현방안-AI이벤트설계.md @@ -0,0 +1,1612 @@ +# 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) + +--- + +**문서 끝**