From f10e66814201274c1ae696c18f5cb232b72bb76c Mon Sep 17 00:00:00 2001 From: MadVedi Date: Sun, 9 Nov 2025 20:03:42 +0900 Subject: [PATCH 1/8] Update CatalogService.java --- src/main/java/org/mybatis/jpetstore/service/CatalogService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/mybatis/jpetstore/service/CatalogService.java b/src/main/java/org/mybatis/jpetstore/service/CatalogService.java index 1af097edd..decca2f86 100644 --- a/src/main/java/org/mybatis/jpetstore/service/CatalogService.java +++ b/src/main/java/org/mybatis/jpetstore/service/CatalogService.java @@ -88,3 +88,5 @@ public boolean isItemInStock(String itemId) { return itemMapper.getInventoryQuantity(itemId) > 0; } } + +//test \ No newline at end of file From 047dc5e79d79de0674888d3f8d37a60680ac0f0a Mon Sep 17 00:00:00 2001 From: jgk07014 Date: Fri, 14 Nov 2025 15:35:50 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=84=A4=EB=AC=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=8F=BC=20=EA=B0=9C=EB=B0=9C=20=EB=B0=8F=20?= =?UTF-8?q?DB=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/mybatis/jpetstore/domain/Account.java | 59 ++++++++- .../database/jpetstore-hsqldb-data.sql | 6 +- .../database/jpetstore-hsqldb-dataload.sql | 6 +- .../database/jpetstore-hsqldb-schema.sql | 8 +- .../jpetstore/mapper/AccountMapper.xml | 26 +++- .../jsp/account/IncludeAccountFields.jsp | 124 +++++++++++++++++- .../jpetstore/mapper/AccountMapperTest.java | 20 ++- 7 files changed, 231 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/mybatis/jpetstore/domain/Account.java b/src/main/java/org/mybatis/jpetstore/domain/Account.java index eb87ca17d..ad1758b46 100644 --- a/src/main/java/org/mybatis/jpetstore/domain/Account.java +++ b/src/main/java/org/mybatis/jpetstore/domain/Account.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2022 the original author or authors. + * Copyright 2010-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,14 @@ public class Account implements Serializable { private boolean bannerOption; private String bannerName; + // New survey-related fields + private String residenceEnv; + private String carePeriod; + private String petColorPref; + private String petSizePref; + private String activityTime; + private String dietManagement; + public String getUsername() { return username; } @@ -193,4 +201,53 @@ public void setBannerName(String bannerName) { this.bannerName = bannerName; } + // New survey-related getters and setters + public String getResidenceEnv() { + return residenceEnv; + } + + public void setResidenceEnv(String residenceEnv) { + this.residenceEnv = residenceEnv; + } + + public String getCarePeriod() { + return carePeriod; + } + + public void setCarePeriod(String carePeriod) { + this.carePeriod = carePeriod; + } + + public String getPetColorPref() { + return petColorPref; + } + + public void setPetColorPref(String petColorPref) { + this.petColorPref = petColorPref; + } + + public String getPetSizePref() { + return petSizePref; + } + + public void setPetSizePref(String petSizePref) { + this.petSizePref = petSizePref; + } + + public String getActivityTime() { + return activityTime; + } + + public void setActivityTime(String activityTime) { + this.activityTime = activityTime; + } + + public String getDietManagement() { + return dietManagement; + } + + public void setDietManagement(String dietManagement) { + this.dietManagement = dietManagement; + } + } diff --git a/src/main/resources/database/jpetstore-hsqldb-data.sql b/src/main/resources/database/jpetstore-hsqldb-data.sql index 30928b70e..36db32501 100644 --- a/src/main/resources/database/jpetstore-hsqldb-data.sql +++ b/src/main/resources/database/jpetstore-hsqldb-data.sql @@ -1,5 +1,5 @@ -- --- Copyright 2010-2023 the original author or authors. +-- Copyright 2010-2025 the original author or authors. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -173,8 +173,8 @@ INSERT INTO SIGNON VALUES('ACID','ACID'); INSERT INTO ACCOUNT VALUES('j2ee','yourname@yourdomain.com','ABC', 'XYX', 'OK', '901 San Antonio Road', 'MS UCUP02-206', 'Palo Alto', 'CA', '94303', 'USA', '555-555-5555'); INSERT INTO ACCOUNT VALUES('ACID','acid@yourdomain.com','ABC', 'XYX', 'OK', '901 San Antonio Road', 'MS UCUP02-206', 'Palo Alto', 'CA', '94303', 'USA', '555-555-5555'); -INSERT INTO PROFILE VALUES('j2ee','english','DOGS',1,1); -INSERT INTO PROFILE VALUES('ACID','english','CATS',1,1); +INSERT INTO PROFILE VALUES('j2ee','english','DOGS',1,1, NULL, NULL, NULL, NULL, NULL, NULL); +INSERT INTO PROFILE VALUES('ACID','english','CATS',1,1, NULL, NULL, NULL, NULL, NULL, NULL); INSERT INTO BANNERDATA VALUES ('FISH',''); INSERT INTO BANNERDATA VALUES ('CATS',''); diff --git a/src/main/resources/database/jpetstore-hsqldb-dataload.sql b/src/main/resources/database/jpetstore-hsqldb-dataload.sql index 6487da7ed..69006b9c0 100644 --- a/src/main/resources/database/jpetstore-hsqldb-dataload.sql +++ b/src/main/resources/database/jpetstore-hsqldb-dataload.sql @@ -1,5 +1,5 @@ -- --- Copyright 2010-2022 the original author or authors. +-- Copyright 2010-2025 the original author or authors. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ INSERT INTO signon VALUES('ACID','ACID'); INSERT INTO account VALUES('j2ee','yourname@yourdomain.com','ABC', 'XYX', 'OK', '901 San Antonio Road', 'MS UCUP02-206', 'Palo Alto', 'CA', '94303', 'USA', '555-555-5555'); INSERT INTO account VALUES('ACID','acid@yourdomain.com','ABC', 'XYX', 'OK', '901 San Antonio Road', 'MS UCUP02-206', 'Palo Alto', 'CA', '94303', 'USA', '555-555-5555'); -INSERT INTO profile VALUES('j2ee','english','DOGS',1,1); -INSERT INTO profile VALUES('ACID','english','CATS',1,1); +INSERT INTO profile VALUES('j2ee','english','DOGS',1,1, NULL, NULL, NULL, NULL, NULL, NULL); +INSERT INTO profile VALUES('ACID','english','CATS',1,1, NULL, NULL, NULL, NULL, NULL, NULL); INSERT INTO bannerdata VALUES ('FISH',''); INSERT INTO bannerdata VALUES ('CATS',''); diff --git a/src/main/resources/database/jpetstore-hsqldb-schema.sql b/src/main/resources/database/jpetstore-hsqldb-schema.sql index 6b51720e5..4873fde96 100644 --- a/src/main/resources/database/jpetstore-hsqldb-schema.sql +++ b/src/main/resources/database/jpetstore-hsqldb-schema.sql @@ -1,5 +1,5 @@ -- --- Copyright 2010-2022 the original author or authors. +-- Copyright 2010-2025 the original author or authors. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -55,6 +55,12 @@ create table profile ( favcategory varchar(30), mylistopt int, banneropt int, + residence_env varchar(255), + care_period varchar(255), + pet_color_pref varchar(255), + pet_size_pref varchar(255), + activity_time varchar(255), + diet_management varchar(255), constraint pk_profile primary key (userid) ); diff --git a/src/main/resources/org/mybatis/jpetstore/mapper/AccountMapper.xml b/src/main/resources/org/mybatis/jpetstore/mapper/AccountMapper.xml index aecc4f432..a736f2cff 100644 --- a/src/main/resources/org/mybatis/jpetstore/mapper/AccountMapper.xml +++ b/src/main/resources/org/mybatis/jpetstore/mapper/AccountMapper.xml @@ -1,7 +1,7 @@ + + + 1. What is your current living environment like? + + -- Select -- + Indoor-focused (apartment, studio, etc.) + Includes outdoor space (yard, balcony, etc.) + Humid environment (near river/lake) + Dry environment + Can maintain special spaces (aquarium, terrarium, etc.) + + + + 2. What is your preferred or manageable pet care period? + + -- Select -- + 5 years or less + 5-10 years + 10 years or more + Doesn't matter + + + + 3. What pet color best matches your living space's atmosphere or interior colors? + + -- Select -- + Light colors (white | ivory tones) + Warm colors (brown | gold tones) + Dark colors (black | gray tones) + Mixed colors + Doesn't matter + + + + 4. Considering your living space and lifestyle, what pet size do you think is appropriate? + + -- Select -- + Small (suitable for studio/small apartment) + Medium (regular home, some space available) + Large (yard and spacious indoor area possible) + + + + 5. When are you most active? + + -- Select -- + Day + Night + Irregular + + + + 6. When managing your pet's diet, what level of care can you provide? + + -- Select -- + Simple kibble-focused (carnivore, omnivore) + Can provide diverse diet (vegetarian, mixed possible) + Can manage special diets (insect-based, dried food, etc.) + + + diff --git a/src/test/java/org/mybatis/jpetstore/mapper/AccountMapperTest.java b/src/test/java/org/mybatis/jpetstore/mapper/AccountMapperTest.java index 29e42ad83..faa0d6fc0 100644 --- a/src/test/java/org/mybatis/jpetstore/mapper/AccountMapperTest.java +++ b/src/test/java/org/mybatis/jpetstore/mapper/AccountMapperTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2022 the original author or authors. + * Copyright 2010-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,10 +147,15 @@ void insertProfile() { // then Map record = jdbcTemplate.queryForMap("SELECT * FROM profile WHERE userid = ?", "mybatis"); - assertThat(record).hasSize(5).containsEntry("USERID", account.getUsername()) + assertThat(record).hasSize(11).containsEntry("USERID", account.getUsername()) .containsEntry("LANGPREF", account.getLanguagePreference()) .containsEntry("FAVCATEGORY", account.getFavouriteCategoryId()).containsEntry("MYLISTOPT", 1) - .containsEntry("BANNEROPT", 0); + .containsEntry("BANNEROPT", 0).containsEntry("RESIDENCE_ENV", account.getResidenceEnv()) + .containsEntry("CARE_PERIOD", account.getCarePeriod()) + .containsEntry("PET_COLOR_PREF", account.getPetColorPref()) + .containsEntry("PET_SIZE_PREF", account.getPetSizePref()) + .containsEntry("ACTIVITY_TIME", account.getActivityTime()) + .containsEntry("DIET_MANAGEMENT", account.getDietManagement()); } @Test @@ -221,10 +226,15 @@ void updateProfile() { // then Map record = jdbcTemplate.queryForMap("SELECT * FROM profile WHERE userid = ?", "j2ee"); - assertThat(record).hasSize(5).containsEntry("USERID", account.getUsername()) + assertThat(record).hasSize(11).containsEntry("USERID", account.getUsername()) .containsEntry("LANGPREF", account.getLanguagePreference()) .containsEntry("FAVCATEGORY", account.getFavouriteCategoryId()).containsEntry("MYLISTOPT", 0) - .containsEntry("BANNEROPT", 0); + .containsEntry("BANNEROPT", 0).containsEntry("RESIDENCE_ENV", account.getResidenceEnv()) + .containsEntry("CARE_PERIOD", account.getCarePeriod()) + .containsEntry("PET_COLOR_PREF", account.getPetColorPref()) + .containsEntry("PET_SIZE_PREF", account.getPetSizePref()) + .containsEntry("ACTIVITY_TIME", account.getActivityTime()) + .containsEntry("DIET_MANAGEMENT", account.getDietManagement()); } @Test From 836fe762fda16576488a1a26953865bae34ec699 Mon Sep 17 00:00:00 2001 From: jgk07014 Date: Fri, 14 Nov 2025 15:39:09 +0900 Subject: [PATCH 3/8] =?UTF-8?q?chore:=20Dockerfile=20=EC=A0=80=EC=9E=91?= =?UTF-8?q?=EA=B6=8C=20=EC=97=B0=EB=8F=84=202025=EB=85=84=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9E=90=EB=8F=99=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e8705397d..3b4b7e94a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # -# Copyright 2010-2023 the original author or authors. +# Copyright 2010-2025 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 2cf9491ee6a4e0143d8226bf36e0f22dd05930d5 Mon Sep 17 00:00:00 2001 From: jgk07014 Date: Wed, 19 Nov 2025 22:27:52 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20LLM=EC=9C=BC=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=BD=91=EA=B3=A0=20DB?= =?UTF-8?q?=EC=97=90=20=EC=A0=80=EC=9E=A5(=EC=9E=84=EC=8B=9C=20100?= =?UTF-8?q?=EA=B0=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A7=8C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + pom.xml | 10 + .../domain/SurveyRecommendation.java | 104 +++ .../mapper/SurveyRecommendationMapper.java | 34 + .../service/OpenAiRecommendationService.java | 123 ++++ .../util/RecommendationDataGenerator.java | 125 ++++ .../SurveyRecommendationActionBean.java | 62 ++ src/main/resources/applicationContext.xml | 59 ++ .../jpetstore-hsqldb-recommendations.sql | 596 ++++++++++++++++++ .../database/jpetstore-hsqldb-schema.sql | 11 + .../mapper/SurveyRecommendationMapper.xml | 102 +++ .../webapp/WEB-INF/applicationContext.xml | 3 +- .../jsp/survey/SurveyRecommendationView.jsp | 60 ++ .../jpetstore/mapper/MapperTestContext.java | 5 +- 14 files changed, 1293 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/mybatis/jpetstore/domain/SurveyRecommendation.java create mode 100644 src/main/java/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.java create mode 100644 src/main/java/org/mybatis/jpetstore/service/OpenAiRecommendationService.java create mode 100644 src/main/java/org/mybatis/jpetstore/util/RecommendationDataGenerator.java create mode 100644 src/main/java/org/mybatis/jpetstore/web/actions/SurveyRecommendationActionBean.java create mode 100644 src/main/resources/applicationContext.xml create mode 100644 src/main/resources/database/jpetstore-hsqldb-recommendations.sql create mode 100644 src/main/resources/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.xml create mode 100644 src/main/webapp/WEB-INF/jsp/survey/SurveyRecommendationView.jsp diff --git a/.gitignore b/.gitignore index 925c487fb..4f641c11e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ *.properties .env .github/keys/ +.DS_Store +src/main/resources/credentials/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4c774b342..378b9871c 100644 --- a/pom.xml +++ b/pom.xml @@ -165,6 +165,16 @@ hsqldb ${hsqldb.version} + + com.theokanning.openai-gpt3-java + service + 0.18.2 + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + diff --git a/src/main/java/org/mybatis/jpetstore/domain/SurveyRecommendation.java b/src/main/java/org/mybatis/jpetstore/domain/SurveyRecommendation.java new file mode 100644 index 000000000..85a47e512 --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/domain/SurveyRecommendation.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.domain; + +import java.io.Serializable; + +public class SurveyRecommendation implements Serializable { + + private static final long serialVersionUID = 1L; + + private int surveyRecommendationId; + private String residenceEnv; + private String carePeriod; + private String petColorPref; + private String petSizePref; + private String activityTime; + private String dietManagement; + private String recommendedJsonData; + + public int getSurveyRecommendationId() { + return surveyRecommendationId; + } + + public void setSurveyRecommendationId(int surveyRecommendationId) { + this.surveyRecommendationId = surveyRecommendationId; + } + + public String getResidenceEnv() { + return residenceEnv; + } + + public void setResidenceEnv(String residenceEnv) { + this.residenceEnv = residenceEnv; + } + + public String getCarePeriod() { + return carePeriod; + } + + public void setCarePeriod(String carePeriod) { + this.carePeriod = carePeriod; + } + + public String getPetColorPref() { + return petColorPref; + } + + public void setPetColorPref(String petColorPref) { + this.petColorPref = petColorPref; + } + + public String getPetSizePref() { + return petSizePref; + } + + public void setPetSizePref(String petSizePref) { + this.petSizePref = petSizePref; + } + + public String getActivityTime() { + return activityTime; + } + + public void setActivityTime(String activityTime) { + this.activityTime = activityTime; + } + + public String getDietManagement() { + return dietManagement; + } + + public void setDietManagement(String dietManagement) { + this.dietManagement = dietManagement; + } + + public String getRecommendedJsonData() { + return recommendedJsonData; + } + + public void setRecommendedJsonData(String recommendedJsonData) { + this.recommendedJsonData = recommendedJsonData; + } + + @Override + public String toString() { + return "SurveyRecommendation{" + "surveyRecommendationId=" + surveyRecommendationId + ", residenceEnv='" + + residenceEnv + "'" + ", carePeriod='" + carePeriod + "'" + ", petColorPref='" + petColorPref + "'" + + ", petSizePref='" + petSizePref + "'" + ", activityTime='" + activityTime + "'" + ", dietManagement='" + + dietManagement + "'" + ", recommendedJsonData='" + recommendedJsonData + "'" + '}'; + } +} diff --git a/src/main/java/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.java b/src/main/java/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.java new file mode 100644 index 000000000..ed0209a67 --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.mapper; + +import java.util.List; + +import org.mybatis.jpetstore.domain.SurveyRecommendation; + +public interface SurveyRecommendationMapper { + + List getSurveyRecommendations(); + + SurveyRecommendation getSurveyRecommendationById(int surveyRecommendationId); + + void insertSurveyRecommendation(SurveyRecommendation surveyRecommendation); + + void updateSurveyRecommendation(SurveyRecommendation surveyRecommendation); + + void deleteSurveyRecommendation(int surveyRecommendationId); + +} diff --git a/src/main/java/org/mybatis/jpetstore/service/OpenAiRecommendationService.java b/src/main/java/org/mybatis/jpetstore/service/OpenAiRecommendationService.java new file mode 100644 index 000000000..b9e587e83 --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/service/OpenAiRecommendationService.java @@ -0,0 +1,123 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.service.OpenAiService; + +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import org.mybatis.jpetstore.domain.Account; +import org.mybatis.jpetstore.domain.Product; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class OpenAiRecommendationService { + + private static final Logger logger = LoggerFactory.getLogger(OpenAiRecommendationService.class); + private static final String OPENAI_API_KEY; + private static final String OPENAI_MODEL = "gpt-3.5-turbo"; // Or "gpt-4", "gpt-4o" etc. + + private final OpenAiService openAiService; + + static { + String apiKey = null; + try { + ObjectMapper mapper = new ObjectMapper(); + InputStream is = OpenAiRecommendationService.class.getClassLoader() + .getResourceAsStream("credentials/openai_credentials.json"); + if (is == null) { + throw new IOException("openai_credentials.json not found in classpath."); + } + JsonNode rootNode = mapper.readTree(is); + apiKey = rootNode.path("openai_api_key").asText(); + if (apiKey.isEmpty() || "YOUR_OPENAI_API_KEY_HERE".equals(apiKey)) { + throw new IllegalArgumentException( + "OPENAI_API_KEY not found or is a placeholder in openai_credentials.json. Please update the file."); + } + } catch (IOException e) { + logger.error("Error reading openai_credentials.json: {}", e.getMessage()); + throw new RuntimeException("Failed to load OpenAI API key from credentials file.", e); + } + OPENAI_API_KEY = apiKey; + } + + public OpenAiRecommendationService() { + // Set a timeout for the OpenAI API calls + this.openAiService = new OpenAiService(OPENAI_API_KEY, Duration.ofSeconds(30)); + } + + public String getRecommendation(Account account, List productList) { + try { + StringBuilder promptBuilder = new StringBuilder(); + promptBuilder.append( + "Based on the following user preferences and available products, recommend 2 to 5 products from the list.\n"); + promptBuilder.append("User Preferences:\n"); + // promptBuilder.append(" - Favourite Category: ").append(account.getFavouriteCategoryId()).append("\n"); + // promptBuilder.append(" - Language Preference: ").append(account.getLanguagePreference()).append("\n"); + promptBuilder.append(" - Living Environment: ").append(account.getResidenceEnv()).append("\n"); + promptBuilder.append(" - Pet Care Period: ").append(account.getCarePeriod()).append("\n"); + promptBuilder.append(" - Pet Color Preference: ").append(account.getPetColorPref()).append("\n"); + promptBuilder.append(" - Pet Size Preference: ").append(account.getPetSizePref()).append("\n"); + promptBuilder.append(" - Activity Time: ").append(account.getActivityTime()).append("\n"); + promptBuilder.append(" - Diet Management: ").append(account.getDietManagement()).append("\n"); + promptBuilder.append("\nAvailable Products (Product ID - Name - Category):\n"); + for (Product product : productList) { + promptBuilder.append(" - ").append(product.getProductId()).append(" - ").append(product.getName()) + .append(" - ").append(product.getCategoryId()).append("\n"); + } + promptBuilder.append( + "\nBased on the 'User Preferences', you MUST recommend between 2 and 5 products from the 'Available Products' list above. DO NOT recommend fewer than 2 or more than 5 products. Output the recommendations as a JSON array of objects, where each object has 'productId' and 'productName' fields. Do not include any other text or explanation outside the JSON. Example output:\n" + + "[\n" + " {\"productId\": \"FI-FW-01\", \"productName\": \"Koi\"},\n" + + " {\"productId\": \"FI-FW-02\", \"productName\": \"Goldfish\"}\n" + "]"); + + String fullPrompt = promptBuilder.toString(); + ChatMessage userMessage = new ChatMessage("user", fullPrompt); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder().model(OPENAI_MODEL) + .messages(Collections.singletonList(userMessage)).maxTokens(500) // Limit the response length + .temperature(0.7) // Creativity level + .build(); + + logger.info("Sending prompt to OpenAI API. Prompt length: {} characters", fullPrompt.length()); + logger.info("Full Prompt sent to OpenAI API:\n{}", fullPrompt); + String response = openAiService.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage() + .getContent(); + + logger.info("OpenAI API Response: {}", response); + return response; + + } catch (Exception e) { + logger.error("Error calling OpenAI API: {}", e.getMessage(), e); + return "Error getting recommendation from OpenAI: " + e.getMessage(); + } + } + + public List listAvailableModels() { + // This library does not directly expose a simple list of available chat models. + // Hardcoding common ones for demonstration. + return List.of("gpt-3.5-turbo", "gpt-4", "gpt-4o"); + } +} diff --git a/src/main/java/org/mybatis/jpetstore/util/RecommendationDataGenerator.java b/src/main/java/org/mybatis/jpetstore/util/RecommendationDataGenerator.java new file mode 100644 index 000000000..939b6a6bd --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/util/RecommendationDataGenerator.java @@ -0,0 +1,125 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.mybatis.jpetstore.domain.Account; +import org.mybatis.jpetstore.domain.Category; +import org.mybatis.jpetstore.domain.Product; +import org.mybatis.jpetstore.service.AccountService; +import org.mybatis.jpetstore.service.CatalogService; +import org.mybatis.jpetstore.service.OpenAiRecommendationService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class RecommendationDataGenerator { + + private static final String SQL_FILE_PATH = "src/main/resources/database/jpetstore-hsqldb-recommendations.sql"; + + public static void main(String[] args) { + // Initialize Spring context to get access to services + ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); + AccountService accountService = context.getBean(AccountService.class); + CatalogService catalogService = context.getBean(CatalogService.class); + OpenAiRecommendationService openAiRecommendationService = context.getBean(OpenAiRecommendationService.class); + + // Define all survey options + List residenceEnvs = Arrays.asList("Indoor-focused (apartment, studio, etc.)", + "Includes outdoor space (yard, balcony, etc.)", "Humid environment (near river/lake)", "Dry environment", + "Can maintain special spaces (aquarium, terrarium, etc.)"); + List carePeriods = Arrays.asList("5 years or less", "5-10 years", "10 years or more", "Doesn't matter"); + List petColorPrefs = Arrays.asList("Light colors (white | ivory tones)", "Warm colors (brown | gold tones)", + "Dark colors (black | gray tones)", "Mixed colors", "Doesn't matter"); + List petSizePrefs = Arrays.asList("Small (suitable for studio/small apartment)", + "Medium (regular home, some space available)", "Large (yard and spacious indoor area possible)"); + List activityTimes = Arrays.asList("Day", "Night", "Irregular"); + List dietManagements = Arrays.asList("Simple kibble-focused (carnivore, omnivore)", + "Can provide diverse diet (vegetarian, mixed possible)", + "Can manage special diets (insect-based, dried food, etc.)"); + + // Retrieve all products from the catalog + List allProducts = new ArrayList<>(); + List categories = catalogService.getCategoryList(); + for (Category category : categories) { + allProducts.addAll(catalogService.getProductListByCategory(category.getCategoryId())); + } + + try (var writer = Files.newBufferedWriter(Path.of(SQL_FILE_PATH))) { + writer.write( + "INSERT INTO SURVEY_RECOMMENDATIONS (survey_recommendation_id, residence_env, care_period, pet_color_pref, pet_size_pref, activity_time, diet_management, recommended_json_data) VALUES\n"); + + long idCounter = 1; + List insertValues = new ArrayList<>(); + + for (String residenceEnv : residenceEnvs) { + for (String carePeriod : carePeriods) { + for (String petColorPref : petColorPrefs) { + for (String petSizePref : petSizePrefs) { + for (String activityTime : activityTimes) { + for (String dietManagement : dietManagements) { + Account dummyAccount = new Account(); + dummyAccount.setResidenceEnv(residenceEnv); + dummyAccount.setCarePeriod(carePeriod); + dummyAccount.setPetColorPref(petColorPref); + dummyAccount.setPetSizePref(petSizePref); + dummyAccount.setActivityTime(activityTime); + dummyAccount.setDietManagement(dietManagement); + + String recommendedJson = openAiRecommendationService.getRecommendation(dummyAccount, allProducts); + + // Escape single quotes for SQL insertion + String escapedResidenceEnv = residenceEnv.replace("'", "''"); + String escapedCarePeriod = carePeriod.replace("'", "''"); + String escapedPetColorPref = petColorPref.replace("'", "''"); + String escapedPetSizePref = petSizePref.replace("'", "''"); + String escapedActivityTime = activityTime.replace("'", "''"); + String escapedDietManagement = dietManagement.replace("'", "''"); + String escapedRecommendedJson = recommendedJson.replace("'", "''"); + + insertValues.add(String.format("(%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", idCounter++, + escapedResidenceEnv, escapedCarePeriod, escapedPetColorPref, escapedPetSizePref, + escapedActivityTime, escapedDietManagement, escapedRecommendedJson)); + System.out.println("Generated recommendation for combination " + (idCounter - 1)); + // Add a small delay to avoid hitting OpenAI rate limits during generation + try { + Thread.sleep(1000); // 1 second delay + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + } + } + } + + writer.write(String.join(",\n", insertValues)); + writer.write(";\n"); + System.out.println("SQL INSERT statements generated successfully to " + SQL_FILE_PATH); + } catch (IOException e) { + System.err.println("Error writing SQL file: " + e.getMessage()); + } catch (Exception e) { + System.err.println("Error during recommendation generation: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/mybatis/jpetstore/web/actions/SurveyRecommendationActionBean.java b/src/main/java/org/mybatis/jpetstore/web/actions/SurveyRecommendationActionBean.java new file mode 100644 index 000000000..6313aec02 --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/web/actions/SurveyRecommendationActionBean.java @@ -0,0 +1,62 @@ +// test url is http://localhost:8080/jpetstore/actions/SurveyRecommendation.action +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.web.actions; + +import java.util.List; + +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.action.ActionBeanContext; +import net.sourceforge.stripes.action.DefaultHandler; +import net.sourceforge.stripes.action.ForwardResolution; +import net.sourceforge.stripes.action.Resolution; +import net.sourceforge.stripes.integration.spring.SpringBean; + +import org.mybatis.jpetstore.domain.SurveyRecommendation; +import org.mybatis.jpetstore.mapper.SurveyRecommendationMapper; + +public class SurveyRecommendationActionBean implements ActionBean { + + private static final String VIEW_SURVEY_RECOMMENDATIONS = "/WEB-INF/jsp/survey/SurveyRecommendationView.jsp"; + + @SpringBean + private transient SurveyRecommendationMapper surveyRecommendationMapper; + + private ActionBeanContext context; + private List surveyRecommendations; + + public ActionBeanContext getContext() { + return context; + } + + public void setContext(ActionBeanContext context) { + this.context = context; + } + + public List getSurveyRecommendations() { + return surveyRecommendations; + } + + public void setSurveyRecommendations(List surveyRecommendations) { + this.surveyRecommendations = surveyRecommendations; + } + + @DefaultHandler + public Resolution viewAllSurveyRecommendations() { + surveyRecommendations = surveyRecommendationMapper.getSurveyRecommendations(); + return new ForwardResolution(VIEW_SURVEY_RECOMMENDATIONS); + } +} diff --git a/src/main/resources/applicationContext.xml b/src/main/resources/applicationContext.xml new file mode 100644 index 000000000..769d64492 --- /dev/null +++ b/src/main/resources/applicationContext.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/database/jpetstore-hsqldb-recommendations.sql b/src/main/resources/database/jpetstore-hsqldb-recommendations.sql new file mode 100644 index 000000000..415af92fb --- /dev/null +++ b/src/main/resources/database/jpetstore-hsqldb-recommendations.sql @@ -0,0 +1,596 @@ +-- +-- Copyright 2010-2025 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +INSERT INTO SURVEY_RECOMMENDATIONS (survey_recommendation_id, residence_env, care_period, pet_color_pref, pet_size_pref, activity_time, diet_management, recommended_json_data) VALUES + (1, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (2, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (3, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (4, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (5, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (6, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (7, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (8, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (9, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (10, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (11, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (12, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (13, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (14, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (15, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (16, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (17, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (18, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Medium (regular home, some space available)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-SW-01", "productName": "Angelfish"} +]'), + (19, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (20, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (21, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (22, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (23, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (24, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (25, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (26, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (27, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Light colors (white | ivory tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "RP-LI-02", "productName": "Iguana"} +]'), + (28, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (29, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (30, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (31, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (32, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (33, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (34, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (35, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"} +]'), + (36, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (37, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (38, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (39, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (40, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (41, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (42, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"} +]'), + (43, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (44, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (45, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Medium (regular home, some space available)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (46, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (47, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (48, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (49, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (50, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (51, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (52, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (53, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (54, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Warm colors (brown | gold tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (55, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (56, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (57, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (58, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (59, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (60, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (61, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (62, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (63, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (64, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (65, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (66, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (67, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (68, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"} +]'), + (69, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (70, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (71, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"} +]'), + (72, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Medium (regular home, some space available)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (73, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (74, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (75, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "K9-BD-01", "productName": "Bulldog"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FL-DSH-01", "productName": "Manx"} +]'), + (76, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (77, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "RP-LI-02", "productName": "Iguana"} +]'), + (78, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (79, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "K9-BD-01", "productName": "Bulldog"} +]'), + (80, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (81, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Dark colors (black | gray tones)', 'Large (yard and spacious indoor area possible)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FL-DSH-01", "productName": "Manx"} +]'), + (82, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (83, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (84, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"} +]'), + (85, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (86, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "RP-LI-02", "productName": "Iguana"} +]'), + (87, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FI-SW-01", "productName": "Angelfish"} +]'), + (88, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (89, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (90, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Small (suitable for studio/small apartment)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "RP-SN-01", "productName": "Rattlesnake"} +]'), + (91, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (92, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Day', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'), + (93, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Day', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-PO-02", "productName": "Poodle"}, + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (94, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Night', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (95, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Night', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FI-FW-01", "productName": "Koi"}, + {"productId": "FI-FW-02", "productName": "Goldfish"}, + {"productId": "RP-LI-02", "productName": "Iguana"} +]'), + (96, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Night', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-SW-01", "productName": "Angelfish"}, + {"productId": "RP-LI-02", "productName": "Iguana"} +]'), + (97, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Irregular', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-PD-02", "productName": "Poodle"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (98, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Irregular', 'Can provide diverse diet (vegetarian, mixed possible)', '[ + {"productId": "AV-CB-01", "productName": "Amazon Parrot"}, + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DLH-02", "productName": "Persian"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"}, + {"productId": "FI-FW-02", "productName": "Goldfish"} +]'), + (99, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Medium (regular home, some space available)', 'Irregular', 'Can manage special diets (insect-based, dried food, etc.)', '[ + {"productId": "AV-SB-02", "productName": "Finch"}, + {"productId": "FL-DSH-01", "productName": "Manx"}, + {"productId": "K9-CW-01", "productName": "Chihuahua"}, + {"productId": "FI-SW-01", "productName": "Angelfish"} +]'), + (100, 'Indoor-focused (apartment, studio, etc.)', '5 years or less', 'Mixed colors', 'Large (yard and spacious indoor area possible)', 'Day', 'Simple kibble-focused (carnivore, omnivore)', '[ + {"productId": "K9-RT-01", "productName": "Golden Retriever"}, + {"productId": "K9-RT-02", "productName": "Labrador Retriever"} +]'); diff --git a/src/main/resources/database/jpetstore-hsqldb-schema.sql b/src/main/resources/database/jpetstore-hsqldb-schema.sql index 4873fde96..9f896dd89 100644 --- a/src/main/resources/database/jpetstore-hsqldb-schema.sql +++ b/src/main/resources/database/jpetstore-hsqldb-schema.sql @@ -64,6 +64,17 @@ create table profile ( constraint pk_profile primary key (userid) ); +create table survey_recommendations ( + survey_recommendation_id int generated by default as identity (start with 1) primary key, + residence_env varchar(255) not null, + care_period varchar(255) not null, + pet_color_pref varchar(255) not null, + pet_size_pref varchar(255) not null, + activity_time varchar(255) not null, + diet_management varchar(255) not null, + recommended_json_data varchar(1024) not null +); + create table bannerdata ( favcategory varchar(80) not null, bannername varchar(255) null, diff --git a/src/main/resources/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.xml b/src/main/resources/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.xml new file mode 100644 index 000000000..dc48c15f6 --- /dev/null +++ b/src/main/resources/org/mybatis/jpetstore/mapper/SurveyRecommendationMapper.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO SURVEY_RECOMMENDATIONS ( + residence_env, + care_period, + pet_color_pref, + pet_size_pref, + activity_time, + diet_management, + recommended_json_data + ) VALUES ( + #{residenceEnv}, + #{carePeriod}, + #{petColorPref}, + #{petSizePref}, + #{activityTime}, + #{dietManagement}, + #{recommendedJsonData} + ) + + + + UPDATE SURVEY_RECOMMENDATIONS + SET + residence_env = #{residenceEnv}, + care_period = #{carePeriod}, + pet_color_pref = #{petColorPref}, + pet_size_pref = #{petSizePref}, + activity_time = #{activityTime}, + diet_management = #{dietManagement}, + recommended_json_data = #{recommendedJsonData} + WHERE survey_recommendation_id = #{surveyRecommendationId} + + + + DELETE FROM SURVEY_RECOMMENDATIONS + WHERE survey_recommendation_id = #{surveyRecommendationId} + + + diff --git a/src/main/webapp/WEB-INF/applicationContext.xml b/src/main/webapp/WEB-INF/applicationContext.xml index 53d76e86d..6150db4ff 100644 --- a/src/main/webapp/WEB-INF/applicationContext.xml +++ b/src/main/webapp/WEB-INF/applicationContext.xml @@ -1,7 +1,7 @@ diff --git a/src/main/webapp/WEB-INF/jsp/survey/SurveyRecommendationView.jsp b/src/main/webapp/WEB-INF/jsp/survey/SurveyRecommendationView.jsp new file mode 100644 index 000000000..45df18eae --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/survey/SurveyRecommendationView.jsp @@ -0,0 +1,60 @@ +<%-- + + Copyright 2010-2025 the original author or authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--%> +<%@ include file="../common/IncludeTop.jsp"%> + +
+ +

