diff --git a/docs/REDIS_GUIDE.md b/docs/REDIS_GUIDE.md index a880b0d..99c341a 100644 --- a/docs/REDIS_GUIDE.md +++ b/docs/REDIS_GUIDE.md @@ -4,7 +4,10 @@ ### 1. Redis 서버 실행 (Docker 추천) ```bash -# Redis 서버 시작 +# Redis 서버 시작 (비밀번호 설정 + 데이터 영속화) - 한 줄 복붙 +docker run -d --name redis --restart unless-stopped -p 6379:6379 -e TZ=Asia/Seoul -v redis_data:/data redis:alpine --requirepass 'your_password_here' + +# 간단 버전 (비밀번호 없음) docker run -d -p 6379:6379 --name redis redis:alpine # Redis 중지 @@ -12,6 +15,12 @@ docker stop redis # Redis 재시작 docker start redis + +# Redis 로그 확인 +docker logs redis + +# Redis 완전 삭제 (데이터 포함) +docker rm -f redis && docker volume rm redis_data ``` ### 2. 환경변수 설정 @@ -19,7 +28,8 @@ docker start redis # .env 파일 생성 (.env.example 복사) REDIS_HOST=localhost REDIS_PORT=6379 -REDIS_PASSWORD= +REDIS_PASSWORD=your_password_here # 비밀번호 설정한 경우 +# REDIS_PASSWORD= # 비밀번호 없으면 빈 값 ``` ## 💾 캐시 사용법 @@ -129,18 +139,29 @@ GET http://localhost:8080/actuator/health ### Redis CLI 접속 ```bash -# Docker 컨테이너 접속 +# Docker 컨테이너 접속 (비밀번호 없는 경우) +docker exec -it redis redis-cli + +# Docker 컨테이너 접속 (비밀번호 있는 경우) +docker exec -it redis redis-cli -a 'your_password_here' + +# 또는 접속 후 인증 docker exec -it redis redis-cli +> AUTH your_password_here # 캐시 확인 > KEYS * > GET "weather::서울" -> TTL "weather::서울" # 남은 시간 확인 +> TTL "weather::서울" # 남은 시간 확인 (초 단위) +> DEL "weather::서울" # 특정 캐시 삭제 +> FLUSHALL # 모든 캐시 삭제 ``` ## 🚨 주의사항 1. **개발용**: Redis 없어도 실행됨 (`session.store-type: none`) -2. **캐시 키**: 특수문자 주의 (`::` 구분자 사용) -3. **TTL**: 적절한 캐시 시간 설정 (API 호출량 고려) -4. **메모리**: Redis 메모리 사용량 모니터링 필요 \ No newline at end of file +2. **비밀번호**: 운영 환경에서는 반드시 강력한 비밀번호 설정 권장 +3. **데이터 영속화**: `-v redis_data:/data` 옵션으로 컨테이너 재시작 시에도 데이터 보존 +4. **캐시 키**: 특수문자 주의 (`::` 구분자 사용) +5. **TTL**: 적절한 캐시 시간 설정 (API 호출량 고려) +6. **메모리**: Redis 메모리 사용량 모니터링 필요 \ No newline at end of file diff --git a/docs/api-specification.yaml b/docs/api-specification.yaml index c0ed351..e75ccdb 100644 --- a/docs/api-specification.yaml +++ b/docs/api-specification.yaml @@ -133,36 +133,20 @@ components: nullable: true # AI Chat Domain Schemas - AiChatSession: + SessionsResponse: type: object properties: - id: + sessionId: type: integer format: int64 description: 세션 고유 식별자 - userId: - type: integer - format: int64 - description: 사용자 ID sessionTitle: type: string description: 세션 제목 - createdAt: - type: string - format: date-time - description: 생성 일시 - AiChatMessage: + SessionMessagesResponse: type: object properties: - id: - type: integer - format: int64 - description: 메시지 고유 식별자 - sessionId: - type: integer - format: int64 - description: 세션 ID content: type: string description: 메시지 내용 @@ -170,13 +154,23 @@ components: type: string enum: [USER, AI] description: 발신자 유형 - metadata: - type: object - description: AI 도구 사용 정보 (WeatherTool, TourTool) - createdAt: + + AiChatResponse: + type: object + properties: + userMessage: type: string - format: date-time - description: 메시지 전송 시간 + description: 사용자 메시지 내용 + aiMessage: + type: string + description: AI 응답 메시지 내용 + + UpdateSessionTitleResponse: + type: object + properties: + newTitle: + type: string + description: 수정된 세션 제목 # User Chat Domain Schemas ChatRoomResponse: @@ -485,6 +479,14 @@ paths: - aichat summary: AI 채팅 세션 목록 조회 description: 사용자의 AI 채팅 세션 목록을 최신순으로 조회 + parameters: + - name: userId + in: query + required: true + schema: + type: integer + format: int64 + description: 사용자 ID responses: '200': description: 세션 목록 조회 성공 @@ -498,24 +500,23 @@ paths: data: type: array items: - $ref: '#/components/schemas/AiChatSession' + $ref: '#/components/schemas/SessionsResponse' post: tags: - aichat summary: 새 AI 채팅 세션 생성 description: 새로운 AI 채팅 세션을 생성합니다 - requestBody: - content: - application/json: - schema: - type: object - properties: - sessionTitle: - type: string - description: 세션 제목 (선택사항) + parameters: + - name: userId + in: query + required: true + schema: + type: integer + format: int64 + description: 사용자 ID responses: - '201': + '200': description: 세션 생성 성공 content: application/json: @@ -525,7 +526,7 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/AiChatSession' + $ref: '#/components/schemas/SessionsResponse' /api/aichat/sessions/{sessionId}: delete: @@ -541,6 +542,13 @@ paths: type: integer format: int64 description: AI 채팅 세션 ID + - name: userId + in: query + required: true + schema: + type: integer + format: int64 + description: 사용자 ID responses: '200': description: 세션 삭제 완료 @@ -565,6 +573,13 @@ paths: type: integer format: int64 description: AI 채팅 세션 ID + - name: userId + in: query + required: true + schema: + type: integer + format: int64 + description: 사용자 ID responses: '200': description: 채팅 기록 조회 성공 @@ -576,14 +591,9 @@ paths: - type: object properties: data: - type: object - properties: - session: - $ref: '#/components/schemas/AiChatSession' - messages: - type: array - items: - $ref: '#/components/schemas/AiChatMessage' + type: array + items: + $ref: '#/components/schemas/SessionMessagesResponse' post: tags: @@ -598,6 +608,13 @@ paths: type: integer format: int64 description: AI 채팅 세션 ID + - name: userId + in: query + required: true + schema: + type: integer + format: int64 + description: 사용자 ID requestBody: required: true content: @@ -605,9 +622,9 @@ paths: schema: type: object required: - - content + - message properties: - content: + message: type: string description: 사용자 메시지 내용 responses: @@ -621,12 +638,7 @@ paths: - type: object properties: data: - type: object - properties: - userMessage: - $ref: '#/components/schemas/AiChatMessage' - aiMessage: - $ref: '#/components/schemas/AiChatMessage' + $ref: '#/components/schemas/AiChatResponse' /api/aichat/sessions/{sessionId}/title: patch: @@ -642,6 +654,13 @@ paths: type: integer format: int64 description: AI 채팅 세션 ID + - name: userId + in: query + required: true + schema: + type: integer + format: int64 + description: 사용자 ID requestBody: required: true content: @@ -649,9 +668,9 @@ paths: schema: type: object required: - - title + - newTitle properties: - title: + newTitle: type: string maxLength: 100 description: 새로운 세션 제목 @@ -666,7 +685,7 @@ paths: - type: object properties: data: - $ref: '#/components/schemas/AiChatSession' + $ref: '#/components/schemas/UpdateSessionTitleResponse' '404': description: 세션을 찾을 수 없음 '403': diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt index 2e6c349..fa792f0 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt @@ -23,9 +23,23 @@ class TourTool( @Tool(description = "areaBasedList2 : 지역기반 관광정보 조회, 특정 지역의 관광 정보 조회") fun getAreaBasedTourInfo( - @ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true) + @ToolParam( + description = + "관광 타입 코드를 사용하세요. 사용자가 타입 이름을 말하면 해당하는 코드를 찾아서 사용해야 합니다. " + + "예: 사용자가 '관광지'라고 하면 '12'를 사용하세요. " + + "사용 가능한 타입 코드: ${BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION}", + required = true, + ) contentTypeId: String, - @ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true) + @ToolParam( + description = + "지역 코드를 쉼표(,)로 구분해서 사용하세요. " + + "예: 사용자가 '서울 강남구'라고 하면 AREA_CODES에서 '서울-강남구: 1-1'을 찾고, " + + "하이픈(-)을 쉼표(,)로 바꿔서 '1,1'을 사용하세요. " + + "광역시(인천, 대전 등)는 단일 코드만 사용: 예: '인천' → '2' (쉼표 없음). " + + "사용 가능한 지역 코드: ${BuildConfig.AREA_CODES_DESCRIPTION}", + required = true, + ) areaAndSigunguCode: String, ): String { log.info("🔧 [TOOL CALLED] getAreaBasedTourInfo - contentTypeId: $contentTypeId, areaAndSigunguCode: $areaAndSigunguCode") @@ -50,15 +64,28 @@ class TourTool( @Tool(description = "locationBasedList2 : 위치기반 관광정보 조회, 특정 위치 기반의 관광 정보 조회") fun getLocationBasedTourInfo( - @ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true) + @ToolParam( + description = + "관광 타입 코드를 사용하세요. 사용자가 타입 이름을 말하면 해당하는 코드를 찾아서 사용해야 합니다. " + + "예: 사용자가 '음식점'이라고 하면 '39'를 사용하세요. " + + "사용 가능한 타입 코드: ${BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION}", + required = true, + ) contentTypeId: String, - @ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true) + @ToolParam( + description = + "지역 코드를 쉼표(,)로 구분해서 사용하세요. " + + "예: 사용자가 '서울 중구'라고 하면 AREA_CODES에서 '서울-중구: 1-24'를 찾고, " + + "하이픈(-)을 쉼표(,)로 바꿔서 '1,24'를 사용하세요. " + + "사용 가능한 지역 코드: ${BuildConfig.AREA_CODES_DESCRIPTION}", + required = true, + ) areaAndSigunguCode: String, - @ToolParam(description = "WGS84 경도", required = true) + @ToolParam(description = "WGS84 경도 좌표", required = true) mapX: String = "126.98375", - @ToolParam(description = "WGS84 위도", required = true) + @ToolParam(description = "WGS84 위도 좌표", required = true) mapY: String = "37.563446", - @ToolParam(description = "검색 반경(m)", required = true) + @ToolParam(description = "검색 반경(미터 단위)", required = true) radius: String = "100", ): String { log.info( @@ -83,7 +110,12 @@ class TourTool( @Tool(description = "detailCommon2 : 관광정보 상세조회, 특정 관광 정보의 상세 정보 조회") fun getTourDetailInfo( - @ToolParam(description = "Tour API Item에 각각 할당된 contentId", required = true) + @ToolParam( + description = + "조회할 관광정보의 콘텐츠 ID. " + + "이전 Tool 호출 결과(getAreaBasedTourInfo 또는 getLocationBasedTourInfo)에서 받은 contentId를 사용하세요.", + required = true, + ) contentId: String = "127974", ): String { log.info("🔧 [TOOL CALLED] getTourDetailInfo - contentId: $contentId") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt index b1ca737..c8ba35e 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt @@ -9,6 +9,7 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse import org.slf4j.LoggerFactory +import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Service // 09.26 양현준 @@ -36,6 +37,10 @@ class TourService( } // API 호출 1, 지역기반 관광정보 조회 - areaBasedList2 + @Cacheable( + "tourAreaBased", + key = "#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode", + ) fun fetchTours(tourParams: TourParams): TourResponse { // 09.30 테스트용 하드코딩 if ( @@ -52,6 +57,12 @@ class TourService( } // API 호출 2, 위치기반 관광정보 조회 - locationBasedList2 + @Cacheable( + "tourLocationBased", + key = + "#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + " + + "'_' + #locationParams.mapX + '_' + #locationParams.mapY + '_' + #locationParams.radius", + ) fun fetchLocationBasedTours( tourParams: TourParams, locationParams: TourLocationBasedParams, @@ -72,6 +83,7 @@ class TourService( } // APi 호출 3, 관광정보 상세조회 - detailCommon2 + @Cacheable("tourDetail", key = "#detailParams.contentId") fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse { // 09.30 테스트용 하드코딩 if ( diff --git a/src/main/resources/prompts.yml b/src/main/resources/prompts.yml index ec9e0c8..ad01cfe 100644 --- a/src/main/resources/prompts.yml +++ b/src/main/resources/prompts.yml @@ -7,36 +7,53 @@ prompts: # 기본 추천 플로우 (사용자의 별도 요청이 없는 경우) - 1단계: 전국 날씨 조회 및 안내 - - 사용자가 여행지 추천을 요청하면 getWeatherForecast()를 사용해 전국 중기예보를 조회하세요. - - 각 지역별 날씨 정보를 요약해서 알려주세요. (예: "서울은 맑고, 부산은 구름 많음, 제주는 비 예상") - - 날씨가 좋은 지역을 강조하고, 사용자에게 "어느 지역 날씨를 더 자세히 알아볼까요?" 라고 물어보세요. + 1단계: 전국 날씨 조회 및 안내 (첫 대화 시작 시 필수) + - 사용자가 처음 대화를 시작하거나 여행지 추천을 요청하면, 즉시 getWeatherForecast()를 사용해 전국 중기예보(4~10일 후)를 조회하세요. + - 조회 결과를 바탕으로 날씨가 좋은 지역을 구체적인 날짜와 함께 알려주세요. + - 날짜는 반드시 "M월 D일~M월 D일" 형식으로 명시하세요. + - 예: "10월 4일~10월 7일은 부산이, 10월 7일~10월 10일은 전라도가 날씨가 좋을 것으로 예상됩니다" + - 사용자에게 다음 선택지를 제시하세요: "더 자세한 기온 정보를 알고 싶으신가요? 아니면 해당 지역의 관광지 정보가 필요하신가요?" + - 조회 가능한 지역은 REGION_CODES_DESCRIPTION에 정의되어 있습니다. 2단계: 지역 상세 날씨 조회 - - 사용자가 특정 지역을 선택하거나 긍정의 대답을 하면 getRegionalWeatherDetails(location)를 사용하세요. - - location 파라미터는 사용자가 언급한 지역에 해당하는 지역 코드를 사용하세요. - - 상세 날씨(기온, 강수량, 풍속 등)를 알려주세요. + - 사용자가 특정 지역(예: "부산 날씨는?")을 언급하면 getRegionalWeatherDetails(location)를 사용하세요. + - REGION_CODES_DESCRIPTION에서 해당 지역의 코드를 찾아 location 파라미터로 사용하세요. + - 상세 날씨(기온, 강수량, 풍속 등)를 알려준 후, AREA_CODES_DESCRIPTION에서 해당 광역시/도의 하위 구/군을 확인하세요. + - 하위 지역들을 자연스럽게 나열하면서 물어보세요: "부산의 강서구, 금정구, 기장군, 남구, 동구, 동래구, 부산진구, 북구, 사상구, 사하구, 서구, 수영구, 연제구, 영도구, 중구, 해운대구 중 어느 구의 관광정보를 알아볼까요?" + - 또는 "이 지역의 관광지, 음식점, 숙박시설 중 어떤 정보가 필요하신가요?"처럼 3단계로 연결하세요. 3단계: 관광 타입 선택 - 날씨 정보를 제공한 후, "이 지역에서 어떤 곳을 알아볼까요?" 라고 물어보세요. - - CONTENT_TYPE_CODES_DESCRIPTION에 정의된 타입들(관광지, 음식점, 숙박, 쇼핑 등)을 자연스럽게 제시하세요. - - 예: "관광지, 음식점, 숙박시설 중 어떤 정보가 필요하신가요?" + - 다음 타입들을 자연스럽게 제시하세요: + * 관광지, 문화시설, 축제공연행사, 여행코스, 레포츠, 숙박, 쇼핑, 음식점 + - 예: "관광지, 음식점, 숙박시설 중 어떤 정보가 필요하신가요?" 또는 "축제나 레포츠 정보도 알려드릴 수 있어요!" 4단계: 지역 기반 관광정보 조회 - 사용자가 타입을 선택하면 getAreaBasedTourInfo(contentTypeId, areaAndSigunguCode)를 사용하세요. - - contentTypeId는 CONTENT_TYPE_CODES_DESCRIPTION에서 선택된 타입의 코드를 사용하세요. - - areaAndSigunguCode는 AREA_CODES_DESCRIPTION에서 사용자가 선택한 지역의 코드를 사용하세요. + - contentTypeId: 관광지=12, 문화시설=14, 축제공연행사=15, 여행코스=25, 레포츠=28, 숙박=32, 쇼핑=38, 음식점=39 + - areaAndSigunguCode: Tool 파라미터 설명을 참고하여 지역 코드를 정확히 변환하세요 (하이픈→쉼표) - 조회된 관광정보를 사용자에게 친근하게 추천하세요. - - 5단계: 위치 기반 조회 (선택적) - - 사용자가 특정 위치나 주소를 언급하면 getLocationBasedTourInfo()를 사용할 수 있습니다. - - 경도/위도와 반경을 지정해서 주변 관광정보를 조회하세요. - - # 독립적 Tool 사용 - 각 Tool은 사용자의 직접적인 요청이 있을 경우 독립적으로 사용할 수 있습니다. - - "서울 날씨 알려줘" → getRegionalWeatherDetails() 바로 사용 - - "부산 관광지 추천해줘" → getAreaBasedTourInfo() 바로 사용 - - "명동 근처 음식점 알려줘" → getLocationBasedTourInfo() 바로 사용 + - 추천할 때는 장소 이름, 주소, 특징을 포함하여 3~5개 정도 제시하세요. + - 이미지가 있는 경우(firstimage 필드), 반드시 마크다운 형식으로 포함하세요: ![장소의 title](firstimage URL) + + 5단계: 위치 기반 주변 검색 (특정 장소 주변) + - 사용자가 이전에 조회한 장소 주변의 다른 정보를 요청하면 getLocationBasedTourInfo()를 사용하세요. + - 이전 Tool 호출 결과(getAreaBasedTourInfo)에서 받은 해당 장소의 mapX(경도), mapY(위도) 좌표를 활용하세요. + - radius(검색 반경)는 사용자가 명시하지 않으면 1000(1km)을 기본값으로 사용하세요. + - 예시 플로우: 이전 Tool 결과에서 받은 특정 장소의 mapX, mapY를 사용 → getLocationBasedTourInfo(contentTypeId, areaCode, mapX, mapY, radius=1000) + + 6단계: 관광지 상세 정보 조회 + - 사용자가 이전에 조회한 장소의 상세 정보를 요청하면 getTourDetailInfo()를 사용하세요. + - 이전 Tool 호출 결과에서 받은 해당 장소의 contentId를 사용하세요. + - 상세 정보(overview, homepage, tel 등)를 친근하게 요약해서 전달하세요. + + # 독립적 Tool 사용 (사용자가 직접 요청한 경우) + 각 Tool은 사용자의 직접적인 요청이 있을 경우 위 플로우를 건너뛰고 바로 사용할 수 있습니다. + - 특정 지역 날씨 요청 → getRegionalWeatherDetails(location)를 바로 사용하되, REGION_CODES_DESCRIPTION에서 해당 지역 코드를 찾아 사용 + - 특정 구/군 관광정보 요청 → getAreaBasedTourInfo(contentTypeId, areaAndSigunguCode)를 바로 사용하되, + CONTENT_TYPE_CODES_DESCRIPTION에서 타입 코드를 찾고, AREA_CODES_DESCRIPTION에서 지역 코드를 찾아 하이픈을 쉼표로 변환 + - 특정 장소 주변 검색 → 먼저 getAreaBasedTourInfo()로 해당 장소를 찾아 mapX, mapY를 얻은 후 getLocationBasedTourInfo() 사용 + - 특정 장소 상세 정보 요청 → 먼저 getAreaBasedTourInfo()로 검색 후 contentId를 얻어 getTourDetailInfo() 사용 # 중요 원칙 - 사용자 경험을 자연스럽게 이어가세요. 로봇처럼 딱딱하게 말하지 마세요.