diff --git a/agentPersona/__pycache__/config.cpython-311.pyc b/agentPersona/__pycache__/config.cpython-311.pyc index d80c61f1..1439be8b 100644 Binary files a/agentPersona/__pycache__/config.cpython-311.pyc and b/agentPersona/__pycache__/config.cpython-311.pyc differ diff --git a/agentPersona/__pycache__/models.cpython-311.pyc b/agentPersona/__pycache__/models.cpython-311.pyc index 85a2f4c7..ea1a60eb 100644 Binary files a/agentPersona/__pycache__/models.cpython-311.pyc and b/agentPersona/__pycache__/models.cpython-311.pyc differ diff --git a/agentPersona/__pycache__/routes.cpython-311.pyc b/agentPersona/__pycache__/routes.cpython-311.pyc index f2d83ee6..d91cc22c 100644 Binary files a/agentPersona/__pycache__/routes.cpython-311.pyc and b/agentPersona/__pycache__/routes.cpython-311.pyc differ diff --git a/agentPersona/app4.py b/agentPersona/app4.py index a7c89623..91fbf09f 100644 --- a/agentPersona/app4.py +++ b/agentPersona/app4.py @@ -6,6 +6,7 @@ from routes import main_bp import os from dotenv import load_dotenv +from flask_sqlalchemy import SQLAlchemy # 환경 변수 로드 load_dotenv() @@ -24,7 +25,11 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = SQLALCHEMY_TRACK_MODIFICATIONS # DB 초기화 -db.init_app(app) +db.init_app(app) # Flask 앱과 연결 + +# 기존 테이블 메타데이터 반영 +with app.app_context(): + db.Model.metadata.reflect(bind=db.engine) # Spring Boot에서 관리하는 테이블 메타데이터 반영 # Blueprint 등록 app.register_blueprint(main_bp) @@ -37,4 +42,4 @@ db.create_all() # 테이블 생성 if __name__ == "__main__": - app.run(port=5000, debug=True) + app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/agentPersona/config.py b/agentPersona/config.py index f095b867..68cea479 100644 --- a/agentPersona/config.py +++ b/agentPersona/config.py @@ -1,7 +1,10 @@ # Flask 설정 import os -SQLALCHEMY_DATABASE_URI = 'sqlite:///../data/testdb.sqlite' +# SQLALCHEMY_DATABASE_URI = 'sqlite:///../data/testdb.sqlite' + +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) # 현재 파일의 절대경로 +SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(BASE_DIR, "../data/testdb.sqlite")}' SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = os.getenv("SECRET_KEY", "default_secret_key") SESSION_COOKIE_SECURE = False diff --git a/agentPersona/models.py b/agentPersona/models.py index 00318865..c856d174 100644 --- a/agentPersona/models.py +++ b/agentPersona/models.py @@ -1,9 +1,25 @@ from db import db class TravelPlan(db.Model): + __tablename__ = 'travel_plans' id = db.Column(db.Integer, primary_key=True, autoincrement=True) - # travel_date = db.Column(db.String(20)) - # travel_days = db.Column(db.Integer) - # travel_mate = db.Column(db.String(50)) - # travel_theme = db.Column(db.String(100)) - plan_response = db.Column(db.Text) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + travel_info = db.Column(db.Text, nullable=True) + plan_response = db.Column(db.Text, nullable=True) + location_info = db.Column(db.Text, nullable=True) + # hash_tag = db.Column(db.Text, nullable=True) + + # user = db.relationship("", backref=db.backref('travel_plans', lazy=True)) + +class SavedPlan(db.Model): + __tablename__ = 'saved_plans' + travel_id = db.Column(db.Integer, primary_key=True, autoincrement=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + travel_name = db.Column(db.Text, nullable=True) + travel_info = db.Column(db.Text, nullable=True) + plan_response = db.Column(db.Text, nullable=True) + location_info = db.Column(db.Text, nullable=True) + # hash_tag = db.Column(db.Text, nullable=True) + + # user = db.relationship('User', backref=db.backref('saved_plans', lazy=True)) + # travel_plan = db.relationship('TravelPlan', backref=db.backref('saved_in', lazy=True)) \ No newline at end of file diff --git a/agentPersona/requirements.txt b/agentPersona/requirements.txt index ddd1a1dd..c2b167e8 100644 Binary files a/agentPersona/requirements.txt and b/agentPersona/requirements.txt differ diff --git a/agentPersona/routes.py b/agentPersona/routes.py index 383f2252..8ff7ebf7 100644 --- a/agentPersona/routes.py +++ b/agentPersona/routes.py @@ -2,10 +2,13 @@ from langchain_core.runnables import RunnablePassthrough from pyexpat.errors import messages -from models import TravelPlan -from tamtam.openAi import call_openai_gpt, plan_persona, plan_model +from tamtam.template2 import location_template +from models import TravelPlan, SavedPlan +from tamtam.openAi import call_openai_gpt, plan_model, get_place_details # from tamtam.template import final_template -from tamtam.template2 import agent_prompt, plan_prompt, modify_prompt, final_template +from tamtam.template2 import (agent_prompt, plan_prompt, + modify_prompt, final_template, + location_prompt) from langchain.chains import LLMChain from langchain_core.output_parsers import StrOutputParser from langchain.llms import OpenAI @@ -63,12 +66,13 @@ def greeting(): def plan(): '''사용자 입력을 받아 여행 계획을 생성''' data = request.json + # user_id = data.get("user_id") # 사용자 ID + user_id = 1 # 사용자 ID travel_date = data.get("travel_date") travel_days = data.get("travel_days") travel_mate = data.get("travel_mate") travel_theme = data.get("travel_theme") - # Pinecone에서 테마 관련 정보 검색 search_results = search_theme_in_pinecone(travel_theme) theme_context = "\n".join([ @@ -76,17 +80,7 @@ def plan(): for result in search_results ]) - '''ver2''' output_parser = StrOutputParser() - - # plan_chain = ( - # {"theme_context": retriever, - # "travel_date": RunnablePassthrough(), - # "travel_days": RunnablePassthrough(), - # "travel_mate": RunnablePassthrough(), - # "travel_theme": RunnablePassthrough()}| - # plan_prompt | plan_model | output_parser) - plan_chain = plan_prompt | plan_model | output_parser input_data = { @@ -96,14 +90,115 @@ def plan(): "travel_theme": travel_theme, "theme_context": theme_context } - + # 여행 계획 생성 plan_response = plan_chain.invoke(input_data) - db.session.add(TravelPlan(plan_response=plan_response)) + # 사용자 입력 정보 json + travel_info = { + "travel_date": travel_date, + "travel_days": travel_days, + "travel_mate": travel_mate, + "travel_theme": travel_theme + } + + # 장소 정보 추출 + travel_plan = plan_response + + if not travel_plan: + return Response( + json.dumps({"error": "travel_plan is required"}, ensure_ascii=False), + content_type="application/json; charset=utf-8", + status=400 + ) + + output_parser = StrOutputParser() + location_chain = location_prompt | plan_model | output_parser + + input_data = {"travel_plan": travel_plan} + location_response = location_chain.invoke(input_data) + location_response = location_response.strip().strip("```json") + location_response = json.loads(location_response) + + print(travel_info) + print(plan_response) + print(location_response) + + # 여행 계획 테이블에 세션 컨셉으로 저장 + if TravelPlan.query.get(user_id): + existing_plan = TravelPlan.query.get(user_id) + existing_plan.travel_info = json.dumps(travel_info) + existing_plan.plan_response = plan_response + existing_plan.location_info = location_response + else: + db.session.add(TravelPlan(user_id=user_id, + travel_info=json.dumps(travel_info), + plan_response=plan_response, + location_info=json.dumps(location_response)) + ) + db.session.commit() follow_up_message = "여행 계획이 생성되었습니다. 수정하고 싶은 부분이 있으면 말씀해주세요! 😊" - plan_response_data = {"response": plan_response, "follow_up": follow_up_message} + # """location 추가""" + # travel_plan = plan_response + # + # if not travel_plan: + # return Response( + # json.dumps({"error": "travel_plan is required"}, ensure_ascii=False), + # content_type="application/json; charset=utf-8", + # status=400 + # ) + # + # try: + # # LangChain 사용 + # output_parser = StrOutputParser() + + # location_chain = location_prompt | plan_model | output_parser + # + # input_data = {"travel_plan": travel_plan} + # gpt_response = location_chain.invoke(input_data) + # + # # GPT 응답에서 JSON만 추출 + # try: + # # GPT 응답 파싱 + # start_index = gpt_response.find("{") + # end_index = gpt_response.rfind("}") + 1 + # json_data = gpt_response[start_index:end_index] + # extracted_places = json.loads(json_data)["places"] + # except (ValueError, KeyError, TypeError) as e: + # return Response( + # json.dumps({"error": f"Failed to parse GPT response: {str(e)}"}, ensure_ascii=False), + # content_type="application/json; charset=utf-8", + # status=500 + # ) + # + # # Google Maps API 호출로 상세 정보 보완 + # detailed_places = {} + # for day, places in extracted_places.items(): + # detailed_places[day] = [ + # get_place_details(place["name"]) for place in places + # ] + # + # plan_response_data = {"response": plan_response, + # "follow_up": follow_up_message, + # "travel_info": data, + # "places": detailed_places} + # return Response( + # json.dumps(plan_response_data, ensure_ascii=False), + # content_type="application/json; charset=utf-8" + # ) + # except Exception as e: + # return Response( + # json.dumps({"error": str(e)}, ensure_ascii=False), + # content_type="application/json; charset=utf-8", + # status=500 + # ) + + plan_response_data = {"response": plan_response, + "follow_up": follow_up_message, + "user_id": user_id, + "travel_info": travel_info, + "location_info": location_response} return Response( json.dumps(plan_response_data, ensure_ascii=False), content_type="application/json; charset=utf-8" @@ -113,8 +208,8 @@ def plan(): def modify3(): """사용자 입력을 받아 여행 계획을 수정""" data = request.json - # plan_id = data.get("plan_id") # 수정할 여행 계획 ID - plan_id = 53 + # user_id = data.get("user_id") # 사용자 ID + user_id = 1 # 사용자 ID modification_request = data.get("modify_request") # 수정 요청과 ID 확인 @@ -124,32 +219,22 @@ def modify3(): # if not plan_id: # return jsonify({"error": "Missing 'plan_id' in the request data"}), 403 - # 사용자 의도 판단 프롬프트 + # 사용자 의도 판단 프롬프트 intent_prompt = f""" 사용자가 다음과 같은 요청을 보냈습니다: "{modification_request}". 이 요청이 여행 계획 수정을 끝내겠다는 의도인지 판단해 주세요. 응답은 "수정 종료", "수정 계속" 중 하나로만 작성해주세요. + + '나 그냥 2월 7일에 서울로 돌아오고 싶어' 와 같은 입력은 수정 종료가 아니라, + '수정 계속'으로 판단해야 합니다. """ # 사용자 의도 판단 intent = call_openai_gpt([ {"role": "system", "content": "You analyze user modification intent."}, {"role": "user", "content": intent_prompt} ]) - # print(intent) - # # 수정 종료 하기 - # if intent == "수정 종료": - # end_message = "여행 계획에 만족하셨다니 다행입니다! 계획을 확정합니다. 😊" - # inform = call_openai_gpt([ - # {"role": "system", "content": final_template.format()}, - # ]) - # final_response_data = { - # "end_message": end_message, - # "inform": inform - # } - # return Response( - # json.dumps(final_response_data, ensure_ascii=False), - # content_type="application/json; charset=utf-8" - # ) + + # 디버깅: intent의 값과 타입 출력 print(f"Intent Value: '{intent}' (type: {type(intent)})") intent_cleaned = intent.strip().strip('"') @@ -173,7 +258,7 @@ def modify3(): print(f"Condition not met. Cleaned Intent Value: '{intent_cleaned}'") # 데이터베이스에서 여행 계획 가져오기 - travel_plan = TravelPlan.query.get(plan_id) # 특정 ID에 해당하는 행 가져오기 + travel_plan = TravelPlan.query.get(user_id) # 특정 ID에 해당하는 행 가져오기 if not travel_plan: return jsonify({"error": "No travel plan found with the provided ID"}), 404 @@ -188,21 +273,119 @@ def modify3(): modification_response = modify_chain.invoke(input_data) - # 수정된 여행 계획을 데이터베이스에 업데이트 - travel_plan.plan_response = modification_response + # 장소 정보 추출 + travel_plan = modification_response + + if not travel_plan: + return Response( + json.dumps({"error": "travel_plan is required"}, ensure_ascii=False), + content_type="application/json; charset=utf-8", + status=400 + ) + + output_parser = StrOutputParser() + location_chain = location_prompt | plan_model | output_parser + + input_data = {"travel_plan": travel_plan} + location_response = location_chain.invoke(input_data) + location_response = location_response.strip().strip("```json") + location_response = json.loads(location_response) + + print(modification_response) + print(location_response) + + existing_plan = TravelPlan.query.get(user_id) + + existing_plan.plan_response = modification_response + existing_plan.location_info = location_response + db.session.commit() + travel_info = TravelPlan.query.get(user_id).travel_info follow_up_message = "수정이 완료되었습니다. 추가 수정이 필요하면 말씀해주세요! 😊" # JSON 응답 생성 modify_response_data = { "response": modification_response, - "follow_up": follow_up_message + "follow_up": follow_up_message, + "user_id": user_id, + "travel_info": travel_info, + "location_info": location_response } + return Response( json.dumps(modify_response_data, ensure_ascii=False), content_type="application/json; charset=utf-8" ) + +@main_bp.route("/saveplan", methods=["POST"]) +def save_plan(): + '''여행 계획을 저장''' + data = request.json + user_id = data.get("user_id") + travel_name = data.get("travel_name") + + # 데이터베이스에서 여행 계획 가져오기 + travel_plan = TravelPlan.query.get(user_id) + + travel_info = travel_plan.travel_info + plan_response = travel_plan.plan_response + location_info = travel_plan.location_info + + db.session.add(SavedPlan( + user_id=user_id, + travel_name=travel_name, + travel_info=travel_info, + plan_response=plan_response, + location_info=location_info) + ) + + db.session.commit() + + message = "여행 계획 저장 성공!" + + return jsonify({"message": message}) + +@main_bp.route("/loadplan_mypage", methods=["POST"]) +def load_plan_mypage(): + '''저장된 여행 계획을 불러오기''' + data = request.json + user_id = data.get("user_id") + + saved_plans = SavedPlan.query.filter_by(user_id=user_id).all() + + if not saved_plans: + return jsonify({"message": "저장된 여행 계획이 없습니다.", "plans": []}), 200 + + plans = [] + for plan in saved_plans: + print(type(plan.location_info)) + location_info = json.loads(plan.location_info) + + plan = { + "travel_name": plan.travel_name, + "hashtag": location_info.get("hash_tag") + } + plans.append(plan) + + return jsonify({"message": "저장된 여행 계획을 불러왔습니다.", "plans": plans}) + +@main_bp.route("/loadplan", methods=["POST"]) +def load_plan(): + '''저장된 여행 계획을 불러오기''' + data = request.json + user_id = data.get("user_id") + travel_name = data.get("travel_name") + + saved_plan = SavedPlan.query.filter_by(user_id=user_id, travel_name=travel_name).first() + + if not saved_plan: + return jsonify({"message": "저장된 여행 계획이 없습니다."}), 404 + + location_info = json.loads(saved_plan.location_info) + + return jsonify({"message": "저장된 여행 계획을 불러왔습니다.", "plan": location_info}) + # # # @main_bp.route("/final", methods=["POST"]) # # def final(): @@ -255,4 +438,60 @@ def modify3(): # # def register_routes(app): # '''라우트를 Flask 앱에 등록''' -# app.register_blueprint(main_bp) \ No newline at end of file +# app.register_blueprint(main_bp) + + +@main_bp.route("/location", methods=["POST"]) +def location(): + '''여행 계획에서 장소 정보 추출''' + data = request.json + travel_plan = data.get("travel_plan") + + if not travel_plan: + return Response( + json.dumps({"error": "travel_plan is required"}, ensure_ascii=False), + content_type="application/json; charset=utf-8", + status=400 + ) + + try: + # LangChain 사용 + output_parser = StrOutputParser() + location_chain = location_prompt | plan_model | output_parser + + input_data = {"travel_plan": travel_plan} + gpt_response = location_chain.invoke(input_data) + + # GPT 응답에서 JSON만 추출 + try: + # GPT 응답 파싱 + start_index = gpt_response.find("{") + end_index = gpt_response.rfind("}") + 1 + json_data = gpt_response[start_index:end_index] + extracted_places = json.loads(json_data)["places"] + except (ValueError, KeyError, TypeError) as e: + return Response( + json.dumps({"error": f"Failed to parse GPT response: {str(e)}"}, ensure_ascii=False), + content_type="application/json; charset=utf-8", + status=500 + ) + + # Google Maps API 호출로 상세 정보 보완 + detailed_places = {} + for day, places in extracted_places.items(): + detailed_places[day] = [ + get_place_details(place["name"]) for place in places + ] + + # 최종 JSON 반환 + location_response_data = {"places": detailed_places} + return Response( + json.dumps(location_response_data, ensure_ascii=False), + content_type="application/json; charset=utf-8" + ) + except Exception as e: + return Response( + json.dumps({"error": str(e)}, ensure_ascii=False), + content_type="application/json; charset=utf-8", + status=500 + ) \ No newline at end of file diff --git a/agentPersona/tamtam/__pycache__/openAi.cpython-311.pyc b/agentPersona/tamtam/__pycache__/openAi.cpython-311.pyc index 5331e667..cec6bc01 100644 Binary files a/agentPersona/tamtam/__pycache__/openAi.cpython-311.pyc and b/agentPersona/tamtam/__pycache__/openAi.cpython-311.pyc differ diff --git a/agentPersona/tamtam/__pycache__/template2.cpython-311.pyc b/agentPersona/tamtam/__pycache__/template2.cpython-311.pyc index 6fde1fee..472bf2aa 100644 Binary files a/agentPersona/tamtam/__pycache__/template2.cpython-311.pyc and b/agentPersona/tamtam/__pycache__/template2.cpython-311.pyc differ diff --git a/agentPersona/tamtam/openAi.py b/agentPersona/tamtam/openAi.py index caa7e6ad..1c0fcf8c 100644 --- a/agentPersona/tamtam/openAi.py +++ b/agentPersona/tamtam/openAi.py @@ -1,11 +1,16 @@ # api 중간에 gpt 이중 구조로 호출 하기 위함 +import requests from langchain_openai import ChatOpenAI from openai import OpenAI import os from dotenv import load_dotenv +from requests.models import Response +from pyexpat.errors import messages + load_dotenv() openai_api_key = os.getenv("OPENAI_API_KEY") +GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY") client = OpenAI(api_key=openai_api_key) @@ -16,18 +21,17 @@ def call_openai_gpt(messages): ) return response.choices[0].message.content.strip() -def plan_persona(messages): - """OpenAI GPT 최신 API 호출""" - response = client.chat.completions.create( - model="gpt-4o-mini", - messages=messages, - temperature=1.1, - max_tokens=10000, - top_p=1, - frequency_penalty=0, - presence_penalty=0, - ) - return response.choices[0].message.content.strip() +# def location_json_gpt(travel_plan): +# response = client.chat.completions.create( +# model="gpt-4o-mini", +# messages=[ +# {"role": "system", "content": location_prompt] +# ], +# functions = { +# "travel_plan": travel_plan +# } +# ) + plan_model = ChatOpenAI( @@ -37,4 +41,26 @@ def plan_persona(messages): top_p=1, frequency_penalty=0, presence_penalty=0 -) \ No newline at end of file +) + +# Google Maps API 호출 함수 +def get_place_details(place_name): + url = f"https://maps.googleapis.com/maps/api/place/findplacefromtext/json" + params = { + "input": place_name, + "inputtype": "textquery", + "fields": "formatted_address,geometry", + "key": GOOGLE_MAPS_API_KEY, + "locationbias": "circle:50000@33.4996,126.5312" # 제주도 중심 좌표와 반경 50km + } + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + if data.get('candidates'): + candidate = data['candidates'][0] + return { + "name": place_name, + "location": candidate.get("formatted_address"), + "coordinate": candidate["geometry"]["location"] + } + return {"name": place_name, "location": "정보 없음", "coordinate": "정보 없음"} \ No newline at end of file diff --git a/agentPersona/tamtam/template2.py b/agentPersona/tamtam/template2.py index 715bc204..1073b8c3 100644 --- a/agentPersona/tamtam/template2.py +++ b/agentPersona/tamtam/template2.py @@ -22,8 +22,6 @@ # 정보 - 너는 대답을 반환하고, 추후 사용자로부터 정보를 입력 받을 것이기 때문에 인삿말만 하면 되고, 아니면 제가 추천해드릴까요? 이런말 하지마 - - ''' agent_prompt = PromptTemplate( @@ -47,16 +45,37 @@ # 관련 장소 정보 {theme_context} - # 처리 - 사용자가 너에게 위의 정보들을 주면, 이를 바탕으로 개인 맞춤형 - 제주도 여행 계획을 아래의 형식을 참고 하여 짜주고, 사용자에게 추천한 - 계획에서 수정하고 싶은 부분이 있으면 말해달라고 해. - # 여행 계획 형식(일자별) 날짜 : (날짜, 요일) 오전 : (여행계획) 오후 : (여행계획) 저녁 : (여행계획) + + # 추천하는 여행 계획에 고려하고 반영할 사항 + 1. 시간 관련 + - 관광지와 식당에 머무는 시간을 고려하기 + - 장소 간 이동 시간을 고려하기 + 2. 이동 관련 + - 이동하는 교통 경로가 복잡하지 않아야함 + 3. 추천 장소 관련 + - 추천하는 장소는 구체적이여야함 + 옳지 않은 예시) + **저녁**: 인근 카페에서 저녁 식사 후 제주에서 유명한 돌솥비빔밥으로 마무리! + 옳은 예시) + **저녁**: 인근 카페에서 저녁 식사 후 제주에서 "실제 돌솥비빔밥이 유명한 식당 이름"에서 + 돌솥비빔밥으로 마무리! + - 반드시 제주특별자치도 내에 위치한 장소만 추천하기. + - 체인점이더라도 제주도 내에만 있는 장소로 한정하기. + - 제주도의 위도 범위는 33.1~33.6, 경도 범위는 126.1~126.9에 해당하는 지역만 추천해야 함. + - 제주도 외의 장소(예: 서울, 부산, 프랑스 등)는 절대 포함되지 않도록 주의하기. + + # 행동 지침 + 너는 "제주도 여행 계획 추천 에이전트"로 행동해. + 너의 구체적인 페르소나는 위의 페르소나야. + 사용자 입력 정보와 사용자가 입력한 테마에 해당하는 관련 장소 정보를 + 바탕으로 개인 맞춤형 제주도 여행 계획을 위의 여행 계획 형식으로 짜주고, + 사용자에게 추천한 계획에서 수정하고 싶은 부분이 있으면 말해달라고 해. + 추천하는 여행 계획에 고려하고 반영할 사항을 고려해서 추천해. ''' plan_prompt = PromptTemplate( template = plan_template, @@ -80,15 +99,38 @@ 기존의 여행 계획: {current_plan} 사용자가 요청한 변경 사항: {modification_request} - # 처리 - 사용자가 요청한 변경 사항을 기존의 여행 계획에 반영하여 여행 계획을 수정하되, - 아래의 여행 계획 형식은 유지하면서 사용자에게 수정된 계획을 제시해. + # 수정하는 여행 계획에 고려하고 반영할 사항 + 1. 시간 관련 + - 관광지와 식당에 머무는 시간을 고려하기 + - 장소 간 이동 시간을 고려하기 + 2. 이동 관련 + - 이동하는 교통 경로가 복잡하지 않아야함 + 3. 추천 장소 관련 + - 추천하는 장소는 구체적이여야함 + 옳지 않은 예시) + **저녁**: 인근 카페에서 저녁 식사 후 제주에서 유명한 돌솥비빔밥으로 마무리! + 옳은 예시) + **저녁**: 인근 카페에서 저녁 식사 후 제주에서 "실제 돌솥비빔밥이 유명한 식당 이름"에서 + 돌솥비빔밥으로 마무리! + - 반드시 제주특별자치도 내에 위치한 장소만 추천하기. + - 체인점이더라도 제주도 내에만 있는 장소로 한정하기. + - 제주도의 위도 범위는 33.1~33.6, 경도 범위는 126.1~126.9에 해당하는 지역만 추천해야 함. + - 제주도 외의 장소(예: 서울, 부산, 프랑스 등)는 절대 포함되지 않도록 주의하기. # 여행 계획 형식(일자별) 날짜 : (날짜, 요일) 오전 : (여행계획) 오후 : (여행계획) 저녁 : (여행계획) + + # 행동 지침 + 너는 "제주도 여행 계획 추천 에이전트"로 행동해. + 너의 구체적인 페르소나는 위의 페르소나야. + 사용자가 요청한 변경 사항을 기존의 여행 계획에 반영하여 여행 계획을 수정하는데, + 위의 여행 계획 형식은 유지해. + + 수정된 계획만 보여주는게 아니라, 기존의 여행계획에서 변경된 부분을 합쳐서 + 다시 보여주는 거야. ''' modify_prompt = PromptTemplate( @@ -105,11 +147,102 @@ [기타]: 말이 끝날 때 귤 이모티콘 붙이기 # 처리 - 사용자에게 최종 여행 계획을 보여주고, 즐거운 여행이 되길 바라는 말을 해. + 너는 여행 계획을 확정하고 마지막 인사를 하는 역할을 맡은 api야. + 그래서 인삿말은 필요없어. 마무리 말만 하면 돼. + 사용자에게 즐거운 여행이 되길 바라는 말을 해. 왼쪽의 상세 일정 보기 버튼을 누르면 상세한 일정을 확인할 수 있다고 말을 해. # 정보 - 사용자는 여러명이 아니라, 개인 한명이 여행 계획을 짜는 것임 + # 답변 예시 + ''' +location_template = ''' + # 입력 정보 + 여행 일정 : {travel_plan} + + # 장소 분류 + - 관광지 + - 음식점 + + # 행동 지침 + 너는 입력을 받은 여행 일정에서 여행 일자별로 장소 정보를 추출하고, + 그 장소에 대한 도로명 주소와, 그 장소의 분류를 반환하고, + 그리고 여행을 가장 잘 설명할 수 있는 해시태그를 너가 만들어서 + 아래의 JSON 형식으로 반환해야 해. + + # JSON 형식 + {{ + "places": {{ + "day1": [ + {{ + "name": "장소 이름", + "location": "도로명 주소", + "coordinate": "좌표", + "category": "관광지/음식점" + }}, + ... + ], + "day2": [ + {{ + "name": "장소 이름", + "location": "도로명 주소", + "coordinate": "좌표", + "category": "관광지/음식점" + }}, + ... + ] + }} + "hash_tag": "#액티비티 #... 등등" + }} + + 위 형식에 맞춰서 장소 정보와 해시태그를 반환하면 되고, + JSON 형식의 자료만 반환하고 다른 말은 하지마. + ''' + +location_prompt = PromptTemplate( + template = location_template, + input_variables=["travel_plan"] +) + +# LangChain 설정 +# location_template = ''' +# # 입력 정보 +# 여행 일정 : {travel_plan} +# +# # 장소 분류 +# - 관광지 +# - 음식점 +# - 카페 +# +# # 행동 지침 +# 너는 입력을 받은 여행 일정에서 여행 일자별로 장소 정보를 추출하고, +# 장소 이름과 장소 분류를 판단해서, 아래 JSON 형식으로 반환해야 해: +# +# {{ +# "places": {{ +# "day1": [ +# {{ +# "name": "장소 이름", +# "category": "관광지/음식점/카페" +# }}, +# ... +# ], +# "day2": [ +# {{ +# "name": "장소 이름", +# "category": "관광지/음식점/카페" +# }}, +# ... +# ] +# }} +# }} +# +# JSON 형식만 반환하고 다른 설명은 하지마. +# ''' +# location_prompt = PromptTemplate( +# template=location_template, +# input_variables=["travel_plan"] +# ) \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6024ed81..06508b4e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19018,6 +19018,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "3.1.3", "prop-types": "15.8.1", diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 00000000..1f15604d --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,9 @@ + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 00000000..b34a1cd9 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,21 @@ +{ + "short_name": "탐라, 탐나", + "name": "탐라, 탐나: Jeju Travel Planner", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#000000", + "description": "A Jeju travel planning web application", + "icons": [ + { + "src": "favicon.ico", + "sizes": "16x16 32x32 64x64", + "type": "image/x-icon" + }, + { + "src": "logo.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] +} diff --git a/frontend/src/api/chatApi.js b/frontend/src/api/chatApi.js new file mode 100644 index 00000000..ddd96a90 --- /dev/null +++ b/frontend/src/api/chatApi.js @@ -0,0 +1,43 @@ +import axios from "axios"; + +// 환경 변수에서 Base URL 가져오기 +const API_BASE_URL = process.env.REACT_APP_API_BASE_URL; + +console.log("API_BASE_URL:", API_BASE_URL); + +// Greeting API +export const getGreetingMessage = async (frontInput) => { + try { + const url = `${API_BASE_URL}/greeting`; + console.log("Request URL:", url); // 디버깅용 + const response = await axios.post(url, { front_input: frontInput }); + return response.data.response; + } catch (error) { + console.error("Greeting API Error:", error.response?.data || error.message); + throw error; + } +}; + +// Plan API +export const getTravelPlan = async (requestData) => { + try { + const response = await axios.post(`${API_BASE_URL}/plan`, requestData); + return response.data; + } catch (error) { + console.error("Plan API Error:", error.response?.data || error.message); + throw error; + } +}; + +// Modify API +export const modifyTravelPlan = async (modifyRequest) => { + try { + const response = await axios.post(`${API_BASE_URL}/modify`, { + modify_request: modifyRequest, + }); + return response.data; + } catch (error) { + console.error("Modify API Error:", error.response?.data || error.message); + throw error; + } +}; diff --git a/frontend/src/components/TravelSummary.js b/frontend/src/components/TravelSummary.js index 2bf440a3..a5ef42e0 100644 --- a/frontend/src/components/TravelSummary.js +++ b/frontend/src/components/TravelSummary.js @@ -5,7 +5,6 @@ import styles from "./TravelSummary.module.css"; function TravelSummary({ userName = "예림", itineraryDays = 2 }) { const [itinerarySummary, setItinerarySummary] = useState({ totalDistance: "", - totalPlaces: 0, tags: [], }); const [sampleHighlights, setSampleHighlights] = useState([]); @@ -20,10 +19,8 @@ function TravelSummary({ userName = "예림", itineraryDays = 2 }) { // 총 요약 정보 계산 const totalPlaces = Object.values(data).flat().length; // 모든 장소의 수 const tags = ["#액티비티", "#테마파크", "#바다"]; // 샘플 태그 - const totalDistance = "70km"; // 이동거리 샘플 데이터 setItinerarySummary({ - totalDistance, totalPlaces, tags, }); @@ -52,9 +49,8 @@ function TravelSummary({ userName = "예림", itineraryDays = 2 }) { 여행코스
- 총 이동거리 | {itinerarySummary.totalDistance} + 총 {itinerarySummary.totalPlaces}개 여행지/음식점/카페/숙소
-총 {itinerarySummary.totalPlaces}개 여행지/음식점/카페/숙소
{itinerarySummary.tags.join(" ")}
diff --git a/frontend/src/pages/DetailedItineraryPage.js b/frontend/src/pages/DetailedItineraryPage.js index 968a73eb..e1965a9b 100644 --- a/frontend/src/pages/DetailedItineraryPage.js +++ b/frontend/src/pages/DetailedItineraryPage.js @@ -8,10 +8,16 @@ import Route from "../components/Route"; import Checklist from "../components/Checklist"; import Modal from "../components/Modal"; import styles from "./DetailedItineraryPage.module.css"; +import axios from "axios"; function DetailedItineraryPage() { const [activeTab, setActiveTab] = useState("여행 요약"); const [isModalOpen, setIsModalOpen] = useState(false); + const [dateRange, setDateRange] = useState([ + new Date(), + new Date(new Date().setDate(new Date().getDate() + 3)), + ]); + const [selectedThemes, setSelectedThemes] = useState(["자연", "휴양"]); const navigate = useNavigate(); const tabs = [ @@ -32,9 +38,31 @@ function DetailedItineraryPage() { }; // 일정 확정 (확인 버튼용) - const handleConfirm = () => { + const handleConfirm = async () => { setIsModalOpen(false); // 모달 닫기 - navigate("/mypage"); // 마이페이지로 이동 + + // 카드 형식으로 저장할 일정 데이터 + const itineraryData = { + title: `여행 일정 - ${dateRange[0].toLocaleDateString()} ~ ${dateRange[1].toLocaleDateString()}`, + imageUrl: "https://example.com/your-image.jpg", // 이미지 URL + date: `${dateRange[0].toLocaleDateString()} ~ ${dateRange[1].toLocaleDateString()}`, + tags: selectedThemes.join(", "), // 선택한 테마 + userId: "current_user_id", // 현재 로그인한 사용자의 ID + }; + + try { + // 서버에 일정 데이터 전송 + const response = await axios.post("URL", itineraryData); + + // 서버에서 저장된 일정 다시 받아옴 + const savedItinerary = response.data; + + // 마이페이지로 데이터 전달 + navigate("/mypage", { state: { newItinerary: response.data } }); + } catch (error) { + console.error("일정 저장 오류:", error); + alert("일정을 저장하는데 오류가 발생했습니다."); + } }; return ( diff --git a/frontend/src/pages/KakaoLoginPage.js b/frontend/src/pages/KakaoLoginPage.js index 0c90babf..6a53c7bb 100644 --- a/frontend/src/pages/KakaoLoginPage.js +++ b/frontend/src/pages/KakaoLoginPage.js @@ -5,7 +5,8 @@ import kakaoIcon from "../assets/images/kakao_logo.svg"; import Footer from "../components/Footer"; function KakaoLoginPage() { - const url = "http://localhost:8080/oauth2/authorization/kakao"; + const url = + "https://6596-210-94-220-228.ngrok-free.app/oauth2/authorization/kakao"; // 배포 링크 const onClick = () => { window.location.href = url; diff --git a/frontend/src/pages/NewChat.js b/frontend/src/pages/NewChat.js index 0f38fad2..c9590c19 100644 --- a/frontend/src/pages/NewChat.js +++ b/frontend/src/pages/NewChat.js @@ -10,8 +10,10 @@ import iconSend from "../assets/icon_send.png"; import iconGptProfile from "../assets/icon_gptprofile.png"; import iconUserProfile from "../assets/icon_userprofile.png"; import iconClear from "../assets/icon_clear.png"; -import axios from "axios"; import { v4 as uuidv4 } from "uuid"; +import { getGreetingMessage } from "../api/chatApi"; +import { getTravelPlan } from "../api/chatApi"; +import { modifyTravelPlan } from "../api/chatApi"; function NewChat() { const itinerary = 4; @@ -28,7 +30,6 @@ function NewChat() { const [isGreetingAccepted, setIsGreetingAccepted] = useState(false); // 첫 트리거 const [greetingMessage, setGreetingMessage] = useState(""); // 서버에서 받은 인삿말 const [isWaitingForModify, setIsWaitingForModify] = useState(false); // Modify 대기 - const ngrokUrl = "http://your-ngrok-url.ngrok.io"; // 백엔드 서버 (ngrok URL) const mockUserData = { profileImage: iconUserProfile, @@ -63,12 +64,10 @@ function NewChat() { if (greetingMessage) return; // 이미 메시지가 존재하면 함수 종료 try { - const response = await axios.post(`${ngrokUrl}/greeting`, { - front_input: "탐탐이와 여행 일정 시작", - }); - const generateResponse = response.data.response; + const frontInput = "탐탐이와 여행 일정 시작"; + const generateResponse = await getGreetingMessage(frontInput); - // 상태 업데이트 및 메시지 추가 + // 상태 업데이트 setGreetingMessage(generateResponse); setIsGreetingAccepted(true); } catch (error) { @@ -78,7 +77,7 @@ function NewChat() { }; // plan API 연결 - const handleConfirm = () => { + const handleConfirm = async () => { const requestData = { travel_date: `${dateRange[0].toLocaleDateString()} ~ ${dateRange[1].toLocaleDateString()}`, travel_days: Math.ceil( @@ -90,57 +89,44 @@ function NewChat() { setIsGenerating(true); // 로딩 시작 - axios - .post(`${ngrokUrl}/plan`, requestData) - .then((response) => { - const planResponse = response.data.response; - const followUp = response.data.follow_up; - - // Plan 응답 버블 - addMessage(planResponse, false); - addMessage(followUp, false); - - // Modify 입력 대기 상태 - setIsWaitingForModify(true); - }) - .catch((error) => { - console.error("Plan 요청 오류:", error); - addMessage( - "Error: 일정 생성에 실패했습니다. 다시 시도해주세요.", - false - ); - }) - .finally(() => { - setIsGenerating(false); // 로딩 상태 종료 - }); + try { + const { response: planResponse, follow_up: followUp } = + await getTravelPlan(requestData); + + // Plan 응답 버블 + addMessage(planResponse, false); + addMessage(followUp, false); + + // Modify 입력 대기 상태 + setIsWaitingForModify(true); + } catch (error) { + console.error("Plan 요청 오류:", error); + addMessage("Error: 일정 생성에 실패했습니다. 다시 시도해주세요.", false); + } finally { + setIsGenerating(false); // 로딩 상태 종료 + } }; // modify API 연결 - const handleModifyRequest = (modifyRequest) => { + const handleModifyRequest = async (modifyRequest) => { setIsGenerating(true); // 로딩 시작 - axios - .post(`${ngrokUrl}/modify`, { modify_request: modifyRequest }) - .then((response) => { - const modifyResponse = response.data.response; - const followUp = response.data.follow_up; - - // Modify 응답 버블 추가 - addMessage(modifyResponse, false); - addMessage(followUp, false); - - // Modify 대기 상태 - setIsWaitingForModify(true); - }) - .catch((error) => { - console.error("Modify 요청 오류:", error); - addMessage( - "Error: 일정 수정에 실패했습니다. 다시 시도해주세요.", - false - ); - }) - .finally(() => { - setIsGenerating(false); // 로딩 종료 - }); + + try { + const { response: modifyResponse, follow_up: followUp } = + await modifyTravelPlan(modifyRequest); // Modify API 호출 + + // Modify 응답 버블 + addMessage(modifyResponse, false); + addMessage(followUp, false); + + // Modify 대기 상태 + setIsWaitingForModify(true); + } catch (error) { + console.error("Modify 요청 오류:", error); + addMessage("Error: 일정 수정에 실패했습니다. 다시 시도해주세요.", false); + } finally { + setIsGenerating(false); // 로딩 상태 종료 + } }; const addMessage = (text, isUser) => { @@ -385,4 +371,4 @@ function NewChat() { ); } -export default NewChat; \ No newline at end of file +export default NewChat; diff --git a/frontend/src/pages/RedirectPage.js b/frontend/src/pages/RedirectPage.js index b9a4ae5f..6a83c35e 100644 --- a/frontend/src/pages/RedirectPage.js +++ b/frontend/src/pages/RedirectPage.js @@ -5,41 +5,27 @@ import iconUserProfile from "../assets/icon_userprofile.png"; function RedirectPage() { const navigate = useNavigate(); + const baseUrl = "https://6596-210-94-220-228.ngrok-free.app/oauth/login"; // 배포 링크 - // useEffect(() => { - // // Spring Boot API에서 회원 정보 가져오기 - // axios - // .get("주소 입려부분") - // .then((response) => { - // const userInfo = response.data; - - // // 회원 정보 저장 - // localStorage.setItem("userInfo", JSON.stringify(userInfo)); + useEffect(() => { + // URL에 success를 추가하여 호출 + const urlWithSuccess = `${baseUrl}/success`; - // // 메인 페이지로 이동 - // navigate("/"); - // }) - // .catch((error) => { - // console.error("회원 정보 가져오기 실패:", error); - // }); - // }, [navigate]); - const mockUserInfo = { - nickname: "Hyunjong", - profileImage: iconUserProfile, - }; + // Spring Boot API에서 회원 정보 가져오기 + axios + .get(urlWithSuccess) + .then((response) => { + const userInfo = response.data; - // 회원 정보를 localStorage에 저장하고 메인 페이지로 이동 - useEffect(() => { - // 회원 정보 저장 - try { - localStorage.setItem("userInfo", JSON.stringify(mockUserInfo)); - console.log("회원 정보 저장 완료"); + // 회원 정보 저장 + localStorage.setItem("userInfo", JSON.stringify(userInfo)); - // 저장 성공 후 메인 페이지로 이동 - navigate("/MyPage"); - } catch (error) { - console.error("회원 정보 저장 중 오류 발생:", error); - } + // 메인 페이지로 이동 + navigate("/"); + }) + .catch((error) => { + console.error("회원 정보 가져오기 실패:", error); + }); }, [navigate]); return ( @@ -49,4 +35,5 @@ function RedirectPage() { ); } + export default RedirectPage; diff --git a/server/src/main/java/com/capstone/server/config/SecurityConfig.java b/server/src/main/java/com/capstone/server/config/SecurityConfig.java index 768168c7..6aa09633 100644 --- a/server/src/main/java/com/capstone/server/config/SecurityConfig.java +++ b/server/src/main/java/com/capstone/server/config/SecurityConfig.java @@ -28,7 +28,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정 추가 .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "/login", "/oauth2/**", "/error", "/h2-console/**").permitAll() + .requestMatchers("/", "/login", "/oauth2/**", "/error").permitAll() .anyRequest().authenticated() ) .headers(headers -> headers @@ -47,7 +47,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); // 인증 정보 포함 허용 config.setAllowedOrigins(List.of("http://172.20.10.3:3000")); // 프론트엔드 URL config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 허용 메서드 diff --git a/server/src/main/java/com/capstone/server/controller/UserController.java b/server/src/main/java/com/capstone/server/controller/UserController.java index fa3613dc..61d5dfd1 100644 --- a/server/src/main/java/com/capstone/server/controller/UserController.java +++ b/server/src/main/java/com/capstone/server/controller/UserController.java @@ -48,6 +48,7 @@ public ResponseEntity> loginSuccess(Authentication authentication) { // 반환 데이터 생성 Map