Survey Recommendations

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDResidence EnvCare PeriodPet Color PrefPet Size PrefActivity TimeDiet ManagementRecommended JSON Data
${rec.surveyRecommendationId}${rec.residenceEnv}${rec.carePeriod}${rec.petColorPref}${rec.petSizePref}${rec.activityTime}${rec.dietManagement}
${rec.recommendedJsonData}
No survey recommendations found.
+ +
+ +<%@ include file="../common/IncludeBottom.jsp"%> \ No newline at end of file diff --git a/src/test/java/org/mybatis/jpetstore/mapper/MapperTestContext.java b/src/test/java/org/mybatis/jpetstore/mapper/MapperTestContext.java index 9bb88b4ac..725d1db87 100644 --- a/src/test/java/org/mybatis/jpetstore/mapper/MapperTestContext.java +++ b/src/test/java/org/mybatis/jpetstore/mapper/MapperTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2022 the original author or authors. + * Copyright 2010-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,8 @@ public class MapperTestContext { DataSource dataSource() { return new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(EmbeddedDatabaseType.HSQL) .setScriptEncoding("UTF-8").ignoreFailedDrops(true).addScript("database/jpetstore-hsqldb-schema.sql") - .addScripts("database/jpetstore-hsqldb-dataload.sql").build(); + .addScripts("database/jpetstore-hsqldb-dataload.sql") + .addScripts("database/jpetstore-hsqldb-recommendations.sql").build(); } @Bean From 3c1f5da0288b822ab6bf461f7f627361686ded99 Mon Sep 17 00:00:00 2001 From: MadVedi Date: Thu, 20 Nov 2025 21:27:41 +0900 Subject: [PATCH 5/8] item_terminal_test --- src/main/java/org/mybatis/jpetstore/domain/Item.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/mybatis/jpetstore/domain/Item.java b/src/main/java/org/mybatis/jpetstore/domain/Item.java index 81366bf33..f52c04460 100644 --- a/src/main/java/org/mybatis/jpetstore/domain/Item.java +++ b/src/main/java/org/mybatis/jpetstore/domain/Item.java @@ -143,3 +143,5 @@ public String toString() { } } + +//test \ No newline at end of file From a6c3fb973c47426805dbec0f16be56d46a097694 Mon Sep 17 00:00:00 2001 From: MadVedi Date: Thu, 20 Nov 2025 21:31:30 +0900 Subject: [PATCH 6/8] terminaltest --- src/main/java/org/mybatis/jpetstore/service/CatalogService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/mybatis/jpetstore/service/CatalogService.java b/src/main/java/org/mybatis/jpetstore/service/CatalogService.java index decca2f86..c86eb77d2 100644 --- a/src/main/java/org/mybatis/jpetstore/service/CatalogService.java +++ b/src/main/java/org/mybatis/jpetstore/service/CatalogService.java @@ -89,4 +89,4 @@ public boolean isItemInStock(String itemId) { } } -//test \ No newline at end of file +//test11 \ No newline at end of file From b062f23726b158cf0bd24c2e8daf404646e3f3d8 Mon Sep 17 00:00:00 2001 From: MadVedi Date: Sat, 22 Nov 2025 19:41:58 +0900 Subject: [PATCH 7/8] =?UTF-8?q?complete=20=EB=A0=8C=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + .../org/mybatis/jpetstore/domain/Item.java | 4 +- .../jpetstore/service/CatalogService.java | 4 +- .../service/GptComparisonService.java | 147 +++++++ .../web/actions/ComparisonActionBean.java | 132 ++++++ .../database/jpetstore-hsqldb-data.sql | 7 + .../webapp/WEB-INF/jsp/catalog/Product.jsp | 15 +- .../webapp/WEB-INF/jsp/common/IncludeTop.jsp | 379 +++++++++++++++++- 8 files changed, 677 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/mybatis/jpetstore/service/GptComparisonService.java create mode 100644 src/main/java/org/mybatis/jpetstore/web/actions/ComparisonActionBean.java diff --git a/pom.xml b/pom.xml index 378b9871c..58be25935 100644 --- a/pom.xml +++ b/pom.xml @@ -237,6 +237,11 @@ ${spring.version} test
+ + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + diff --git a/src/main/java/org/mybatis/jpetstore/domain/Item.java b/src/main/java/org/mybatis/jpetstore/domain/Item.java index f52c04460..e7aaec865 100644 --- a/src/main/java/org/mybatis/jpetstore/domain/Item.java +++ b/src/main/java/org/mybatis/jpetstore/domain/Item.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2022 the original author or authors. + * Copyright 2010-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,4 +144,4 @@ public String toString() { } -//test \ No newline at end of file +// test \ No newline at end of file diff --git a/src/main/java/org/mybatis/jpetstore/service/CatalogService.java b/src/main/java/org/mybatis/jpetstore/service/CatalogService.java index c86eb77d2..5010ca8a1 100644 --- a/src/main/java/org/mybatis/jpetstore/service/CatalogService.java +++ b/src/main/java/org/mybatis/jpetstore/service/CatalogService.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2023 the original author or authors. + * Copyright 2010-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,4 +89,4 @@ public boolean isItemInStock(String itemId) { } } -//test11 \ No newline at end of file +// test11 \ No newline at end of file diff --git a/src/main/java/org/mybatis/jpetstore/service/GptComparisonService.java b/src/main/java/org/mybatis/jpetstore/service/GptComparisonService.java new file mode 100644 index 000000000..36396a165 --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/service/GptComparisonService.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.service.OpenAiService; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Collections; + +import org.mybatis.jpetstore.domain.Account; +import org.mybatis.jpetstore.domain.Item; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class GptComparisonService { + + private static final Logger logger = LoggerFactory.getLogger(GptComparisonService.class); + private static final String OPENAI_API_KEY; + private static final String OPENAI_MODEL = "gpt-3.5-turbo"; + + private final OpenAiService openAiService; + + static { + String apiKey = null; + try { + ObjectMapper mapper = new ObjectMapper(); + InputStream is = GptComparisonService.class.getClassLoader() + .getResourceAsStream("credentials/openai_credentials.json"); + if (is == null) { + throw new IOException("openai_credentials.json not found in classpath."); + } + JsonNode rootNode = mapper.readTree(is); + apiKey = rootNode.path("openai_api_key").asText(); + if (apiKey.isEmpty() || "YOUR_OPENAI_API_KEY_HERE".equals(apiKey)) { + throw new IllegalArgumentException("OPENAI_API_KEY not found or is a placeholder in openai_credentials.json."); + } + } catch (IOException e) { + logger.error("Error reading openai_credentials.json: {}", e.getMessage()); + throw new RuntimeException("Failed to load OpenAI API key from credentials file.", e); + } + OPENAI_API_KEY = apiKey; + } + + public GptComparisonService() { + this.openAiService = new OpenAiService(OPENAI_API_KEY, Duration.ofSeconds(30)); + } + + public String compareItems(Item item1, Item item2, Account account) { + try { + StringBuilder promptBuilder = new StringBuilder(); + + String productName1 = item1.getProduct().getName(); + String productName2 = item2.getProduct().getName(); + BigDecimal price1 = item1.getListPrice(); + BigDecimal price2 = item2.getListPrice(); + + String residenceEnv = account.getResidenceEnv(); + String petSizePref = account.getPetSizePref(); + String activityTime = account.getActivityTime(); + String attr1Item1 = item1.getAttribute1(); + String attr1Item2 = item2.getAttribute1(); + + promptBuilder.append("Please compare the following two pets based on the user's preferences.\n\n"); + + promptBuilder.append("User Preferences:\n"); + promptBuilder.append(" - Living Environment: ").append(residenceEnv != null ? residenceEnv : "Not specified") + .append("\n"); + promptBuilder.append(" - Pet Size Preference: ").append(petSizePref != null ? petSizePref : "Not specified") + .append("\n"); + promptBuilder.append(" - Activity Time: ").append(activityTime != null ? activityTime : "Not specified") + .append("\n\n"); + + promptBuilder.append("Product 1: ").append(productName1).append("\n"); + promptBuilder.append(" - Variant: ").append(attr1Item1).append("\n"); + promptBuilder.append(" - Price: $").append(price1).append("\n\n"); + + promptBuilder.append("Product 2: ").append(productName2).append("\n"); + promptBuilder.append(" - Variant: ").append(attr1Item2).append("\n"); + promptBuilder.append(" - Price: $").append(price2).append("\n\n"); + + promptBuilder.append("Based on the user's preferences, please compare these two pets:\n"); + promptBuilder.append("1. Price Comparison (compare the prices)\n"); + promptBuilder.append("2. Living Environment Suitability (how suitable for their living environment)\n"); + promptBuilder.append("3. Pet Size Suitability (how suitable for their size preference)\n"); + promptBuilder.append("4. Activity Time Suitability (how suitable for their activity schedule)\n"); + promptBuilder.append("5. When Product 1 is better (specific conditions)\n"); + promptBuilder.append("6. When Product 2 is better (specific conditions)\n"); + promptBuilder.append("7. Final recommendation for this specific user\n\n"); + + promptBuilder.append("Return your response ONLY in the following JSON format:\n"); + promptBuilder.append("{\n"); + promptBuilder.append(" \"price\": \"price comparison analysis based on the prices\",\n"); + promptBuilder + .append(" \"living_environment\": \"how suitable each pet is for the user's living environment\",\n"); + promptBuilder.append(" \"pet_size\": \"how suitable each pet is for the user's pet size preference\",\n"); + promptBuilder + .append(" \"activity_time\": \"how suitable each pet is for the user's activity time schedule\",\n"); + promptBuilder.append(" \"product1_better\": \"specific conditions where Product 1 is better for this user\",\n"); + promptBuilder.append(" \"product2_better\": \"specific conditions where Product 2 is better for this user\",\n"); + promptBuilder.append( + " \"recommendation\": \"final recommendation for which product is better for this specific user and why\"\n"); + promptBuilder.append("}\n"); + promptBuilder.append("Do NOT include any text outside of the JSON format."); + + String fullPrompt = promptBuilder.toString(); + ChatMessage userMessage = new ChatMessage("user", fullPrompt); + + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder().model(OPENAI_MODEL) + .messages(Collections.singletonList(userMessage)).maxTokens(1500).temperature(0.7).build(); + + logger.info("Sending comparison prompt to OpenAI API."); + logger.info("Full Prompt:\n{}", fullPrompt); + + String response = openAiService.createChatCompletion(chatCompletionRequest).getChoices().get(0).getMessage() + .getContent(); + + logger.info("OpenAI API Response: {}", response); + return response; + + } catch (Exception e) { + logger.error("Error calling OpenAI API for comparison: {}", e.getMessage(), e); + return "Error getting comparison from OpenAI: " + e.getMessage(); + } + } +} diff --git a/src/main/java/org/mybatis/jpetstore/web/actions/ComparisonActionBean.java b/src/main/java/org/mybatis/jpetstore/web/actions/ComparisonActionBean.java new file mode 100644 index 000000000..902839733 --- /dev/null +++ b/src/main/java/org/mybatis/jpetstore/web/actions/ComparisonActionBean.java @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.jpetstore.web.actions; + +import com.fasterxml.jackson.databind.ObjectMapper; // ← 추가 + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpSession; + +import net.sourceforge.stripes.action.ActionBean; +import net.sourceforge.stripes.action.ActionBeanContext; +import net.sourceforge.stripes.action.DefaultHandler; +import net.sourceforge.stripes.action.DontValidate; +import net.sourceforge.stripes.action.Resolution; +import net.sourceforge.stripes.action.StreamingResolution; +import net.sourceforge.stripes.integration.spring.SpringBean; + +import org.mybatis.jpetstore.domain.Account; +import org.mybatis.jpetstore.domain.Item; +import org.mybatis.jpetstore.service.CatalogService; +import org.mybatis.jpetstore.service.GptComparisonService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ComparisonActionBean implements ActionBean { + + private static final long serialVersionUID = 1L; + private static final Logger logger = LoggerFactory.getLogger(ComparisonActionBean.class); + + private ActionBeanContext context; + private static final ObjectMapper objectMapper = new ObjectMapper(); // ← 추가 + + @SpringBean + private CatalogService catalogService; + + @SpringBean + private GptComparisonService gptComparisonService; + + @Override + public void setContext(ActionBeanContext context) { + this.context = context; + } + + @Override + public ActionBeanContext getContext() { + return context; + } + + @DefaultHandler + @DontValidate + public Resolution compareItems() { + try { + String itemId1Str = context.getRequest().getParameter("itemId1"); + String itemId2Str = context.getRequest().getParameter("itemId2"); + + logger.info("Comparing items: {} vs {}", itemId1Str, itemId2Str); + + Item item1 = catalogService.getItem(itemId1Str); + Item item2 = catalogService.getItem(itemId2Str); + + HttpSession session = context.getRequest().getSession(); + AccountActionBean accountBean = (AccountActionBean) session.getAttribute("accountBean"); + Account account = new Account(); + + if (accountBean != null) { + account = accountBean.getAccount(); + } + + // GPT 분석 결과 받기 + String gptAnalysisStr = gptComparisonService.compareItems(item1, item2, account); + + logger.info("GPT Analysis String: {}", gptAnalysisStr); + + // ✅ Map으로 응답 데이터 구조화 + Map response = new HashMap<>(); + response.put("item1_id", item1.getItemId()); + response.put("item2_id", item2.getItemId()); + response.put("item1_name", item1.getItemId()); + response.put("item2_name", item2.getItemId()); + response.put("item1_price", "$" + item1.getListPrice()); + response.put("item2_price", "$" + item2.getListPrice()); + response.put("user_living_environment", + account.getResidenceEnv() != null ? account.getResidenceEnv() : "Not specified"); + response.put("user_pet_size", account.getPetSizePref() != null ? account.getPetSizePref() : "Not specified"); + response.put("user_activity_time", + account.getActivityTime() != null ? account.getActivityTime() : "Not specified"); + + // ✅ gpt_analysis를 Object로 파싱해서 넣기 + try { + Object gptAnalysisObj = objectMapper.readValue(gptAnalysisStr, Object.class); + response.put("gpt_analysis", gptAnalysisObj); + logger.info("GPT Analysis parsed successfully"); + } catch (Exception e) { + logger.error("Failed to parse GPT analysis JSON: {}", e.getMessage(), e); + response.put("gpt_analysis", new HashMap<>()); // 빈 객체 + } + + // ✅ Jackson으로 안전하게 JSON 변환 + String fullResponse = objectMapper.writeValueAsString(response); + + logger.info("Final Response JSON: {}", fullResponse); + + return new StreamingResolution("application/json", fullResponse); + + } catch (Exception e) { + logger.error("Error in compareItems: {}", e.getMessage(), e); + try { + Map errorMap = new HashMap<>(); + errorMap.put("error", e.getMessage()); + return new StreamingResolution("application/json", objectMapper.writeValueAsString(errorMap)); + } catch (Exception jsonError) { + // JSON 변환도 실패하면 수동으로 + return new StreamingResolution("application/json", "{\"error\": \"Internal server error\"}"); + } + } + } +} diff --git a/src/main/resources/database/jpetstore-hsqldb-data.sql b/src/main/resources/database/jpetstore-hsqldb-data.sql index 36db32501..268d402c9 100644 --- a/src/main/resources/database/jpetstore-hsqldb-data.sql +++ b/src/main/resources/database/jpetstore-hsqldb-data.sql @@ -55,6 +55,13 @@ create table PROFILE ( favcategory varchar(30), mylistopt int, banneropt int, + + residence_env varchar(100), + care_period varchar(100), + pet_color_pref varchar(100), + pet_size_pref varchar(100), + activity_time varchar(100), + diet_management varchar(100), constraint pk_profile primary key (userid) ); diff --git a/src/main/webapp/WEB-INF/jsp/catalog/Product.jsp b/src/main/webapp/WEB-INF/jsp/catalog/Product.jsp index ad9c11a6f..76d328fa4 100644 --- a/src/main/webapp/WEB-INF/jsp/catalog/Product.jsp +++ b/src/main/webapp/WEB-INF/jsp/catalog/Product.jsp @@ -1,6 +1,6 @@ <%-- - Copyright 2010-2022 the original author or authors. + Copyright 2010-2025 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +17,12 @@ --%> <%@ include file="../common/IncludeTop.jsp"%> + + + + @@ -32,7 +38,7 @@

${actionBean.product.name}

- +
@@ -61,10 +67,7 @@ - - - +
Item ID Product ID
-
diff --git a/src/main/webapp/WEB-INF/jsp/common/IncludeTop.jsp b/src/main/webapp/WEB-INF/jsp/common/IncludeTop.jsp index 78828462e..ab98c05cb 100644 --- a/src/main/webapp/WEB-INF/jsp/common/IncludeTop.jsp +++ b/src/main/webapp/WEB-INF/jsp/common/IncludeTop.jsp @@ -1,6 +1,6 @@ -<%-- + <%-- - Copyright 2010-2023 the original author or authors. + Copyright 2010-2025 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,10 +21,8 @@ <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - + + + + + + + + + + + + + + + +