import os import re import time import requests import numpy as np import torch import joblib import xgboost as xgb from fastapi import FastAPI, HTTPException from pydantic import BaseModel from sentence_transformers import SentenceTransformer from typing import Optional, List,Dict from transformers import BertTokenizer, BertForSequenceClassification import uvicorn import gradio as gr from threading import Thread from fastapi.middleware.cors import CORSMiddleware ##################################### # 1) 앱 및 모델 초기화 ##################################### app = FastAPI() import os app.add_middleware( CORSMiddleware, allow_origins=["*"], # 모든 도메인 허용 (보안상 필요에 따라 제한) allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) os.environ["HF_HOME"] = "/workspace/huggingface_cache" os.environ["HF_HUB_CACHE"] = "/workspace/huggingface_cache" os.environ["TRANSFORMERS_CACHE"] = "/workspace/huggingface_cache" # 🟢 (A) 모델 로딩 print("Loading SentenceTransformer model...)") model_bert = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") print("Model loaded.") # 🟢 (B) 예시 아이템 목록 items = [ # ========= 1~12: 운동 (헬스, 요가, 필라테스, 수영, 테니스, 골프, 클라이밍, 축구, 농구, 볼링, 배드민턴, 러닝) ========= { "item_id": 1, "title": "헬스 퍼스널 프로젝트", "desc": "PT 전문 코치와 함께 근력·유산소를 체계적으로 관리 (운동, 헬스)", "personality": "외향형, 이성형" }, { "item_id": 2, "title": "빈야사 요가 in 강릉", "desc": "강릉 바다 풍경 속에서 호흡·동작을 익히는 빈야사 요가 클래스 (운동, 요가)", "personality": "내향형, 감정형" }, { "item_id": 3, "title": "매트 필라테스 집중 워크숍", "desc": "매트 동작 위주로 기초 코어 힘을 기르는 소그룹 훈련 (운동, 필라테스)", "personality": "내향형, 이성형" }, { "item_id": 4, "title": "오픈워터 스위밍 체험", "desc": "바다나 호수에서 수영 실력을 시험하는 자연 친화형 이벤트 (운동, 수영)", "personality": "외향형, 감정형" }, { "item_id": 5, "title": "테니스 포핸드 마스터클래스", "desc": "포핸드 스윙·풋워크를 집중 훈련하고 미니 게임으로 실습 (운동, 테니스)", "personality": "외향형, 감정형" }, { "item_id": 6, "title": "골프 숏게임 특화 레슨", "desc": "어프로치·퍼팅 등 숏게임 구간을 집중적으로 연습하는 프로그램 (운동, 골프)", "personality": "내향형, 이성형" }, { "item_id": 7, "title": "아웃도어 클라이밍 교실", "desc": "실외 바위 지형에서 암벽등반 기초와 안전 수칙을 배우는 챌린지 (운동, 클라이밍)", "personality": "외향형, 감정형" }, { "item_id": 8, "title": "풋살 드리블&패스 이벤트", "desc": "축구 소규모 버전 풋살에서 전술·팀워크를 즐기는 주말 클래스 (운동, 축구)", "personality": "외향형, 감정형" }, { "item_id": 9, "title": "스트리트 농구 대결전", "desc": "도심 코트에서 농구 토너먼트를 열어 실력을 겨루는 액션 (운동, 농구)", "personality": "외향형, 이성형" }, { "item_id": 10, "title": "볼링 스핀&릴리스 테크닉", "desc": "볼링장 전문 강사와 함께 볼 스핀, 릴리스 자세를 교정 (운동, 볼링)", "personality": "내향형, 이성형" }, { "item_id": 11, "title": "배드민턴 기술 업 워크숍", "desc": "스매시·드롭샷·푸트워크를 체계적으로 배우는 배드민턴 교실 (운동, 배드민턴)", "personality": "외향형, 감정형" }, { "item_id": 12, "title": "10K 러닝 크루 레이스", "desc": "도심 10km 달리기에 함께 도전하며 기록 향상을 목표 (운동, 러닝)", "personality": "내향형, 감정형" }, # ========= 13~20: 여행 (국내여행, 해외여행, 백패킹, 캠핑, 도시여행, 맛집탐방, 문화탐방, 힐링여행) ========= { "item_id": 13, "title": "부산 해안 로드트립", "desc": "차로 부산 해안 도로를 달리며 해산물과 해변 풍경 만끽 (여행, 국내여행)", "personality": "외향형, 감정형" }, { "item_id": 14, "title": "미국 서부 배낭투어", "desc": "LA·샌프란시스코·라스베이거스 등 미국 서부 도시에 떠나는 자유여행 (여행, 해외여행)", "personality": "외향형, 감정형" }, { "item_id": 15, "title": "설악산 백패킹 트레킹", "desc": "설악산 국립공원에서 배낭 야영하며 산세와 계곡을 체험 (여행, 백패킹)", "personality": "내향형, 감정형" }, { "item_id": 16, "title": "모닥불 캠핑 나이트", "desc": "텐트 피칭과 모닥불 요리로 캠핑 문화를 느끼는 하룻밤 (여행, 캠핑)", "personality": "내향형, 감정형" }, { "item_id": 17, "title": "홍콩 시티 골목탐방", "desc": "홍콩 소호·몽콕 등 골목 시장과 길거리 음식을 즐기는 도시여행 (여행, 도시여행)", "personality": "외향형, 이성형" }, { "item_id": 18, "title": "전국 맛집 로드쇼", "desc": "블로그·SNS 추천 맛집을 직접 찾아다니며 식도락을 즐기는 투어 (여행, 맛집탐방)", "personality": "외향형, 감정형" }, { "item_id": 19, "title": "경주 역사 문화탐방", "desc": "신라시대 유적·박물관·전통 공연을 둘러보며 문화유산을 학습 (여행, 문화탐방)", "personality": "내향형, 이성형" }, { "item_id": 20, "title": "지중해 요가 힐링 투어", "desc": "지중해 연안 소도시에서 요가·명상으로 몸과 마음을 재충전 (여행, 힐링여행)", "personality": "외향형, 감정형" }, # ========= 21~30: 독서 (소설, 시, 에세이, 자기계발, 인문, 역사, 과학, 경제/경영, 철학, 예술) ========= { "item_id": 21, "title": "이문열 '우리들의 일그러진 영웅' 독서토론", "desc": "한국 현대소설의 학교 폭력·권력 관계를 토론하며 생각 확장 (독서, 소설)", "personality": "내향형, 감정형" }, { "item_id": 22, "title": "김소월 시 낭독 살롱", "desc": "'진달래꽃' 등 한국적 정서가 깃든 시를 낭송하고 감상을 교류 (독서, 시)", "personality": "내향형, 감정형" }, { "item_id": 23, "title": "공지영 '즐거운 나의 집' 에세이 톡", "desc": "가족과 일상 이야기를 담은 에세이를 함께 읽고 공감 (독서, 에세이)", "personality": "내향형, 감정형" }, { "item_id": 24, "title": "하이 퍼포먼스 습관 만들기", "desc": "브렌든 버처드 등 자기계발서를 기반으로 아침 루틴을 실천 (독서, 자기계발)", "personality": "내향형, 이성형" }, { "item_id": 25, "title": "장하석 '온도계의 철학' 인문 스터디", "desc": "온도·측정의 철학적 의미를 다룬 책을 통해 사고 확장 (독서, 인문)", "personality": "내향형, 이성형" }, { "item_id": 26, "title": "유홍준 '나의 문화유산답사기' 역사 독해", "desc": "한국 문화유산 답사기를 읽고 현장 답사 욕구를 높이는 토론 (독서, 역사)", "personality": "내향형, 이성형" }, { "item_id": 27, "title": "리처드 도킨스 '이기적 유전자' 과학 세션", "desc": "유전학 이론을 대중적으로 풀어낸 도서를 함께 읽고 토론 (독서, 과학)", "personality": "내향형, 이성형" }, { "item_id": 28, "title": "짐 콜린스 '좋은 기업을 넘어 위대한 기업으로' 세미나", "desc": "기업 성공 사례를 분석하며 경제·경영 통찰을 얻는 과정 (독서, 경제/경영)", "personality": "내향형, 이성형" }, { "item_id": 29, "title": "장자 '소요유' 철학 살롱", "desc": "도가 사상을 담은 장자의 글을 읽고 자유로운 삶의 가치 논의 (독서, 철학)", "personality": "내향형, 이성형" }, { "item_id": 30, "title": "에드가 드가 '발레 수업' 예술책 토크", "desc": "인상주의 화가 드가의 발레 시리즈와 예술서를 같이 탐독 (독서, 예술)", "personality": "내향형, 감정형" }, # ========= 31~40: 영화 (로맨스, 코미디, 액션, 스릴러, 공포, SF, 판타지, 드라마, 애니메이션, 다큐멘터리) ========= { "item_id": 31, "title": "'시간 여행자의 아내' 로맨스 스크리닝", "desc": "시간여행을 소재로 한 사랑 이야기를 함께 보고 울고 웃는 자리 (영화, 로맨스)", "personality": "외향형, 감정형" }, { "item_id": 32, "title": "코미디 '슈퍼배드' 웃음 파티", "desc": "'슈퍼배드' 시리즈를 감상하며 유머 코드를 분석하고 떠들썩한 웃음을 공유 (영화, 코미디)", "personality": "외향형, 감정형" }, { "item_id": 33, "title": "'킹스맨' 액션씬 관전", "desc": "'킹스맨' 시리즈의 액션 연출·스파이 코드를 짚어보는 상영회 (영화, 액션)", "personality": "외향형, 이성형" }, { "item_id": 34, "title": "'서스펙트' 스릴러 미스터리", "desc": "긴장감 넘치는 한국 스릴러 영화를 함께 감상하고 반전 요소 해석 (영화, 스릴러)", "personality": "외향형, 감정형" }, { "item_id": 35, "title": "'에일리언' 공포 SF 밤샘", "desc": "'에일리언' 시리즈를 몰아보며 외계 생명체 공포와 SF 설정을 토론 (영화, 공포)", "personality": "내향형, 감정형" }, { "item_id": 36, "title": "'그래비티' SF 토크", "desc": "우주 공간 조난 스토리를 그린 '그래비티'의 과학·연출을 분석 (영화, SF)", "personality": "외향형, 감정형" }, { "item_id": 37, "title": "'해리 포터' 판타지 앤딩", "desc": "호그와트 마법 세계관을 연속 상영하고 팬심을 나누는 스팟 (영화, 판타지)", "personality": "외향형, 감정형" }, { "item_id": 38, "title": "'인생은 아름다워' 드라마 감동", "desc": "전쟁 속 가족사랑을 그린 이탈리아 명작으로 희망 메시지 나눔 (영화, 드라마)", "personality": "외향형, 감정형" }, { "item_id": 39, "title": "'너의 이름은' 애니메이션 스페셜", "desc": "신카이 마코토 감독의 빛나는 작화·청춘 판타지를 감상 (영화, 애니메이션)", "personality": "외향형, 감정형" }, { "item_id": 40, "title": "'마이클 무어' 다큐 포럼", "desc": "'화씨 9/11' 등 사회·정치적 메시지가 담긴 다큐를 보고 토론 (영화, 다큐멘터리)", "personality": "외향형, 이성형" }, # ========= 41~50: 게임 (RPG, FPS, 액션, 전략, 시뮬레이션, 스포츠, 퍼즐, 음악/리듬, 카드, MMORPG) ========= { "item_id": 41, "title": "'젤다의 전설: 야생의 숨결' RPG 탐험", "desc": "오픈월드 명작 '젤다' 시리즈를 탐험하며 퍼즐·전투 방식을 공유 (게임, RPG)", "personality": "내향형, 감정형" }, { "item_id": 42, "title": "'콜 오브 듀티' FPS 워존 배틀", "desc": "밀리터리 FPS에서 팀 단위 전술을 실행하고 승리를 노리는 대전 (게임, FPS)", "personality": "외향형, 이성형" }, { "item_id": 43, "title": "'갓 오브 워' 액션 앤 레이지", "desc": "크레토스의 여정·스킬 콤보를 파고들며 액션게임의 재미 체험 (게임, 액션)", "personality": "외향형, 감정형" }, { "item_id": 44, "title": "'문명6' 전략 빌드 세션", "desc": "턴제 전략 '문명6'에서 문명별 특성과 승리 조건을 분석 (게임, 전략)", "personality": "내향형, 이성형" }, { "item_id": 45, "title": "'플래닛 코스터' 시뮬레이션 제작소", "desc": "놀이공원 경영 게임에서 놀이기구 배치·경영을 디자인 (게임, 시뮬레이션)", "personality": "내향형, 감정형" }, { "item_id": 46, "title": "'NBA 2K' 스포츠 매치", "desc": "농구게임 'NBA 2K'로 온라인·오프라인 토너먼트를 개최 (게임, 스포츠)", "personality": "외향형, 감정형" }, { "item_id": 47, "title": "'폴가이즈' 퍼즐 액션 배틀", "desc": "귀엽고 혼란스러운 장애물 코스로 다수 인원이 경쟁 (게임, 퍼즐)", "personality": "외향형, 이성형" }, { "item_id": 48, "title": "'비트세이버' 리듬 VR 체험", "desc": "VR 기기를 착용해 박자에 맞춰 블록을 베는 리듬 대결 (게임, 음악/리듬)", "personality": "내향형, 감정형" }, { "item_id": 49, "title": "'매직: 더 개더링' 카드 덱실험", "desc": "TCG의 원조격인 ‘매직’에서 덱 빌드와 대전 전략을 연구 (게임, 카드)", "personality": "내향형, 이성형" }, { "item_id": 50, "title": "'월드 오브 워크래프트' MMORPG 길드레이드", "desc": "아제로스에서 길드원이 힘을 합쳐 레이드를 진행 (게임, MMORPG)", "personality": "외향형, 이성형" }, # ========= 51~60: 공예 (뜨개질, 자수, 도자기, 가죽공예, 목공예, 비즈공예, 캔들/디퓨저, 페이퍼크래프트, 마크라메, 레진아트) ========= { "item_id": 51, "title": "코바늘 뜨개질 애착인형 DIY", "desc": "코바늘 기법으로 인형이나 소품을 만드는 초급자 대상 워크숍 (공예, 뜨개질)", "personality": "내향형, 감정형" }, { "item_id": 52, "title": "일본 자수 '사시코' 교실", "desc": "사시코 기법으로 앞치마·티매트를 장식하는 레슨 (공예, 자수)", "personality": "내향형, 이성형" }, { "item_id": 53, "title": "도자기 핸드페인팅 체험", "desc": "빚은 도자기에 직접 그림·문양을 그려 소성까지 경험 (공예, 도자기)", "personality": "내향형, 이성형" }, { "item_id": 54, "title": "가죽 파우치 제작 스튜디오", "desc": "가죽 재단·바느질·마감까지 소형 파우치를 만드는 실습 (공예, 가죽공예)", "personality": "내향형, 이성형" }, { "item_id": 55, "title": "목공예 숟가락 만들기 워크숍", "desc": "나무를 깎고 다듬어 주방용 숟가락을 만드는 기초 체험 (공예, 목공예)", "personality": "내향형, 이성형" }, { "item_id": 56, "title": "비즈공예 액세서리 디자인", "desc": "비즈 구슬로 팔찌·목걸이를 만들고 색감 패턴을 익히는 클래스 (공예, 비즈공예)", "personality": "내향형, 감정형" }, { "item_id": 57, "title": "젤 캔들 & 디퓨저 아틀리에", "desc": "투명 젤 왁스로 캔들과 디퓨저를 창작하며 향기를 디자인 (공예, 캔들/디퓨저)", "personality": "내향형, 이성형" }, { "item_id": 58, "title": "페이퍼크래프트 동물 피규어", "desc": "종이로 입체 동물 모형을 만들고 채색·디테일 작업 (공예, 페이퍼크래프트)", "personality": "내향형, 이성형" }, { "item_id": 59, "title": "마크라메 해먹 의자 만들기", "desc": "매듭 기법을 이용해 해먹 의자·월행잉 등 인테리어 소품 제작 (공예, 마크라메)", "personality": "내향형, 이성형" }, { "item_id": 60, "title": "레진아트 트레이 공방", "desc": "레진과 색소·글리터로 컵받침이나 작은 트레이를 만드는 작업 (공예, 레진아트)", "personality": "내향형, 이성형" }, # ========== 61~71: 추가 11개 자유 항목 (임의 분야) ========== { "item_id": 61, "title": "보드게임 '카탄' 챌린지", "desc": "전략 보드게임 '카탄' 대회로 무역·개척 전술을 겨루는 이벤트 (기타, 보드게임)", "personality": "외향형, 이성형" }, { "item_id": 62, "title": "스페인어 기초 회화 교실", "desc": "hola! 간단한 문장·회화를 배워 여행·일상에서 활용 (기타, 언어학습)", "personality": "내향형, 감정형" }, { "item_id": 63, "title": "홈브루잉 커피 실습", "desc": "원두 고르기부터 핸드드립·에스프레소 추출까지 커피 제작 전과정 체험 (기타, 요리/음료)", "personality": "내향형, 이성형" }, { "item_id": 64, "title": "K-POP 댄스 커버 스튜디오", "desc": "최신 아이돌 안무를 함께 연습·촬영해보는 퍼포먼스 프로젝트 (기타, 댄스)", "personality": "외향형, 감정형" }, { "item_id": 65, "title": "플라워 레슨 & 꽃다발 디자인", "desc": "생화를 활용해 부케·꽃다발을 직접 만드는 교실 (기타, 플라워)", "personality": "내향형, 감정형" }, { "item_id": 66, "title": "드론 항공촬영 워크숍", "desc": "드론 조종법·촬영 기법·영상 편집을 배우고 실습 (기타, 드론)", "personality": "외향형, 이성형" }, { "item_id": 67, "title": "비건 베이커리 클래스", "desc": "달걀·버터 없이 쿠키·빵을 만드는 레시피와 노하우 학습 (기타, 요리/베이킹)", "personality": "내향형, 이성형" }, { "item_id": 68, "title": "애견 행동교정 & 트릭 클래스", "desc": "반려견 기본 행동교정·산책 요령·간단한 재주를 훈련 (기타, 반려동물)", "personality": "외향형, 감정형" }, { "item_id": 69, "title": "영화 시나리오 작법 워크숍", "desc": "시놉시스·캐릭터 구성을 배우고 단편 시나리오를 써보는 활동 (기타, 글쓰기)", "personality": "내향형, 이성형" }, { "item_id": 70, "title": "홈칵테일 믹솔로지 이벤트", "desc": "칵테일 재료·비율을 익혀 집에서 간단히 만들 수 있는 레시피 실습 (기타, 요리/음료)", "personality": "외향형, 감정형" }, { "item_id": 71, "title": "유튜브 브이로그 크리에이팅", "desc": "영상 기획·촬영·편집 노하우를 배우고 브이로그를 완성 (기타, 영상콘텐츠)", "personality": "내향형, 이성형" }, # ==== (운동 - 12개 대응, item_id 73~84) ==================================== {"item_id": 73, "title": "크로스핏 열정단", "desc": "고강도 크로스핏 운동으로 체력을 단련하는 열정적 모임 (온동, 헬스장)", "personality": "외향형, 감정형"}, {"item_id": 74, "title": "명상 요가 서클", "desc": "몸의 이완과 마음의 평화를 함께 추구하는 명상 요가 모임 (운동, 요가)", "personality": "외향형, 감정형"}, {"item_id": 75, "title": "코어 트레이닝 팸", "desc": "필라테스로 코어 근력과 유연성을 키우는 그룹 (운동, 필라테스)", "personality": "외향형, 감정형"}, {"item_id": 76, "title": "파도타기 워터팀", "desc": "수영과 워터 스포츠를 함께 즐기며 체력을 키우는 모임 (운동, 수영)", "personality": "외향형, 감정형"}, {"item_id": 77, "title": "라켓 드림클럽", "desc": "테니스를 비롯한 라켓 스포츠를 같이 배우고 즐기는 동호회 (운동, 테니스)", "personality": "외향형, 감정형"}, {"item_id": 78, "title": "필드 골프 리더스", "desc": "골프 스윙 기술과 필드 매너를 함께 익히는 라운딩 팀 (운동, 골프)", "personality": "외향형, 감정형"}, {"item_id": 79, "title": "슬링 텀블러즈", "desc": "암벽등반과 슬링 트레이닝으로 색다른 운동을 체험하는 그룹 (운동, 클라이밍)", "personality": "외향형, 감정형"}, {"item_id": 80, "title": "스포츠 골든리그", "desc": "주중·주말에 축구 경기를 주기적으로 여는 커뮤니티 (운동, 축구)", "personality": "외향형, 감정형"}, {"item_id": 81, "title": "하프코트 슈터스", "desc": "농구 코트에서 함께 뛰고 실력을 배양하는 팀 (운동, 농구)", "personality": "외향형, 감정형"}, {"item_id": 82, "title": "스트라이크 러버스", "desc": "볼링 점수를 올리기 위해 함께 연구하고 즐기는 동아리 (운동, 볼링)", "personality": "외향형, 감정형"}, {"item_id": 83, "title": "스매싱 배드민턴", "desc": "배드민턴 스매시 기술과 경기를 함께 연습하는 학습 모임 (운동, 배드민턴)", "personality": "외향형, 감정형"}, {"item_id": 84, "title": "나이트 러너스", "desc": "야간 러닝을 통해 도시를 누비며 체력을 기르는 크루 (운동, 러닝)", "personality": "외향형, 감정형"}, # ==== (여행 - 8개 대응, item_id 85~92) ===================================== {"item_id": 85, "title": "오지여행 탐험대", "desc": "국내 숨겨진 오지를 찾아 떠나며 모험심을 기르는 모임 (여행, 국내여행)", "personality": "외향형, 감정형"}, {"item_id": 86, "title": "글로벌 백팩 트립", "desc": "해외 배낭여행 루트를 함께 계획하고 실행하는 동호회 (여행, 해외여행)", "personality": "외향형, 감정형"}, {"item_id": 87, "title": "필드 캠핑 어드벤처", "desc": "백패킹과 캠핑을 결합해 자연에서 살아보는 체험 모임 (여행, 백패킹)", "personality": "외향형, 감정형"}, {"item_id": 88, "title": "패밀리 캠핑 잼", "desc": "가족 단위로 캠핑하며 자연 속 소통을 즐기는 그룹 (여행, 캠핑)", "personality": "외향형, 감정형"}, {"item_id": 89, "title": "골목길 투어 클럽", "desc": "도시 골목길과 역사적 공간을 함께 탐방하는 여행 서클 (여행, 도시여행)", "personality": "외향형, 감정형"}, {"item_id": 90, "title": "미식헌터 크루", "desc": "전국 맛집을 조사하고 직접 시식 투어를 떠나는 모임 (여행, 맛집탐방)", "personality": "외향형, 감정형"}, {"item_id": 91, "title": "문화유산 탐방기획사", "desc": "전통 문화유산과 축제를 기획·탐방하는 여행 동아리 (여행, 문화탐방)", "personality": "외향형, 감정형"}, {"item_id": 92, "title": "숲속 힐링 트래블", "desc": "자연 속에서 치유와 재충전을 목적으로 여행하는 그룹 (여행, 힐링여행)", "personality": "외향형, 감정형"}, # ==== (독서 - 10개 대응, item_id 93~102) ================================== {"item_id": 93, "title": "문학 리뷰 서클", "desc": "소설과 문학작품을 읽고 비평하는 책동아리 (독서, 소설)", "personality": "내향형, 감정형"}, {"item_id": 94, "title": "시인 꿈나무 모임", "desc": "서로의 시를 낭독하고 공유하는 창작 시클럽 (독서, 시)", "personality": "내향형, 감정형"}, {"item_id": 95, "title": "마음글쓰기 에세이회", "desc": "에세이로 일상과 감정을 표현하며 소통하는 글모임 (독서, 에세이)", "personality": "내향형, 이성형"}, {"item_id": 96, "title": "실천형 자기계발단", "desc": "자기계발서를 읽고 즉시 실천 계획을 세우는 스터디 (독서, 자기계발)", "personality": "내향형, 이성형"}, {"item_id": 97, "title": "깊이읽기 인문소사이어티", "desc": "인문학 텍스트를 심층 토론하며 사고력을 확장하는 그룹 (독서, 인문)", "personality": "내향형, 이성형"}, {"item_id": 98, "title": "역사공부 디스커션", "desc": "시대별 역사적 사건을 공부하고 자유롭게 논의하는 모임 (독서, 역사)", "personality": "내향형, 이성형"}, {"item_id": 99, "title": "과학탐구 북클럽", "desc": "최신 과학 서적을 중심으로 아이디어를 나누는 독서회 (독서, 과학)", "personality": "내향형, 이성형"}, {"item_id": 100, "title": "경영전략 독해팀", "desc": "경제/경영 관련 서적을 함께 분석하고 사례를 연구하는 그룹 (독서, 경제/경영)", "personality": "내향형, 이성형"}, {"item_id": 101, "title": "철학강독 세미나", "desc": "철학 원전을 읽고 핵심 개념을 토론하는 학술 모임 (독서, 철학)", "personality": "내향형, 이성형"}, {"item_id": 102, "title": "아트북 인사이트회", "desc": "예술 관련 서적을 통해 예술사와 작품 세계를 탐구하는 모임 (독서, 예술)", "personality": "내향형, 감정형"}, # ==== (영화 - 10개 대응, item_id 103~112) ================================= {"item_id": 103, "title": "낭만 영화 애호가들", "desc": "감성적인 로맨스 영화를 좋아하는 사람들의 모임 (영화, 로맨스)", "personality": "외향형, 감정형"}, {"item_id": 104, "title": "폭소 코미디 페스티벌", "desc": "웃음을 주제로 코미디 영화를 함께 보는 소모임 (영화, 코미디)", "personality": "외향형, 감정형, 이성형"}, {"item_id": 105, "title": "액션매니아즈", "desc": "박진감 넘치는 액션 영화의 매력을 해부하는 그룹 (영화, 액션)", "personality": "외향형, 감정형"}, {"item_id": 106, "title": "서스펜스 시네마분석단", "desc": "스릴러 영화 속 긴장감과 스토리를 분석하는 모임 (영화, 스릴러)", "personality": "외향형, 감정형"}, {"item_id": 107, "title": "호러매틱 스터디", "desc": "공포영화의 분위기와 연출을 집중 탐구하는 팀 (영화, 공포)", "personality": "외향형, 감정형"}, {"item_id": 108, "title": "SF 월드 디스커버리", "desc": "공상과학 영화를 보고 미래 기술과 세계관을 토론하는 모임 (영화, SF)", "personality": "외향형, 감정형"}, {"item_id": 109, "title": "판타지 이매지네이션", "desc": "환상적인 판타지 영화를 감상하며 아이디어를 공유하는 서클 (영화, 판타지)", "personality": "외향형, 감정형"}, {"item_id": 110, "title": "휴먼드라마 인사이트", "desc": "감동적인 드라마 장르 영화를 함께 보고 교훈을 찾는 모임 (영화, 드라마)", "personality": "외향형, 감정형"}, {"item_id": 111, "title": "애니 애호회", "desc": "다양한 애니메이션 영화를 정기 상영하며 감상하는 팬클럽 (영화, 애니메이션)", "personality": "외향형, 감정형"}, {"item_id": 112, "title": "다큐 리뷰 포럼", "desc": "다큐멘터리 영화를 보고 사회·환경 문제를 논의하는 모임 (영화, 다큐멘터리)", "personality": "외향형, 이성형"}, # ==== (게임 - 10개 대응, item_id 113~122) ================================= {"item_id": 113, "title": "RPG 모험단", "desc": "판타지 세계에서 함께 파티를 이뤄 모험하는 RPG 팀 (게임, RPG)", "personality": "외향형, 이성형"}, {"item_id": 114, "title": "FPS 고인물 클랜", "desc": "고난도 슈팅 기술을 연습하며 대회에도 나가는 FPS 동호회 (게임, FPS)", "personality": "외향형, 이성형"}, {"item_id": 115, "title": "액션코어 전략팀", "desc": "액션 게임의 숨겨진 테크닉과 콤보를 연구하는 그룹 (게임, 액션)", "personality": "외향형, 이성형"}, {"item_id": 116, "title": "전략모드 시뮬클럽", "desc": "전략 시뮬레이션 장르를 심도 있게 플레이·분석하는 동호회 (게임, 전략)", "personality": "외향형, 이성형"}, {"item_id": 117, "title": "시뮬레이션 마니아들", "desc": "도시건설, 경영 시뮬레이션 등 다양한 시뮬 게임을 즐기는 모임 (게임, 시뮬레이션)", "personality": "외향형, 이성형, 감정형"}, {"item_id": 118, "title": "스포츠게이머 챌린지", "desc": "축구·농구 등 스포츠게임 리그를 운영하는 커뮤니티 (게임, 스포츠)", "personality": "외향형, 이성형"}, {"item_id": 119, "title": "퍼즐 브레인홀릭", "desc": "퍼즐·두뇌게임을 함께 풀며 아이디어를 공유하는 팀 (게임, 퍼즐)", "personality": "외향형, 이성형"}, {"item_id": 120, "title": "리듬게임 오케스트라", "desc": "음악/리듬게임에서 합주하는 느낌으로 협력 플레이하는 모임 (게임, 음악/리듬)", "personality": "외향형, 이성형"}, {"item_id": 121, "title": "카드플레이 매니아", "desc": "포커·TCG 등 카드 기반 게임을 심도 있게 즐기는 동아리 (게임, 카드)", "personality": "외향형, 이성형"}, {"item_id": 122, "title": "MMORPG 연합길드", "desc": "대규모 온라인 RPG에서 길드원들과 함께 도전하는 팀 (게임, MMORPG)", "personality": "외향형, 이성형"}, # ==== (공예 - 10개 대응, item_id 123~132) ================================= {"item_id": 123, "title": "니트디자인 스터디오", "desc": "뜨개질로 패션 소품과 의류를 창작하는 워크숍 (공예, 뜨개질)", "personality": "내향형, 이성형"}, {"item_id": 124, "title": "자수 디테일연구회", "desc": "섬세한 자수 기법을 연구하며 작품을 완성하는 모임 (공예, 자수)", "personality": "내향형, 이성형"}, {"item_id": 125, "title": "도예 공예단", "desc": "도자기 공예로 식기·인테리어 소품을 제작해보는 그룹 (공예, 도자기)", "personality": "내향형, 이성형"}, {"item_id": 126, "title": "가죽공예 스킬랩", "desc": "가죽 재단부터 바느질까지 실습하고 디자인을 공유하는 모임 (공예, 가죽공예)", "personality": "내향형, 이성형"}, {"item_id": 127, "title": "목공 마스터스", "desc": "목재를 활용해 가구·장식품을 제작하며 기술을 배우는 모임 (공예, 목공예)", "personality": "내향형, 이성형"}, {"item_id": 128, "title": "비즈 디자인 팩토리", "desc": "비즈공예로 액세서리·장신구를 창작하는 교실 (공예, 비즈공예)", "personality": "내향형, 이성형"}, {"item_id": 129, "title": "캔들/디퓨저 크리에이티브", "desc": "향초와 디퓨저를 예술적으로 표현해보는 창작 스튜디오 (공예, 캔들/디퓨저)", "personality": "내향형, 이성형"}, {"item_id": 130, "title": "페이퍼아트 디자인랩", "desc": "종이 공예로 독특한 작품과 미니어처를 만드는 연구회 (공예, 페이퍼크래프트)", "personality": "내향형, 이성형"}, {"item_id": 131, "title": "마크라메 공예실", "desc": "마크라메 매듭 기법을 다양하게 시도해보는 작업실 (공예, 마크라메)", "personality": "내향형, 이성형"}, {"item_id": 132, "title": "레진아트 크리에이터즈", "desc": "레진을 활용해 장식품·소품을 디자인하고 공유하는 모임 (공예, 레진아트)", "personality": "내향형, 이성형"}, # ===== [133~144] 운동 (12개) { "item_id": 133, "title": "나이키 헬스 피트니스 챌린지", "desc": "헬스장에서 나이키 프로그램을 통해 웨이트 및 유산소를 마스터 (운동, 헬스)", "personality": "외향형, 감정형" }, { "item_id": 134, "title": "아쉬탕가 요가 클래스 in 발리", "desc": "발리 자연 속에서 명상·아사나 요가를 심도 있게 체험 (운동, 요가)", "personality": "내향형, 감정형" }, { "item_id": 135, "title": "리포머 필라테스 스튜디오", "desc": "기구 필라테스로 자세 교정과 코어 강화에 집중 (운동, 필라테스)", "personality": "내향형, 이성형" }, { "item_id": 136, "title": "아쿠아 스위밍 프로젝트", "desc": "실내 수영장에서 영법·호흡법을 익히고 오픈워터 도전 (운동, 수영)", "personality": "외향형, 감정형" }, { "item_id": 137, "title": "윔블던 테니스 레슨", "desc": "프로 코치와 함께 랠리·발리를 집중 훈련, 미니 매치 진행 (운동, 테니스)", "personality": "외향형, 이성형" }, { "item_id": 138, "title": "스크린 골프 라운딩", "desc": "필드 대신 스크린 골프로 스윙 교정하고 퍼팅 실력을 키움 (운동, 골프)", "personality": "내향형, 이성형" }, { "item_id": 139, "title": "실내 클라이밍 챌린지", "desc": "암벽등반 센터에서 다양한 코스를 공략하며 근지구력 향상 (운동, 클라이밍)", "personality": "외향형, 감정형" }, { "item_id": 140, "title": "풋볼 매치데이 이벤트", "desc": "축구 팬들이 모여 포메이션 짜고 미니 경기를 즐기는 일정 (운동, 축구)", "personality": "외향형, 감정형" }, { "item_id": 141, "title": "NBA 농구 액션타임", "desc": "코트에서 드리블·슛을 연습하고 3:3 스크리미지를 진행 (운동, 농구)", "personality": "외향형, 감정형" }, { "item_id": 142, "title": "락볼링 스트라이크 파티", "desc": "볼링장에서 음악과 함께 스트라이크 확률을 높이는 교류전 (운동, 볼링)", "personality": "내향형, 감정형" }, { "item_id": 143, "title": "셔틀콕 배드민턴 교실", "desc": "배드민턴 스매시·푸트워크 집중 연습 후 미니 토너먼트 (운동, 배드민턴)", "personality": "외향형, 감정형" }, { "item_id": 144, "title": "하프마라톤 러닝 레이스", "desc": "도심 10km·21km 레이스에 도전하며 기록 향상을 목표 (운동, 러닝)", "personality": "내향형, 감정형" }, # ===== [145~152] 여행 (8개) { "item_id": 145, "title": "강원도 로드트립", "desc": "차로 강원도 해안·산골 명소를 돌아보며 자연을 만끽 (여행, 국내여행)", "personality": "내향형, 감정형" }, { "item_id": 146, "title": "유럽 배낭 자유여행", "desc": "파리·로마·바르셀로나를 포함해 유럽 도시들을 여행 (여행, 해외여행)", "personality": "외향형, 감정형" }, { "item_id": 147, "title": "지리산 백패킹 트레일", "desc": "배낭 하나로 지리산 둘레길을 걸으며 자연 속 야영 (여행, 백패킹)", "personality": "내향형, 감정형" }, { "item_id": 148, "title": "캠핑 파이어나이트", "desc": "텐트 치고 모닥불을 즐기며 가족·친구와 소통하는 프로그램 (여행, 캠핑)", "personality": "외향형, 감정형" }, { "item_id": 149, "title": "서울 골목투어", "desc": "익선동·을지로 등 오래된 골목길 카페·맛집을 탐방 (여행, 도시여행)", "personality": "외향형, 감정형" }, { "item_id": 150, "title": "전주 한옥마을 미식 탐방", "desc": "전주의 비빔밥·막걸리·한정식을 둘러보는 식도락 여행 (여행, 맛집탐방)", "personality": "외향형, 감정형" }, { "item_id": 151, "title": "경복궁 문화탐방", "desc": "왕궁·전통 공연·박물관을 돌며 한국 역사를 배우는 코스 (여행, 문화탐방)", "personality": "내향형, 이성형" }, { "item_id": 152, "title": "제주 오름 힐링투어", "desc": "제주 오름·용암동굴을 걸으며 자연 속에서 재충전 (여행, 힐링여행)", "personality": "내향형, 감정형" }, # ===== [153~162] 독서 (10개) { "item_id": 153, "title": "무라카미 '1Q84' 북클럽", "desc": "하루키 장편소설을 함께 읽고 상징·메타포를 토론 (독서, 소설)", "personality": "내향형, 감정형" }, { "item_id": 154, "title": "윤동주 시 낭독회", "desc": "‘하늘과 바람과 별과 시’ 등 윤동주 시를 낭송하며 문학적 감성을 공유 (독서, 시)", "personality": "내향형, 감정형" }, { "item_id": 155, "title": "김영하 '여행의 이유' 에세이 살롱", "desc": "여행과 삶의 관계를 에세이로 탐구하고 느낀 점을 나누는 시간 (독서, 에세이)", "personality": "내향형, 감정형" }, { "item_id": 156, "title": "스티븐 코비 '성공하는 사람들의 7가지 습관'", "desc": "자기계발서를 함께 읽고 실천 전략을 세우는 성장 프로젝트 (독서, 자기계발)", "personality": "내향형, 이성형" }, { "item_id": 157, "title": "인문학 토크: '총, 균, 쇠' & '사피엔스'", "desc": "문명의 발전 과정을 다룬 명저를 비교·분석하는 심층 독서 (독서, 인문)", "personality": "내향형, 이성형" }, { "item_id": 158, "title": "유발 하라리 '사피엔스' 역사 세션", "desc": "인류 역사를 조망하는 '사피엔스'를 읽고 현재 사회와 연결 (독서, 역사)", "personality": "내향형, 이성형" }, { "item_id": 159, "title": "칼 세이건 '코스모스' 과학 독서모임", "desc": "우주와 과학적 사고를 확장해보는 '코스모스' 함께 읽기 (독서, 과학)", "personality": "내향형, 이성형" }, { "item_id": 160, "title": "애덤 스미스 '국부론' 경제포럼", "desc": "고전 경제학을 토대로 시장경제 원리를 이해하는 세미나 (독서, 경제/경영)", "personality": "내향형, 이성형" }, { "item_id": 161, "title": "니체 '차라투스트라는 이렇게 말했다' 철학 토론", "desc": "니체 철학을 깊이 있게 탐독하고 현대적 의미를 해석 (독서, 철학)", "personality": "내향형, 이성형" }, { "item_id": 162, "title": "빈센트 반 고흐 예술책 리뷰", "desc": "화가 반 고흐의 작품 세계를 미술서적으로 접하고 교감 (독서, 예술)", "personality": "내향형, 감정형" }, # ===== [163~172] 영화 (10개) { "item_id": 163, "title": "'러브 액츄얼리' 로맨스 상영회", "desc": "여러 커플의 사랑 이야기를 담은 '러브 액츄얼리'를 감상 후 토론 (영화, 로맨스)", "personality": "외향형, 감정형" }, { "item_id": 164, "title": "코미디 '미스터 빈' 특별전", "desc": "로완 앳킨슨의 바디 코미디를 감상하며 웃음 포인트를 분석 (영화, 코미디)", "personality": "외향형, 감정형" }, { "item_id": 165, "title": "'매드 맥스: 분노의 도로' 액션 토크", "desc": "포스트 아포칼립스 배경의 질주 액션을 해부하는 상영회 (영화, 액션)", "personality": "외향형, 감정형" }, { "item_id": 166, "title": "스릴러 '겟 아웃' 심리분석", "desc": "조던 필의 '겟 아웃'을 통해 공포와 사회적 메시지를 살피는 이벤트 (영화, 스릴러)", "personality": "외향형, 감정형" }, { "item_id": 167, "title": "공포 '컨저링' 오싹 체험", "desc": "심령현상을 다룬 공포영화 '컨저링'으로 사운드·분위기 연출을 탐구 (영화, 공포)", "personality": "외향형, 감정형" }, { "item_id": 168, "title": "'인터스텔라' SF 디스커션", "desc": "크리스토퍼 놀란의 우주·중력 이론을 담은 영화를 보고 과학적 설정 토론 (영화, SF)", "personality": "외향형, 감정형" }, { "item_id": 169, "title": "'반지의 제왕' 판타지 데이", "desc": "톨킨 세계관을 기반으로 한 대서사시를 연속 상영·분석 (영화, 판타지)", "personality": "외향형, 감정형" }, { "item_id": 170, "title": "'쇼생크 탈출' 드라마 감동", "desc": "인생영화 '쇼생크 탈출'을 보고 희망·인내의 메시지를 토론 (영화, 드라마)", "personality": "외향형, 감정형" }, { "item_id": 171, "title": "지브리 애니메이션 스페셜", "desc": "'센과 치히로' 등 지브리 작품을 상영하며 판타지·동심을 느끼는 감상회 (영화, 애니메이션)", "personality": "외향형, 감정형" }, { "item_id": 172, "title": "다큐 '지구(Earth)' 환경 포럼", "desc": "BBC 제작 다큐멘터리 '지구'를 보고 자연·환경 이슈를 논의 (영화, 다큐멘터리)", "personality": "외향형, 이성형" }, # ===== [173~182] 게임 (10개) { "item_id": 173, "title": "'엘든 링' RPG 도전", "desc": "프롬소프트웨어의 ‘엘든 링’에서 난이도 극복 전략과 빌드 연구 (게임, RPG)", "personality": "외향형, 이성형" }, { "item_id": 174, "title": "'오버워치2' FPS 팀플", "desc": "‘오버워치2’에서 역할 분담·전술 플레이를 협력하는 대전 (게임, FPS)", "personality": "외향형, 이성형" }, { "item_id": 175, "title": "'데빌 메이 크라이' 액션 콤보 연구", "desc": "콤보 액션의 정수 ‘데메크’ 시리즈를 플레이하며 기술을 익힘 (게임, 액션)", "personality": "외향형, 이성형" }, { "item_id": 176, "title": "'스타크래프트' 전략 빌드토론", "desc": "테란·저그·프로토스 종족별 빌드 오더·리플레이 분석 (게임, 전략)", "personality": "내향형, 이성형" }, { "item_id": 177, "title": "'심시티' 시뮬레이션 건설", "desc": "도시건설 게임 ‘심시티’로 자신만의 도시를 디자인하고 운영 (게임, 시뮬레이션)", "personality": "내향형, 감정형" }, { "item_id": 178, "title": "'피파23' 스포츠 대전", "desc": "축구게임 ‘피파23’에서 오프라인 리그전을 열고 순위를 집계 (게임, 스포츠)", "personality": "외향형, 이성형" }, { "item_id": 179, "title": "'테트리스99' 퍼즐 배틀로얄", "desc": "닌텐도 스위치 ‘테트리스99’로 다수 인원이 동시에 경쟁 (게임, 퍼즐)", "personality": "외향형, 이성형" }, { "item_id": 180, "title": "'DJMAX 리스펙트 V' 리듬 페스티벌", "desc": "국산 리듬게임 DJMAX로 콤보와 노트 패턴에 도전 (게임, 음악/리듬)", "personality": "외향형, 이성형" }, { "item_id": 181, "title": "'하스스톤' 카드 덱실험실", "desc": "블리자드 TCG ‘하스스톤’에서 전략적 덱 구성과 토너먼트 플레이 (게임, 카드)", "personality": "외향형, 이성형" }, { "item_id": 182, "title": "'로스트아크' MMORPG 레이드", "desc": "대규모 온라인 RPG ‘로스트아크’에서 길드원과 협동해 레이드를 공략 (게임, MMORPG)", "personality": "외향형, 이성형" }, # ===== [183~192] 공예 (10개) { "item_id": 183, "title": "니트 코바늘 인형 만들기", "desc": "뜨개질 기법으로 귀여운 인형이나 소품을 제작하는 워크숍 (공예, 뜨개질)", "personality": "내향형, 이성형" }, { "item_id": 184, "title": "프랑스자수 브로치 교실", "desc": "세심한 자수로 브로치·미니 파우치를 장식하는 공예 클래스 (공예, 자수)", "personality": "내향형, 감정형" }, { "item_id": 185, "title": "도예 핸드빌딩 체험", "desc": "흙을 손으로 빚어 머그·접시를 만드는 도자기 공예 (공예, 도자기)", "personality": "내향형, 이성형" }, { "item_id": 186, "title": "가죽공예 카드지갑 제작", "desc": "가죽 재단·바느질·엣지 마감 실습으로 카드지갑을 완성 (공예, 가죽공예)", "personality": "내향형, 이성형" }, { "item_id": 187, "title": "나무도마 목공 교실", "desc": "원목을 가공·샌딩하여 주방용 나무도마를 제작하는 기초 체험 (공예, 목공예)", "personality": "내향형, 이성형" }, { "item_id": 188, "title": "비즈공예 액세서리 디자인", "desc": "비즈 구슬로 팔찌·목걸이를 만들고 색채 감각을 익히는 클래스 (공예, 비즈공예)", "personality": "내향형, 감정형" }, { "item_id": 189, "title": "소이 캔들 & 디퓨저 만들기", "desc": "천연 소이왁스로 향초, 디퓨저를 제작해 감성 분위기를 조성 (공예, 캔들/디퓨저)", "personality": "내향형, 이성형" }, { "item_id": 190, "title": "페이퍼크래프트 3D 미니어처", "desc": "종이 공예로 입체 구조물을 제작하고 채색·디테일을 살리는 교실 (공예, 페이퍼크래프트)", "personality": "내향형, 이성형" }, { "item_id": 191, "title": "마크라메 인테리어 월행잉", "desc": "매듭 기법을 응용해 벽걸이·화분홀더 등 인테리어 소품을 만드는 실습 (공예, 마크라메)", "personality": "내향형, 이성형" }, { "item_id": 192, "title": "레진아트 키링 & 액세서리", "desc": "투명 레진에 색소·글리터를 활용해 키링·반지를 만드는 창작 (공예, 레진아트)", "personality": "내향형, 이성형" } ] ##################################### # 2) 아이템 임베딩 로직 ##################################### def make_item_embedding_dict(items, model): item_embedding_dict = {} for item in items: text = f"{item['title']} {item['desc']}" emb = model.encode(text, convert_to_numpy=True) item_embedding_dict[item['item_id']] = emb return item_embedding_dict item_embedding_dict = make_item_embedding_dict(items, model_bert) ##################################### # 3) 사용자 프로필 -> 문장화 ##################################### def make_user_profile_text(user_profile: dict) -> str: ext = user_profile.get("extroversion", "") ft = user_profile.get("feeling_thinking", "") hobby = user_profile.get("hobby", "") detail = user_profile.get("detail_hobby", "") if hobby == "여행": return (f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"여행을 정말 좋아하며, 특히 {detail}을 즐깁니다. " "다양한 문화와 맛집을 직접 체험하고 싶어합니다.") elif hobby == "운동": return (f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"운동에 열정을 가지고 있으며, 특히 {detail}을 즐깁니다. " "건강과 체력을 증진시키고자 꾸준히 활동합니다.") elif hobby == "독서": return (f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"독서를 사랑하며, 특히 {detail}에 깊은 관심을 가지고 있습니다. " "새로운 지식과 다양한 관점을 배우고자 합니다.") elif hobby == "영화": return (f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"영화를 감상하는 것을 좋아하며, 특히 {detail} 장르에 큰 흥미를 느낍니다. " "감동과 재미를 동시에 경험하고 싶어합니다.") elif hobby == "게임": return (f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"게임을 즐기며, 특히 {detail} 게임에 많은 관심을 가지고 있습니다. " "전략적 사고와 도전을 통해 성취감을 느끼고자 합니다.") elif hobby == "공예": return (f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"공예 활동에 열정을 가지고 있으며, 특히 {detail}을 즐깁니다. " "창의적인 아이디어를 실현하며 섬세한 작업을 즐깁니다.") else: return f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. 다양한 취미를 즐기고 있습니다." ##################################### # 2) 아이템 임베딩 로직 ##################################### def make_item_embedding_dict(items, model): item_embedding_dict = {} for item in items: text = f"{item['title']} {item['desc']}" emb = model.encode(text, convert_to_numpy=True) item_embedding_dict[item['item_id']] = emb return item_embedding_dict item_embedding_dict = make_item_embedding_dict(items, model_bert) ##################################### # 3) 사용자 프로필 -> 문장화 (다중 취미 대응) ##################################### def make_user_profile_text(user_profile: dict) -> str: ext = user_profile.get("extroversion", "") ft = user_profile.get("feeling_thinking", "") # 🟢 hobby, detail_hobby를 리스트로 받는다고 가정 hobby_list = user_profile.get("hobby", []) detail_list = user_profile.get("detail_hobby", []) # 리스트를 문자열로 합치기 # 예: hobby_list=["운동","독서"] → "운동, 독서" hobby_str = ", ".join(hobby_list) if hobby_list else "" detail_str = ", ".join(detail_list) if detail_list else "" # 간단히 하나의 문장으로 구성 # 필요하다면 취미마다 별도 문장 등을 추가 가능 text = ( f"저는 {ext}이며 {ft}한 성향을 가진 사람입니다. " f"주요 취미로는 {hobby_str}를 즐기고, " f"특히 {detail_str} 분야에 관심이 많습니다." ) return text ##################################### # 4) 사용자 임베딩 + 추천 로직 (다중 취미 대응) ##################################### def get_user_embedding(user_profile: dict): user_text = make_user_profile_text(user_profile) return model_bert.encode(user_text, convert_to_numpy=True) def extract_hobby(desc): """ 예: "PT 전문 코치... (운동, 헬스)" -> "운동, 헬스" """ match = re.search(r"\((.*?)\)", desc) return match.group(1) if match else "" def cosine_similarity(vec1, vec2): norm1 = np.linalg.norm(vec1) norm2 = np.linalg.norm(vec2) if norm1 == 0 or norm2 == 0: return 0.0 return float(np.dot(vec1, vec2) / (norm1 * norm2)) def recommend_content_based(user_profile: dict, top_n=5): user_emb = get_user_embedding(user_profile) scored = [] seen_items = set() # 중복 방지를 위한 집합 # 🟢 다중 취미/세부취미 지원 user_hobbies = user_profile.get("hobby", []) or [] user_details = user_profile.get("detail_hobby", []) or [] user_extroversion = user_profile.get("extroversion", "") user_feeling_thinking = user_profile.get("feeling_thinking", "") for item in items: item_id = item["item_id"] if item_id in seen_items: # 중복 방지 continue item_emb = item_embedding_dict[item_id] sim = cosine_similarity(user_emb, item_emb) # 기본 가중치 weight = 1.0 # (1) 취미 가중치 desc_hobby = extract_hobby(item["desc"]) # 예: "(운동, 헬스)" for h in user_hobbies: if h in desc_hobby: weight *= 1.15 # (2) 세부 취미 가중치 for dh in user_details: if dh in desc_hobby: weight *= 1.3 # (3) 성향 가중치 (외향형/내향형, 감정형/이성형) personality_match_count = sum( trait in item["personality"] for trait in [user_extroversion, user_feeling_thinking] ) if personality_match_count == 1: weight *= 1.2 elif personality_match_count == 2: weight *= 1.3 final_score = sim * weight scored.append((item, final_score)) # 🟢 점수가 높은 순으로 정렬 scored.sort(key=lambda x: x[1], reverse=True) # 🟢 상위 5개를 확보 (중복 없이) selected_items = [] for (item, score) in scored: if len(selected_items) >= top_n: break if item["item_id"] not in seen_items: seen_items.add(item["item_id"]) selected_items.append((item, score)) return selected_items # 추천 개수가 부족하다면, 나머지 추천을 추가 if len(balanced_recommendations) < top_n: for item, score in scored: if len(balanced_recommendations) < top_n: balanced_recommendations.append((item, score)) else: break return balanced_recommendations ##################################### # 5) 챗봇 로직 ##################################### HF_API_KEY = os.environ.get("HF_API_KEY", "YOUR_HF_API_KEY") API_URL = "https://api-inference.huggingface.co/models/Chanjeans/tfchatbot_2" HEADERS = {"Authorization": f"Bearer {HF_API_KEY}"} def chat_response(user_input, mode="emotion", max_retries=5): if mode not in ["emotion", "rational"]: raise HTTPException(status_code=400, detail="mode는 'emotion' 또는 'rational'이어야 합니다.") prompt = f"<{mode}>{user_input}" payload = { "inputs": prompt, "parameters": { "max_new_tokens": 128, "temperature": 0.7, "top_p": 0.9, "top_k": 50, "repetition_penalty": 1.2, "do_sample": True }, "options": {"wait_for_model": True} } for attempt in range(max_retries): response = requests.post(API_URL, headers=HEADERS, json=payload) if response.status_code == 200: try: result = response.json() if isinstance(result, list) and "generated_text" in result[0]: generated_text = result[0]["generated_text"] return generated_text.replace(prompt, "").strip() else: return "응답 형식이 예상과 다릅니다." except Exception as e: return f"JSON 파싱 오류: {e}" elif response.status_code == 503: # 모델 로딩 중 error_info = response.json() estimated_time = error_info.get("estimated_time", 15) time.sleep(min(estimated_time, 15)) else: return f"API Error: {response.status_code}, {response.text}" return "🚨 모델 로딩이 너무 오래 걸립니다. 잠시 후 다시 시도하세요." #우울분류 모델 추가 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") tokenizer = BertTokenizer.from_pretrained("monologg/kobert") bert_model = BertForSequenceClassification.from_pretrained("monologg/kobert", num_labels=2) bert_model.load_state_dict(torch.load("emotion_bert_model.pth", map_location=device)) bert_model.to(device) bert_model.eval() xgb_model = joblib.load("xgboost_model.pkl") vectorizer = joblib.load("tfidf_vectorizer.pkl") def predict_depression(text: str): encoding = tokenizer(text, truncation=True, padding="max_length", max_length=64, return_tensors="pt") input_ids = encoding["input_ids"].to(device) attention_mask = encoding["attention_mask"].to(device) with torch.no_grad(): outputs = bert_model(input_ids, attention_mask=attention_mask) probabilities = torch.nn.functional.softmax(outputs.logits, dim=1) kobert_score = probabilities[0][1].item() text_vec = vectorizer.transform([text]) xgb_proba = xgb_model.predict_proba(text_vec)[0][1] kobert_score = max(0.35, min(kobert_score, 0.88)) xgb_proba = max(0.3, min(xgb_proba, 0.83)) combined_score = (kobert_score * 0.55) + (xgb_proba * 0.45) if combined_score > 0.72: label = "상담 권장" elif combined_score > 0.68: label = "관심 필요" else: label = "정상" return combined_score, label ##################################### # 6) FastAPI Endpoint ##################################### ##################################### # 6) FastAPI Endpoint ##################################### # (1) 메인 페이지 @app.get("/") def home(): return {"message": "안녕하세요! 추천 & 챗봇 FastAPI 서버입니다."} # (2) 추천용 모델 class UserProfile(BaseModel): extroversion: str feeling_thinking: str hobby: str detail_hobby: str @app.post("/recommend") def recommend_api(profile: UserProfile): top_items = recommend_content_based(profile.dict(), top_n=5) results = [] for (item, score) in top_items: results.append({ "item_id": item["item_id"], "title": item["title"], "desc": item["desc"], "personality": item["personality"], "score": round(score, 4) }) return {"recommendations": results} # (3) 챗봇용 모델 + 다중 취미/세부취미 대응 class ChatRequest(BaseModel): user_input: str mode: str # "emotion" or "rational" # (4) 챗봇 + 추천 자동 분기용 모델 class ChatOrRecommendRequest(BaseModel): user_input: str # 사용자의 채팅 메시지 mode: str # "emotion" 또는 "rational" # 다중 선택 가능하도록 List[str]로 변경 extroversion: Optional[str] = None feeling_thinking: Optional[str] = None hobby: Optional[List[str]] = None detail_hobby: Optional[List[str]] = None # (5) 자동 분기 엔드포인트 @app.post("/chat_or_recommend") def chat_or_recommend(req: ChatOrRecommendRequest): """ 1) 만약 '추천' 키워드가 포함되어 있다면 → 추천만(return) 2) 없다면 → 우울분석 + 챗봇 응답 2-1) 우울도가 '상담 권장'이면 → 상담안내 + 챗봇만 2-2) 우울도가 상담 권장이 아니면 → 상담안내(필요 시) + 챗봇 + (취미 정보 있으면) 추천 """ user_input = req.user_input mode = req.mode.lower() # "emotion" or "rational" RECOMMEND_KEYWORDS = ["추천", "추천해줘", "취미 추천"] HI_KEYWORDS = ["안녕","안녕!","안녕~","안녕?"] is_hi_query = any(keyword in user_input for keyword in HI_KEYWORDS) # (A) '추천' 키워드 포함 여부 확인 if is_hi_query: return { "mode": "chat", "response": "안녕! 만나서 반가워. 오늘 너의 기분은 어땠어?" } is_recommend_query = any(keyword in user_input for keyword in RECOMMEND_KEYWORDS) # (B) '추천' 키워드가 있으면 → 추천만 수행 (챗봇/우울분석 X) if is_recommend_query: # 1) 사용자 프로필 생성 user_profile = { "extroversion": req.extroversion or "", "feeling_thinking": req.feeling_thinking or "", "hobby": req.hobby or [], "detail_hobby": req.detail_hobby or [], } # 2) 추천 로직 top_items = recommend_content_based(user_profile, top_n=5) rec_results = [] recommendation_msg = "너를 위한 추천 목록이야" for i, (item, score) in enumerate(top_items, start=1): clean_desc = re.sub(r"\(.*?\)", "", item["desc"]).strip() rec_results.append({ "item_id": item["item_id"], "title": item["title"], "desc": item["desc"], "personality": item["personality"], "score": round(score, 4) }) recommendation_msg += f"{i}. **{item['title']}** - {clean_desc}\n" return { "mode": "recommend_only", "response": recommendation_msg, "recommendations": rec_results } # (C) '추천' 키워드가 없는 경우 else: # 1) 우울 분석 depression_score, depression_label = predict_depression(user_input) # 2) 우울도가 '정상'이면 챗봇만 수행 if depression_label == "정상": chatbot_msg = chat_response(user_input, mode=mode) return { "mode": "chat", "response": chatbot_msg, "depression_label": depression_label } # 2) 상담 안내 메시지 준비 counseling_response = "" if depression_label == "상담 권장": counseling_response = ( "입력하신 메시지에서 심각한 우울 신호가 감지되었습니다. " "전문 상담을 받으실 것을 권장드립니다.\n\n" ) # 3) 챗봇 응답 if mode not in ["emotion", "rational"]: raise HTTPException( status_code=400, detail="mode는 'emotion' 또는 'rational'이어야 합니다." ) chatbot_msg = chat_response(user_input, mode=mode) # 4) 추천 메시지 준비 recommendation_msg = "" recommendations_list = [] # 우울도가 '상담 권장'이 아니고, 사용자가 hobby 정보가 있으면 → 추천 if depression_label == "관심 필요" and req.hobby: user_profile = { "extroversion": req.extroversion or "", "feeling_thinking": req.feeling_thinking or "", "hobby": req.hobby, "detail_hobby": req.detail_hobby or [], } top_items = recommend_content_based(user_profile, top_n=5) recommendation_msg = "너를 위한 맞춤 추천 목록을 알려줄게! ☺️" for i, (item, score) in enumerate(top_items, start=1): clean_desc = re.sub(r"\(.*?\)", "", item["desc"]).strip() recommendations_list.append({ "item_id": item["item_id"], "title": item["title"], "desc": item["desc"], "personality": item["personality"], "score": round(score, 4) }) recommendation_msg += f"{i}. **{item['title']}** - {clean_desc}\n" recommendation_msg += "\n이 중에서 어떤 활동이 가장 마음에 들어? 🌟" # 5) 최종 응답 결합 final_response = counseling_response + chatbot_msg # 6) 응답 JSON 구성 response_dict = { "mode": "chat+recommend" if recommendation_msg else "chat", "response": final_response, # 🔹 챗봇 응답만 포함 "depression_label": depression_label, "recommendation_msg": recommendation_msg if recommendation_msg else None, # 🔹 추천 메시지 별도 필드 "recommendations": recommendations_list if recommendations_list else None } if recommendation_msg: response_dict["recommendations"] = recommendations_list return response_dict #def run_fastapi(): # uvicorn.run(app, host="0.0.0.0", port=7860) #if __name__ == "__main__": # Thread(target=run_fastapi).start() # iface.launch(server_name="0.0.0.0", server_port=7861)