From d322d9d5ff2f74f9dfbd7146c1cb9b1f20b2f167 Mon Sep 17 00:00:00 2001 From: Park Sejin Date: Tue, 13 Aug 2024 23:58:01 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]:=20Material=20Gpt=20Api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{GptService.java => GptManager.java} | 140 +++++++++++------- .../building/application/BuildingService.java | 2 +- .../controller/BuildingController.java | 7 +- .../building/dto/BuildingDetailResponse.java | 2 +- .../domain/building/dto/MaterialResult.java | 7 + .../domain/building/entity/Building.java | 14 +- .../domain/material/MaterialController.java | 13 ++ .../domain/material/MaterialRepository.java | 3 + .../domain/material/MaterialService.java | 46 +++++- 9 files changed, 167 insertions(+), 67 deletions(-) rename src/main/java/org/khtml/hexagonal/domain/ai/application/{GptService.java => GptManager.java} (80%) create mode 100644 src/main/java/org/khtml/hexagonal/domain/building/dto/MaterialResult.java diff --git a/src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java b/src/main/java/org/khtml/hexagonal/domain/ai/application/GptManager.java similarity index 80% rename from src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java rename to src/main/java/org/khtml/hexagonal/domain/ai/application/GptManager.java index f572571..1a5fc55 100644 --- a/src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java +++ b/src/main/java/org/khtml/hexagonal/domain/ai/application/GptManager.java @@ -7,8 +7,10 @@ import org.khtml.hexagonal.domain.building.dto.BuildingUpdate; import org.khtml.hexagonal.domain.building.dto.ImageRequest; import org.khtml.hexagonal.domain.building.dto.MaterialInfo; +import org.khtml.hexagonal.domain.building.dto.MaterialResult; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; @@ -18,10 +20,9 @@ import java.util.List; import java.util.Map; -@Transactional(readOnly = true) @RequiredArgsConstructor -@Service -public class GptService { +@Component +public class GptManager { @Value("${openai.api.url}") private String openAiApiUrl; @@ -30,41 +31,52 @@ public class GptService { private String openAiApiKey; public BuildingUpdate analyzeBuilding(List urls) throws JsonProcessingException { - BuildingUpdate buildingUpdate; - ImageRequest imageRequest = new ImageRequest(); - List contentList = new ArrayList<>(); + ImageRequest imageRequest = getImageRequest(urls); + return analyzeHouseImages(imageRequest); + } - imageRequest.setRole("user"); + public MaterialResult analyzeMaterial(List urls) throws JsonProcessingException { + ImageRequest imageRequest = getImageRequest(urls); + Map result = analyzeMaterialImages(imageRequest); - for (String url : urls) { - ImageRequest.Content content = new ImageRequest.Content(); - ImageRequest.ImageUrl imageUrl = new ImageRequest.ImageUrl(); + String material = null; + String usage = null; - imageUrl.setUrl(url); - content.setType("image_url"); - content.setImage_url(imageUrl); - contentList.add(content); + List> choices = (List>) result.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map firstChoice = choices.get(0); + Map message = (Map) firstChoice.get("message"); + if (message != null) { + String content = (String) message.get("content"); + + // content 값을 JSON으로 파싱 + ObjectMapper objectMapper = new ObjectMapper(); + Map jsonData = objectMapper.readValue(content, new TypeReference>() { + }); + // 필요한 데이터 추출 + material = (String) jsonData.get("material"); + usage = (String) jsonData.get("usage"); + } } - imageRequest.setContent(contentList); - return analyzeHouseImages(imageRequest); + return new MaterialResult(material, usage); } public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonProcessingException { // 시스템 메시지 String systemMessage = """ - + 너는 오래된 건물의 상태를 평가하는 챗봇 AI다. 제공된 사진을 분석하여 건물의 상태를 진단하고, 결과를 한글로 작성된 JSON 형식으로 출력해야 한다. 이 JSON은 아래 예시와 같이 각 필드가 특정 영어 변수에 매핑되도록 구성되어야 한다. - + 건물 구조 분석 (structureReason): - + 구조: 건물의 구조가 전통적인지 현대적인지 판단한다. 이유: 판단의 이유를 설명한다. 건축 요소 평가 (roof, walls, windowsAndDoors): - + 지붕 형태 (roof): 재료: 지붕에 사용된 재료를 확인한다. 상태: 지붕의 상태를 평가한다. @@ -75,27 +87,27 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP 재료: 창문과 문에 사용된 재료를 확인한다. 상태: 창문과 문의 상태를 평가한다. 건물 상태 평가 (overallCondition): - + 평가: 건물의 전반적인 상태를 구체적으로 평가한다. 이유: 해당 평가의 이유를 제시한다. 상세 점수화 (detailedScores): - + 균열 여부 (cracks): 균열의 존재 여부와 심각성을 100점 만점으로 점수화한다. 누수 여부 (leaks): 누수의 존재 여부와 심각성을 100점 만점으로 점수화한다. 부식 여부 (corrosion): 부식의 정도를 100점 만점으로 점수화한다. 노후화 정도 (aging): 건물의 노후화를 100점 만점으로 점수화한다. 총점수 (totalScore): 위의 점수를 합산하여 반올림한 총점을 계산한다. 보수 필요성 판단 (repairNeeds): - + 조명: 사진 속 조명이 형광등이라면 LED로 교체가 필요한지, 이미 LED라면 교체가 불필요한지 판단한다. 창호 보강, 도배, 장판 교체: 창호 보강, 도배, 장판 교체의 필요 여부를 판단한다. - + 판단하기 어려운 부분, 예를 들어 실내 사진인데 지붕 재료, 지붕 상태 등을 판단해야 하는 경우에는 해당 부분을 ""으로 처리한다. repairlist의 경우 반드시 ,로 구분한다. repairlist가 없는 경우에는 ""으로 처리한다. - - + + 예시 JSON은 다음과 같다. - + { "structureReason": "구조 평가 이유", "roofMaterial": "지붕 재료", @@ -113,10 +125,10 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP "totalScore": 20, "repairList": "LED 교체, 창호 보강, 도배, 장판 교체" } - - - - + + + + """; // OpenAI API에 보낼 요청 데이터 작성 @@ -182,7 +194,8 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP // JSON 데이터를 Map으로 파싱 ObjectMapper objectMapper = new ObjectMapper(); - Map jsonData = objectMapper.readValue(jsonContent, new TypeReference>() {}); + Map jsonData = objectMapper.readValue(jsonContent, new TypeReference>() { + }); // BuildingUpdate DTO로 데이터 매핑 BuildingUpdate buildingUpdate = new BuildingUpdate(); @@ -211,27 +224,27 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP return buildingUpdate; } - public ResponseEntity> analyzeMaterialImages(ImageRequest imageRequest) throws JsonProcessingException { + public Map analyzeMaterialImages(ImageRequest imageRequest) throws JsonProcessingException { // 시스템 메시지 String systemMessage = """ - 너는 사진을 받으면 해당 사진에 나오는 부자재의 종류들과 그 사용법들을 반환하는 챗봇 AI다. - - 제공된 사진을 분석하여 부자재의 종류와 사용법을 양식에 맞게 작성된 JSON 형식으로 출력해야 한다. - - 반드시 양식에 맞게 작성해야 하며, 다른 방식으로의 응답은 절대 허용하지 않는다. - - mateial과 usage는 여러개가 올 수 있지만 리스트 형식으로는 '절대' 출력하면 안된다. - - mateiral과 usage가 여러 가지 이상으로 판단될 시, 반드시 ,로만 구분하여 한 개의 string으로 출력해야 한다. - - 예시 JSON은 다음과 같다. - - { - "material" : "나무 판자, 쇠 파이프" - "usage" : "벽 만들기, 천장 고치기" - } - """; + 너는 사진을 받으면 해당 사진에 나오는 부자재의 종류들과 그 사용법들을 반환하는 챗봇 AI다. + + 제공된 사진을 분석하여 부자재의 종류와 사용법을 양식에 맞게 작성된 JSON 형식으로 출력해야 한다. + + 반드시 양식에 맞게 작성해야 하며, 다른 방식으로의 응답은 절대 허용하지 않는다. + + mateial과 usage는 여러개가 올 수 있지만 리스트 형식으로는 '절대' 출력하면 안된다. + + mateiral과 usage가 여러 가지 이상으로 판단될 시, 반드시 ,로만 구분하여 한 개의 string으로 출력해야 한다. + + 예시 JSON은 다음과 같다. + + { + "material" : "나무 판자, 쇠 파이프", + "usage" : "벽 만들기, 천장 고치기" + } + """; // OpenAI API에 보낼 요청 데이터 작성 Map requestBody = new HashMap<>(); @@ -292,17 +305,38 @@ public ResponseEntity> analyzeMaterialImages(ImageRequest im String content = (String) ((Map) ((Map) ((List) responseBody.get("choices")).get(0)).get("message")).get("content"); // "```json"과 "```"을 제거하여 실제 JSON 데이터만 추출 - String jsonContent = content.replaceAll("```json\\n|```", "").trim(); + String jsonContent = content.replaceAll("(?s)```json\\s*|```", "").trim(); // JSON 데이터를 Map으로 파싱 ObjectMapper objectMapper = new ObjectMapper(); - Map jsonData = objectMapper.readValue(jsonContent, new TypeReference>() {}); + Map jsonData = objectMapper.readValue(jsonContent, new TypeReference>() { + }); // 필요한 데이터 사용 MaterialInfo materialInfo = new MaterialInfo((String) jsonData.get("material"), (String) jsonData.get("usage")); System.out.println(materialInfo); // 리턴값 반환 - return ResponseEntity.ok(responseBody); + return responseBody; + } + + private static ImageRequest getImageRequest(List urls) { + ImageRequest imageRequest = new ImageRequest(); + List contentList = new ArrayList<>(); + + imageRequest.setRole("user"); + + for (String url : urls) { + ImageRequest.Content content = new ImageRequest.Content(); + ImageRequest.ImageUrl imageUrl = new ImageRequest.ImageUrl(); + + imageUrl.setUrl(url); + content.setType("image_url"); + content.setImage_url(imageUrl); + contentList.add(content); + } + + imageRequest.setContent(contentList); + return imageRequest; } } diff --git a/src/main/java/org/khtml/hexagonal/domain/building/application/BuildingService.java b/src/main/java/org/khtml/hexagonal/domain/building/application/BuildingService.java index aecc111..26cbec6 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/application/BuildingService.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/application/BuildingService.java @@ -118,7 +118,7 @@ public void updateBuildingDescription(String buildingId, Long userId, String des throw new IllegalArgumentException(ErrorType.DEFAULT_ERROR.getMessage()); } - building.setDescription(description); + building.setBuildingDescription(description); } } diff --git a/src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java b/src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java index 3d374d1..aa1795c 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java @@ -1,7 +1,7 @@ package org.khtml.hexagonal.domain.building.controller; import lombok.RequiredArgsConstructor; -import org.khtml.hexagonal.domain.ai.application.GptService; +import org.khtml.hexagonal.domain.ai.application.GptManager; import org.khtml.hexagonal.domain.building.dto.*; import org.khtml.hexagonal.domain.auth.JwtValidator; import org.khtml.hexagonal.domain.building.application.BuildingService; @@ -20,7 +20,7 @@ public class BuildingController { private final BuildingService buildingService; private final JwtValidator jwtValidator; - private final GptService gptService; + private final GptManager gptManager; @GetMapping("/{building-id}") public ApiResponse getBuildingDetail( @@ -38,7 +38,7 @@ public ApiResponse registerBuilding( User requestUser = jwtValidator.getUserFromToken(token); List urls = buildingService.registerBuilding(buildingId, requestUser, multipartFiles); - BuildingUpdate buildingUpdate = gptService.analyzeBuilding(urls); + BuildingUpdate buildingUpdate = gptManager.analyzeBuilding(urls); buildingService.updateAnalyzedBuilding(buildingId, buildingUpdate); return ApiResponse.success(buildingUpdate); @@ -56,5 +56,4 @@ public ApiResponse registerBuilding( return ApiResponse.success(); } - } diff --git a/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java b/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java index 02d8d82..6a160b7 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java @@ -22,7 +22,7 @@ public static BuildingDetailResponse toResponse(Building building) { return new BuildingDetailResponse( building.getGisBuildingId(), building.getLegalDistrictName() + " " + building.getLandLotNumber(), - building.getDescription(), + building.getBuildingDescription(), building.getUser() == null ? null : building.getUser().getPhoneNumber(), building.getCrackScore(), building.getLeakScore(), diff --git a/src/main/java/org/khtml/hexagonal/domain/building/dto/MaterialResult.java b/src/main/java/org/khtml/hexagonal/domain/building/dto/MaterialResult.java new file mode 100644 index 0000000..284ea22 --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/building/dto/MaterialResult.java @@ -0,0 +1,7 @@ +package org.khtml.hexagonal.domain.building.dto; + +public record MaterialResult( + String material, + String usage +) { +} diff --git a/src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java b/src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java index 85aac91..22578d7 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java @@ -103,8 +103,14 @@ public class Building { @Column(name = "repair_list") private String repairList; - @Column(name = "description") - private String description; + @Column(name = "building_description") + private String buildingDescription; + + @Column(name = "material_description") + private String materialDescription; + + @Column(name = "material_usage") + private String materialUsage; @Column(name = "is_analyzed") private Boolean isAnalyzed = false; @@ -117,6 +123,10 @@ public void updateUser(User user) { this.user = user; } + public void updateMaterialUsage(String materialUsage) { + this.materialUsage = materialUsage; + } + public void updateAnalyzedData(BuildingUpdate buildingUpdate) { this.structureReason = buildingUpdate.getStructureReason(); this.roofMaterial = buildingUpdate.getRoofMaterial(); diff --git a/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java b/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java index efc53b0..a64b3e5 100644 --- a/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.khtml.hexagonal.domain.auth.JwtValidator; +import org.khtml.hexagonal.domain.building.dto.BuildingDescriptionRequest; import org.khtml.hexagonal.domain.user.User; import org.khtml.hexagonal.global.support.response.ApiResponse; import org.springframework.web.bind.annotation.*; @@ -30,4 +31,16 @@ public ApiResponse registerBuilding( return ApiResponse.success(); } + @PostMapping("/{building-id}/register/description") + public ApiResponse registerBuilding( + @RequestHeader("Authorization") String token, + @PathVariable(name = "building-id") String buildingId, + @RequestBody BuildingDescriptionRequest buildingDescriptionRequest + ) throws IOException { + User requestUser = jwtValidator.getUserFromToken(token); + materialService.updateMaterialDescription(buildingId, requestUser.getId(), buildingDescriptionRequest.description()); + + return ApiResponse.success(); + } + } diff --git a/src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java b/src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java index 8a009ad..ec2ff0a 100644 --- a/src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java @@ -3,4 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface MaterialRepository extends JpaRepository { + + Boolean existsByName(String name); + } diff --git a/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java b/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java index 1497d1e..8bdc255 100644 --- a/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java @@ -1,8 +1,10 @@ package org.khtml.hexagonal.domain.material; import lombok.RequiredArgsConstructor; +import org.khtml.hexagonal.domain.ai.application.GptManager; import org.khtml.hexagonal.domain.building.*; import org.khtml.hexagonal.domain.building.application.BlobManager; +import org.khtml.hexagonal.domain.building.dto.MaterialResult; import org.khtml.hexagonal.domain.building.entity.Building; import org.khtml.hexagonal.domain.building.entity.BuildingImage; import org.khtml.hexagonal.domain.building.entity.Image; @@ -11,12 +13,15 @@ import org.khtml.hexagonal.domain.building.repository.ImageRepository; import org.khtml.hexagonal.domain.user.User; import org.khtml.hexagonal.domain.user.UserRepository; +import org.khtml.hexagonal.global.support.error.ErrorType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; @RequiredArgsConstructor @Transactional(readOnly = true) @@ -28,24 +33,53 @@ public class MaterialService { private final ImageRepository imageRepository; private final UserRepository userRepository; private final MaterialRepository materialRepository; + private final GptManager gptManager; private final BlobManager blobManager; @Transactional public void registerMaterials(String buildingId, User requestUser, List multipartFiles) throws IOException { - for(MultipartFile file : multipartFiles) { + List urls = new ArrayList<>(); + Building building = buildingRepository.findBuildingByGisBuildingId(buildingId) + .orElseThrow(() -> new IllegalArgumentException("Building not found")); + User user = userRepository.findById(requestUser.getId()) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + for (MultipartFile file : multipartFiles) { String url = blobManager.storeFile(file.getOriginalFilename(), file.getInputStream(), file.getSize()); - Building building = buildingRepository.findBuildingByGisBuildingId(buildingId) - .orElseThrow(() -> new IllegalArgumentException("Building not found")); - User user = userRepository.findById(requestUser.getId()) - .orElseThrow(() -> new IllegalArgumentException("User not found")); Image image = Image.builder().url(url).imageType(ImageType.MATERIAL).user(user).build(); BuildingImage buildingImage = BuildingImage.builder().image(image).building(building).build(); - // **Material GPT 로직 수행 후 저장 로직 필요 **// + urls.add(url); + imageRepository.save(image); buildingImageRepository.save(buildingImage); } + + MaterialResult materialResult = gptManager.analyzeMaterial(urls); + String[] materials = materialResult.material().split(","); + + for (String material : materials) { + if (materialRepository.existsByName(material)) { + continue; + } + Material materialEntity = Material.builder().name(material.trim()).build(); + materialRepository.save(materialEntity); + } + + building.updateMaterialUsage(materialResult.usage()); + } + + @Transactional + public void updateMaterialDescription(String buildingId, Long userId, String description) { + Building building = buildingRepository.findBuildingByGisBuildingId(buildingId) + .orElseThrow(() -> new IllegalArgumentException("Building not found")); + + if(!Objects.equals(building.getUser().getId(), userId)) { + throw new IllegalArgumentException(ErrorType.DEFAULT_ERROR.getMessage()); + } + + building.setMaterialDescription(description); } }