From 033d02e7687d248e5dd934941cc5bfe4b84cc45a Mon Sep 17 00:00:00 2001 From: Sejin Park <95167215+sejineer@users.noreply.github.com> Date: Tue, 13 Aug 2024 20:18:18 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]:=20material=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hexagonal/domain/building/Image.java | 6 ++- .../hexagonal/domain/building/ImageType.java | 5 +++ .../hexagonal/domain/material/Material.java | 30 +++++++++++++ .../domain/material/MaterialController.java | 33 ++++++++++++++ .../domain/material/MaterialRepository.java | 6 +++ .../domain/material/MaterialService.java | 44 +++++++++++++++++++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/khtml/hexagonal/domain/building/ImageType.java create mode 100644 src/main/java/org/khtml/hexagonal/domain/material/Material.java create mode 100644 src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java create mode 100644 src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java create mode 100644 src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java diff --git a/src/main/java/org/khtml/hexagonal/domain/building/Image.java b/src/main/java/org/khtml/hexagonal/domain/building/Image.java index 80209c0..cc84f0a 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/Image.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/Image.java @@ -16,13 +16,17 @@ public class Image extends BaseEntity { private String url; + @Enumerated(EnumType.STRING) + private ImageType imageType; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; @Builder - public Image(String url, User user) { + public Image(String url, ImageType imageType, User user) { this.url = url; + this.imageType = imageType; this.user = user; } diff --git a/src/main/java/org/khtml/hexagonal/domain/building/ImageType.java b/src/main/java/org/khtml/hexagonal/domain/building/ImageType.java new file mode 100644 index 0000000..e0a2f60 --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/building/ImageType.java @@ -0,0 +1,5 @@ +package org.khtml.hexagonal.domain.building; + +public enum ImageType { + BUILDING, MATERIAL +} diff --git a/src/main/java/org/khtml/hexagonal/domain/material/Material.java b/src/main/java/org/khtml/hexagonal/domain/material/Material.java new file mode 100644 index 0000000..d6ab4ba --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/material/Material.java @@ -0,0 +1,30 @@ +package org.khtml.hexagonal.domain.material; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.khtml.hexagonal.domain.building.Building; +import org.khtml.hexagonal.domain.common.BaseEntity; + +@Table(name = "material") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Material extends BaseEntity { + + @Column(name = "name") + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "building_id") + private Building building; + + @Builder + public Material(String name, Building building) { + this.name = name; + this.building = building; + } + +} diff --git a/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java b/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java new file mode 100644 index 0000000..efc53b0 --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java @@ -0,0 +1,33 @@ +package org.khtml.hexagonal.domain.material; + +import lombok.RequiredArgsConstructor; +import org.khtml.hexagonal.domain.auth.JwtValidator; +import org.khtml.hexagonal.domain.user.User; +import org.khtml.hexagonal.global.support.response.ApiResponse; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + + +@RequestMapping("/api/v1/materials") +@RequiredArgsConstructor +@RestController +public class MaterialController { + + private final MaterialService materialService; + private final JwtValidator jwtValidator; + + @PostMapping("/{building-id}/register") + public ApiResponse registerBuilding( + @RequestHeader("Authorization") String token, + @RequestParam("images") List multipartFiles, + @PathVariable(name = "building-id") String buildingId + ) throws IOException { + User requestUser = jwtValidator.getUserFromToken(token); + materialService.registerMaterials(buildingId, requestUser, multipartFiles); + 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 new file mode 100644 index 0000000..8a009ad --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java @@ -0,0 +1,6 @@ +package org.khtml.hexagonal.domain.material; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MaterialRepository extends JpaRepository { +} diff --git a/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java b/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java new file mode 100644 index 0000000..70bd971 --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java @@ -0,0 +1,44 @@ +package org.khtml.hexagonal.domain.material; + +import lombok.RequiredArgsConstructor; +import org.khtml.hexagonal.domain.building.*; +import org.khtml.hexagonal.domain.user.User; +import org.khtml.hexagonal.domain.user.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class MaterialService { + + private final BuildingRepository buildingRepository; + private final BuildingImageRepository buildingImageRepository; + private final ImageRepository imageRepository; + private final UserRepository userRepository; + private final MaterialRepository materialRepository; + private final BlobManager blobManager; + + @Transactional + public void registerMaterials(String buildingId, User requestUser, List multipartFiles) throws IOException { + 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 로직 수행 후 저장 로직 필요 **// + imageRepository.save(image); + buildingImageRepository.save(buildingImage); + } + } + +} From c33545b21ca58f9920288d0bd4dd775ba85c0037 Mon Sep 17 00:00:00 2001 From: ZhongdanBae <128568951+ZhongdanBae@users.noreply.github.com> Date: Tue, 13 Aug 2024 20:41:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[FEAT]=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ai/presentation/GptController.java | 283 ----------------- .../domain/building/BuildingController.java | 300 +++++++++++++++++- .../building/application/BuildingService.java | 11 +- .../{ai => building}/dto/BuildingUpdate.java | 2 +- .../{ai => building}/dto/ImageRequest.java | 2 +- .../{ai => building}/dto/MaterialInfo.java | 2 +- 6 files changed, 309 insertions(+), 291 deletions(-) rename src/main/java/org/khtml/hexagonal/domain/{ai => building}/dto/BuildingUpdate.java (98%) rename src/main/java/org/khtml/hexagonal/domain/{ai => building}/dto/ImageRequest.java (91%) rename src/main/java/org/khtml/hexagonal/domain/{ai => building}/dto/MaterialInfo.java (93%) diff --git a/src/main/java/org/khtml/hexagonal/domain/ai/presentation/GptController.java b/src/main/java/org/khtml/hexagonal/domain/ai/presentation/GptController.java index c32d62d..f7392f4 100644 --- a/src/main/java/org/khtml/hexagonal/domain/ai/presentation/GptController.java +++ b/src/main/java/org/khtml/hexagonal/domain/ai/presentation/GptController.java @@ -1,293 +1,10 @@ package org.khtml.hexagonal.domain.ai.presentation; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.khtml.hexagonal.domain.ai.dto.BuildingUpdate; -import org.khtml.hexagonal.domain.ai.dto.ImageRequest; -import org.khtml.hexagonal.domain.ai.dto.MaterialInfo; -import org.khtml.hexagonal.domain.building.application.BuildingService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; @RestController @RequestMapping("/api") public class GptController { - @Autowired - private BuildingService buildingService; - - @Value("${openai.api.url}") - private String openAiApiUrl; - - @Value("${openai.api.key}") - private String openAiApiKey; - - @PostMapping("/analyze-images/house") - public ResponseEntity> analyzeHouseImages(@RequestBody ImageRequest imageRequest) throws JsonProcessingException { - - // 시스템 메시지 - String systemMessage = """ - - 너는 오래된 건물의 상태를 평가하는 챗봇 AI다. 제공된 사진을 분석하여 건물의 상태를 진단하고, 결과를 한글로 작성된 JSON 형식으로 출력해야 한다. - 이 JSON은 아래 예시와 같이 각 필드가 특정 영어 변수에 매핑되도록 구성되어야 한다. - - 건물 구조 분석 (structureReason): - - 구조: 건물의 구조가 전통적인지 현대적인지 판단한다. - 이유: 판단의 이유를 설명한다. - 건축 요소 평가 (roof, walls, windowsAndDoors): - - 지붕 형태 (roof): - 재료: 지붕에 사용된 재료를 확인한다. - 상태: 지붕의 상태를 평가한다. - 외벽 재질 (walls): - 재료: 외벽에 사용된 재료를 확인한다. - 상태: 외벽의 상태를 평가한다. - 창문 및 문 형태 (windowsAndDoors): - 재료: 창문과 문에 사용된 재료를 확인한다. - 상태: 창문과 문의 상태를 평가한다. - 건물 상태 평가 (overallCondition): - - 평가: 건물의 전반적인 상태를 구체적으로 평가한다. - 이유: 해당 평가의 이유를 제시한다. - 상세 점수화 (detailedScores): - - 균열 여부 (cracks): 균열의 존재 여부와 심각성을 100점 만점으로 점수화한다. - 누수 여부 (leaks): 누수의 존재 여부와 심각성을 100점 만점으로 점수화한다. - 부식 여부 (corrosion): 부식의 정도를 100점 만점으로 점수화한다. - 노후화 정도 (aging): 건물의 노후화를 100점 만점으로 점수화한다. - 총점수 (totalScore): 위의 점수를 합산하여 반올림한 총점을 계산한다. - 보수 필요성 판단 (repairNeeds): - - 조명: 사진 속 조명이 형광등이라면 LED로 교체가 필요한지, 이미 LED라면 교체가 불필요한지 판단한다. - 창호 보강, 도배, 장판 교체: 창호 보강, 도배, 장판 교체의 필요 여부를 판단한다. - - 판단하기 어려운 부분, 예를 들어 실내 사진인데 지붕 재료, 지붕 상태 등을 판단해야 하는 경우에는 해당 부분을 ""으로 처리한다. - repairlist의 경우 반드시 ,로 구분한다. repairlist가 없는 경우에는 ""으로 처리한다. - - - 예시 JSON은 다음과 같다. - - { - "structureReason": "구조 평가 이유", - "roofMaterial": "지붕 재료", - "roofCondition": "지붕 상태", - "wallMaterial": "외벽 재료", - "wallCondition": "외벽 상태", - "windowDoorMaterial": "창문 및 문 재료", - "windowDoorCondition": "창문 및 문 상태", - "overallCondition": "건물 상태 평가", - "conditionReason": "건물 상태 평가 이유", - "crackScore": 20, - "leakScore": 0, - "corrosionScore": 0, - "agingScore": 0, - "totalScore": 20, - "repairList": "LED 교체, 창호 보강, 도배, 장판 교체" - } - - - - - """; - - // OpenAI API에 보낼 요청 데이터 작성 - Map requestBody = new HashMap<>(); - List> messages = new ArrayList<>(); - - // System 메시지 추가 - Map systemContent = new HashMap<>(); - systemContent.put("type", "text"); - systemContent.put("text", systemMessage); - messages.add(Map.of("role", "system", "content", List.of(systemContent))); - - // User 메시지 추가 (여러 이미지 URL 포함) - List> imageUrls = new ArrayList<>(); - imageRequest.getContent().forEach(content -> { - String url = content.getImageUrlOrNull(); - if (url != null) { - Map imageUrlContent = new HashMap<>(); - imageUrlContent.put("type", "image_url"); - imageUrlContent.put("image_url", Map.of("url", url)); - imageUrls.add(imageUrlContent); - } - }); - messages.add(Map.of("role", "user", "content", imageUrls)); - - // Assistant 메시지 추가 (빈 값으로 초기화) - Map assistantContent = new HashMap<>(); - assistantContent.put("type", "text"); - assistantContent.put("text", ""); - messages.add(Map.of("role", "assistant", "content", List.of(assistantContent))); - - // 요청 본문에 messages 리스트와 옵션 추가 - requestBody.put("messages", messages); - requestBody.put("temperature", 0.7); - requestBody.put("top_p", 0.95); - requestBody.put("max_tokens", 2000); - - // API 요청 헤더 설정 - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("api-key", openAiApiKey); - - // HTTP 요청 생성 - HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); - - // REST API 호출 - RestTemplate restTemplate = new RestTemplate(); - ResponseEntity response = restTemplate.exchange( - openAiApiUrl, - HttpMethod.POST, - requestEntity, - Map.class - ); - - // API 결과 처리 - Map responseBody = response.getBody(); - - // responseBody에서 "content" 부분을 추출 - String content = (String) ((Map) ((Map) ((List) responseBody.get("choices")).get(0)).get("message")).get("content"); - - // "```json"과 "```"을 제거하여 실제 JSON 데이터만 추출 - String jsonContent = content.replaceAll("```json\\n|```", "").trim(); - - // JSON 데이터를 Map으로 파싱 - ObjectMapper objectMapper = new ObjectMapper(); - Map jsonData = objectMapper.readValue(jsonContent, new TypeReference>() {}); - - // BuildingUpdate DTO로 데이터 매핑 - BuildingUpdate buildingUpdate = new BuildingUpdate(); - buildingUpdate.setStructureReason((String) jsonData.get("structureReason")); - buildingUpdate.setRoofMaterial((String) jsonData.get("roofMaterial")); - buildingUpdate.setRoofCondition((String) jsonData.get("roofCondition")); - buildingUpdate.setWallMaterial((String) jsonData.get("wallMaterial")); - buildingUpdate.setWallCondition((String) jsonData.get("wallCondition")); - buildingUpdate.setWindowDoorMaterial((String) jsonData.get("windowDoorMaterial")); - buildingUpdate.setWindowDoorCondition((String) jsonData.get("windowDoorCondition")); - buildingUpdate.setOverallCondition((String) jsonData.get("overallCondition")); - buildingUpdate.setConditionReason((String) jsonData.get("conditionReason")); - buildingUpdate.setCrackScore((Integer) jsonData.get("crackScore")); - buildingUpdate.setLeakScore((Integer) jsonData.get("leakScore")); - buildingUpdate.setCorrosionScore((Integer) jsonData.get("corrosionScore")); - buildingUpdate.setAgingScore((Integer) jsonData.get("agingScore")); - buildingUpdate.setTotalScore((Integer) jsonData.get("totalScore")); - buildingUpdate.setRepairList((String) jsonData.get("repairList")); - - // 매핑된 DTO 출력 (디버깅 용도) - System.out.println(buildingUpdate); - - //buildingService.updateBuilding(buildingId, buildingUpdate); - - // 리턴값 반환 - return ResponseEntity.ok(responseBody); - } - - @PostMapping("/analyze-images/material") - public ResponseEntity> analyzeMaterialImages(@RequestBody ImageRequest imageRequest) throws JsonProcessingException { - - // 시스템 메시지 - String systemMessage = """ - 너는 사진을 받으면 해당 사진에 나오는 부자재의 종류들과 그 사용법들을 반환하는 챗봇 AI다. - - 제공된 사진을 분석하여 부자재의 종류와 사용법을 양식에 맞게 작성된 JSON 형식으로 출력해야 한다. - - 반드시 양식에 맞게 작성해야 하며, 다른 방식으로의 응답은 절대 허용하지 않는다. - - mateial과 usage는 여러개가 올 수 있지만 리스트 형식으로는 '절대' 출력하면 안된다. - - mateiral과 usage가 여러 가지 이상으로 판단될 시, 반드시 ,로만 구분하여 한 개의 string으로 출력해야 한다. - - 예시 JSON은 다음과 같다. - - { - "material" : "나무 판자, 쇠 파이프" - "usage" : "벽 만들기, 천장 고치기" - } - """; - - // OpenAI API에 보낼 요청 데이터 작성 - Map requestBody = new HashMap<>(); - List> messages = new ArrayList<>(); - - // System 메시지 추가 - Map systemContent = new HashMap<>(); - systemContent.put("type", "text"); - systemContent.put("text", systemMessage); - messages.add(Map.of("role", "system", "content", List.of(systemContent))); - - // User 메시지 추가 (여러 이미지 URL 포함) - List> imageUrls = new ArrayList<>(); - imageRequest.getContent().forEach(content -> { - String url = content.getImageUrlOrNull(); - if (url != null) { - Map imageUrlContent = new HashMap<>(); - imageUrlContent.put("type", "image_url"); - imageUrlContent.put("image_url", Map.of("url", url)); - imageUrls.add(imageUrlContent); - } - }); - messages.add(Map.of("role", "user", "content", imageUrls)); - - // Assistant 메시지 추가 (빈 값으로 초기화) - Map assistantContent = new HashMap<>(); - assistantContent.put("type", "text"); - assistantContent.put("text", ""); - messages.add(Map.of("role", "assistant", "content", List.of(assistantContent))); - - // 요청 본문에 messages 리스트와 옵션 추가 - requestBody.put("messages", messages); - requestBody.put("temperature", 0.7); - requestBody.put("top_p", 0.95); - requestBody.put("max_tokens", 2000); - - // API 요청 헤더 설정 - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("api-key", openAiApiKey); - - // HTTP 요청 생성 - HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); - - // REST API 호출 - RestTemplate restTemplate = new RestTemplate(); - ResponseEntity response = restTemplate.exchange( - openAiApiUrl, - HttpMethod.POST, - requestEntity, - Map.class - ); - - // API 결과 처리 - Map responseBody = response.getBody(); - - // responseBody에서 "content" 부분을 추출 - String content = (String) ((Map) ((Map) ((List) responseBody.get("choices")).get(0)).get("message")).get("content"); - - // "```json"과 "```"을 제거하여 실제 JSON 데이터만 추출 - String jsonContent = content.replaceAll("```json\\n|```", "").trim(); - - // JSON 데이터를 Map으로 파싱 - ObjectMapper objectMapper = new ObjectMapper(); - 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); - } } diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java b/src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java index 5c81de3..2abddb4 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java @@ -1,15 +1,27 @@ package org.khtml.hexagonal.domain.building; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +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.auth.JwtValidator; import org.khtml.hexagonal.domain.building.application.BuildingService; import org.khtml.hexagonal.domain.user.User; import org.khtml.hexagonal.global.support.response.ApiResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @RequestMapping("/api/v1/buildings") @RequiredArgsConstructor @@ -19,6 +31,12 @@ public class BuildingController { private final BuildingService buildingService; private final JwtValidator jwtValidator; + @Value("${openai.api.url}") + private String openAiApiUrl; + + @Value("${openai.api.key}") + private String openAiApiKey; + @GetMapping("/{building-id}") public ApiResponse getBuildingDetail( @PathVariable(name = "building-id") String buildingId @@ -31,10 +49,288 @@ public ApiResponse registerBuilding( @RequestHeader("Authorization") String token, @RequestParam("images") List multipartFiles, @PathVariable(name = "building-id") String buildingId + ) throws IOException { User requestUser = jwtValidator.getUserFromToken(token); - buildingService.registerBuilding(buildingId, requestUser, multipartFiles); - return ApiResponse.success(); + List urls = buildingService.registerBuilding(buildingId, requestUser, multipartFiles); + BuildingUpdate buildingUpdate; + 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); + + + buildingUpdate = analyzeHouseImages(imageRequest); + + return ApiResponse.success(buildingUpdate); + } + + + public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonProcessingException { + + // 시스템 메시지 + String systemMessage = """ + + 너는 오래된 건물의 상태를 평가하는 챗봇 AI다. 제공된 사진을 분석하여 건물의 상태를 진단하고, 결과를 한글로 작성된 JSON 형식으로 출력해야 한다. + 이 JSON은 아래 예시와 같이 각 필드가 특정 영어 변수에 매핑되도록 구성되어야 한다. + + 건물 구조 분석 (structureReason): + + 구조: 건물의 구조가 전통적인지 현대적인지 판단한다. + 이유: 판단의 이유를 설명한다. + 건축 요소 평가 (roof, walls, windowsAndDoors): + + 지붕 형태 (roof): + 재료: 지붕에 사용된 재료를 확인한다. + 상태: 지붕의 상태를 평가한다. + 외벽 재질 (walls): + 재료: 외벽에 사용된 재료를 확인한다. + 상태: 외벽의 상태를 평가한다. + 창문 및 문 형태 (windowsAndDoors): + 재료: 창문과 문에 사용된 재료를 확인한다. + 상태: 창문과 문의 상태를 평가한다. + 건물 상태 평가 (overallCondition): + + 평가: 건물의 전반적인 상태를 구체적으로 평가한다. + 이유: 해당 평가의 이유를 제시한다. + 상세 점수화 (detailedScores): + + 균열 여부 (cracks): 균열의 존재 여부와 심각성을 100점 만점으로 점수화한다. + 누수 여부 (leaks): 누수의 존재 여부와 심각성을 100점 만점으로 점수화한다. + 부식 여부 (corrosion): 부식의 정도를 100점 만점으로 점수화한다. + 노후화 정도 (aging): 건물의 노후화를 100점 만점으로 점수화한다. + 총점수 (totalScore): 위의 점수를 합산하여 반올림한 총점을 계산한다. + 보수 필요성 판단 (repairNeeds): + + 조명: 사진 속 조명이 형광등이라면 LED로 교체가 필요한지, 이미 LED라면 교체가 불필요한지 판단한다. + 창호 보강, 도배, 장판 교체: 창호 보강, 도배, 장판 교체의 필요 여부를 판단한다. + + 판단하기 어려운 부분, 예를 들어 실내 사진인데 지붕 재료, 지붕 상태 등을 판단해야 하는 경우에는 해당 부분을 ""으로 처리한다. + repairlist의 경우 반드시 ,로 구분한다. repairlist가 없는 경우에는 ""으로 처리한다. + + + 예시 JSON은 다음과 같다. + + { + "structureReason": "구조 평가 이유", + "roofMaterial": "지붕 재료", + "roofCondition": "지붕 상태", + "wallMaterial": "외벽 재료", + "wallCondition": "외벽 상태", + "windowDoorMaterial": "창문 및 문 재료", + "windowDoorCondition": "창문 및 문 상태", + "overallCondition": "건물 상태 평가", + "conditionReason": "건물 상태 평가 이유", + "crackScore": 20, + "leakScore": 0, + "corrosionScore": 0, + "agingScore": 0, + "totalScore": 20, + "repairList": "LED 교체, 창호 보강, 도배, 장판 교체" + } + + + + + """; + + // OpenAI API에 보낼 요청 데이터 작성 + Map requestBody = new HashMap<>(); + List> messages = new ArrayList<>(); + + // System 메시지 추가 + Map systemContent = new HashMap<>(); + systemContent.put("type", "text"); + systemContent.put("text", systemMessage); + messages.add(Map.of("role", "system", "content", List.of(systemContent))); + + // User 메시지 추가 (여러 이미지 URL 포함) + List> imageUrls = new ArrayList<>(); + imageRequest.getContent().forEach(content -> { + String url = content.getImageUrlOrNull(); + if (url != null) { + Map imageUrlContent = new HashMap<>(); + imageUrlContent.put("type", "image_url"); + imageUrlContent.put("image_url", Map.of("url", url)); + imageUrls.add(imageUrlContent); + } + }); + messages.add(Map.of("role", "user", "content", imageUrls)); + + // Assistant 메시지 추가 (빈 값으로 초기화) + Map assistantContent = new HashMap<>(); + assistantContent.put("type", "text"); + assistantContent.put("text", ""); + messages.add(Map.of("role", "assistant", "content", List.of(assistantContent))); + + // 요청 본문에 messages 리스트와 옵션 추가 + requestBody.put("messages", messages); + requestBody.put("temperature", 0.7); + requestBody.put("top_p", 0.95); + requestBody.put("max_tokens", 2000); + + // API 요청 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("api-key", openAiApiKey); + + // HTTP 요청 생성 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // REST API 호출 + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange( + openAiApiUrl, + HttpMethod.POST, + requestEntity, + Map.class + ); + + // API 결과 처리 + Map responseBody = response.getBody(); + + // responseBody에서 "content" 부분을 추출 + String content = (String) ((Map) ((Map) ((List) responseBody.get("choices")).get(0)).get("message")).get("content"); + + // "```json"과 "```"을 제거하여 실제 JSON 데이터만 추출 + String jsonContent = content.replaceAll("```json\\n|```", "").trim(); + + // JSON 데이터를 Map으로 파싱 + ObjectMapper objectMapper = new ObjectMapper(); + Map jsonData = objectMapper.readValue(jsonContent, new TypeReference>() {}); + + // BuildingUpdate DTO로 데이터 매핑 + BuildingUpdate buildingUpdate = new BuildingUpdate(); + buildingUpdate.setStructureReason((String) jsonData.get("structureReason")); + buildingUpdate.setRoofMaterial((String) jsonData.get("roofMaterial")); + buildingUpdate.setRoofCondition((String) jsonData.get("roofCondition")); + buildingUpdate.setWallMaterial((String) jsonData.get("wallMaterial")); + buildingUpdate.setWallCondition((String) jsonData.get("wallCondition")); + buildingUpdate.setWindowDoorMaterial((String) jsonData.get("windowDoorMaterial")); + buildingUpdate.setWindowDoorCondition((String) jsonData.get("windowDoorCondition")); + buildingUpdate.setOverallCondition((String) jsonData.get("overallCondition")); + buildingUpdate.setConditionReason((String) jsonData.get("conditionReason")); + buildingUpdate.setCrackScore((Integer) jsonData.get("crackScore")); + buildingUpdate.setLeakScore((Integer) jsonData.get("leakScore")); + buildingUpdate.setCorrosionScore((Integer) jsonData.get("corrosionScore")); + buildingUpdate.setAgingScore((Integer) jsonData.get("agingScore")); + buildingUpdate.setTotalScore((Integer) jsonData.get("totalScore")); + buildingUpdate.setRepairList((String) jsonData.get("repairList")); + + // 매핑된 DTO 출력 (디버깅 용도) + System.out.println(buildingUpdate); + + //buildingService.updateBuilding(buildingId, buildingUpdate); + + // 리턴값 반환 + return buildingUpdate; + } + + public ResponseEntity> analyzeMaterialImages(ImageRequest imageRequest) throws JsonProcessingException { + + // 시스템 메시지 + String systemMessage = """ + 너는 사진을 받으면 해당 사진에 나오는 부자재의 종류들과 그 사용법들을 반환하는 챗봇 AI다. + + 제공된 사진을 분석하여 부자재의 종류와 사용법을 양식에 맞게 작성된 JSON 형식으로 출력해야 한다. + + 반드시 양식에 맞게 작성해야 하며, 다른 방식으로의 응답은 절대 허용하지 않는다. + + mateial과 usage는 여러개가 올 수 있지만 리스트 형식으로는 '절대' 출력하면 안된다. + + mateiral과 usage가 여러 가지 이상으로 판단될 시, 반드시 ,로만 구분하여 한 개의 string으로 출력해야 한다. + + 예시 JSON은 다음과 같다. + + { + "material" : "나무 판자, 쇠 파이프" + "usage" : "벽 만들기, 천장 고치기" + } + """; + + // OpenAI API에 보낼 요청 데이터 작성 + Map requestBody = new HashMap<>(); + List> messages = new ArrayList<>(); + + // System 메시지 추가 + Map systemContent = new HashMap<>(); + systemContent.put("type", "text"); + systemContent.put("text", systemMessage); + messages.add(Map.of("role", "system", "content", List.of(systemContent))); + + // User 메시지 추가 (여러 이미지 URL 포함) + List> imageUrls = new ArrayList<>(); + imageRequest.getContent().forEach(content -> { + String url = content.getImageUrlOrNull(); + if (url != null) { + Map imageUrlContent = new HashMap<>(); + imageUrlContent.put("type", "image_url"); + imageUrlContent.put("image_url", Map.of("url", url)); + imageUrls.add(imageUrlContent); + } + }); + messages.add(Map.of("role", "user", "content", imageUrls)); + + // Assistant 메시지 추가 (빈 값으로 초기화) + Map assistantContent = new HashMap<>(); + assistantContent.put("type", "text"); + assistantContent.put("text", ""); + messages.add(Map.of("role", "assistant", "content", List.of(assistantContent))); + + // 요청 본문에 messages 리스트와 옵션 추가 + requestBody.put("messages", messages); + requestBody.put("temperature", 0.7); + requestBody.put("top_p", 0.95); + requestBody.put("max_tokens", 2000); + + // API 요청 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("api-key", openAiApiKey); + + // HTTP 요청 생성 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + // REST API 호출 + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange( + openAiApiUrl, + HttpMethod.POST, + requestEntity, + Map.class + ); + + // API 결과 처리 + Map responseBody = response.getBody(); + + // responseBody에서 "content" 부분을 추출 + String content = (String) ((Map) ((Map) ((List) responseBody.get("choices")).get(0)).get("message")).get("content"); + + // "```json"과 "```"을 제거하여 실제 JSON 데이터만 추출 + String jsonContent = content.replaceAll("```json\\n|```", "").trim(); + + // JSON 데이터를 Map으로 파싱 + ObjectMapper objectMapper = new ObjectMapper(); + 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); } } 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 5f6df5a..b36bb50 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 @@ -1,7 +1,7 @@ package org.khtml.hexagonal.domain.building.application; import lombok.RequiredArgsConstructor; -import org.khtml.hexagonal.domain.ai.dto.BuildingUpdate; +import org.khtml.hexagonal.domain.building.dto.BuildingUpdate; import org.khtml.hexagonal.domain.building.*; import org.khtml.hexagonal.domain.user.User; import org.khtml.hexagonal.domain.user.UserRepository; @@ -10,8 +10,8 @@ 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) @@ -62,7 +62,8 @@ public Building updateBuilding(String buildingId, BuildingUpdate buildingUpdate) } @Transactional - public void registerBuilding(String buildingId, User requestUser, List multipartFiles) throws IOException { + public List registerBuilding(String buildingId, User requestUser, List multipartFiles) throws IOException { + List ret = new ArrayList<>(); for(MultipartFile file : multipartFiles) { String url = blobManager.storeFile(file.getOriginalFilename(), file.getInputStream(), file.getSize()); @@ -84,7 +85,11 @@ public void registerBuilding(String buildingId, User requestUser, List Date: Tue, 13 Aug 2024 22:22:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[FEAT]:=20GPT=20API=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/GptService.java} | 44 +++----------- .../hexagonal/domain/auth/AuthService.java | 27 +++++---- .../{ => application}/BlobManager.java | 2 +- .../building/application/BuildingService.java | 33 +++++++++- .../controller/BuildingController.java | 60 +++++++++++++++++++ .../dto/BuildingDescriptionRequest.java | 6 ++ .../{ => dto}/BuildingDetailResponse.java | 4 +- .../building/{ => entity}/Building.java | 30 +++++++++- .../building/{ => entity}/BuildingImage.java | 2 +- .../domain/building/{ => entity}/Image.java | 3 +- .../BuildingImageRepository.java | 3 +- .../{ => repository}/BuildingRepository.java | 3 +- .../{ => repository}/ImageRepository.java | 3 +- .../hexagonal/domain/material/Material.java | 2 +- .../domain/material/MaterialService.java | 7 +++ .../hexagonal/domain/user/UserRepository.java | 5 ++ src/main/resources/application.yml | 7 +++ 17 files changed, 181 insertions(+), 60 deletions(-) rename src/main/java/org/khtml/hexagonal/domain/{building/BuildingController.java => ai/application/GptService.java} (90%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => application}/BlobManager.java (97%) create mode 100644 src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java create mode 100644 src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDescriptionRequest.java rename src/main/java/org/khtml/hexagonal/domain/building/{ => dto}/BuildingDetailResponse.java (92%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => entity}/Building.java (65%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => entity}/BuildingImage.java (93%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => entity}/Image.java (87%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => repository}/BuildingImageRepository.java (55%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => repository}/BuildingRepository.java (75%) rename src/main/java/org/khtml/hexagonal/domain/building/{ => repository}/ImageRepository.java (54%) diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java b/src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java similarity index 90% rename from src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java rename to src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java index 2abddb4..f572571 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BuildingController.java +++ b/src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java @@ -1,4 +1,4 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.ai.application; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -7,29 +7,21 @@ 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.auth.JwtValidator; -import org.khtml.hexagonal.domain.building.application.BuildingService; -import org.khtml.hexagonal.domain.user.User; -import org.khtml.hexagonal.global.support.response.ApiResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; -import org.springframework.web.bind.annotation.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; -import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -@RequestMapping("/api/v1/buildings") +@Transactional(readOnly = true) @RequiredArgsConstructor -@RestController -public class BuildingController { - - private final BuildingService buildingService; - private final JwtValidator jwtValidator; +@Service +public class GptService { @Value("${openai.api.url}") private String openAiApiUrl; @@ -37,22 +29,7 @@ public class BuildingController { @Value("${openai.api.key}") private String openAiApiKey; - @GetMapping("/{building-id}") - public ApiResponse getBuildingDetail( - @PathVariable(name = "building-id") String buildingId - ) { - return ApiResponse.success(BuildingDetailResponse.toResponse(buildingService.getBuilding(buildingId))); - } - - @PostMapping("/{building-id}/register") - public ApiResponse registerBuilding( - @RequestHeader("Authorization") String token, - @RequestParam("images") List multipartFiles, - @PathVariable(name = "building-id") String buildingId - - ) throws IOException { - User requestUser = jwtValidator.getUserFromToken(token); - List urls = buildingService.registerBuilding(buildingId, requestUser, multipartFiles); + public BuildingUpdate analyzeBuilding(List urls) throws JsonProcessingException { BuildingUpdate buildingUpdate; ImageRequest imageRequest = new ImageRequest(); List contentList = new ArrayList<>(); @@ -71,14 +48,9 @@ public ApiResponse registerBuilding( } imageRequest.setContent(contentList); - - - buildingUpdate = analyzeHouseImages(imageRequest); - - return ApiResponse.success(buildingUpdate); + return analyzeHouseImages(imageRequest); } - public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonProcessingException { // 시스템 메시지 diff --git a/src/main/java/org/khtml/hexagonal/domain/auth/AuthService.java b/src/main/java/org/khtml/hexagonal/domain/auth/AuthService.java index f269ec4..86a8f71 100644 --- a/src/main/java/org/khtml/hexagonal/domain/auth/AuthService.java +++ b/src/main/java/org/khtml/hexagonal/domain/auth/AuthService.java @@ -17,17 +17,22 @@ public class AuthService { @Transactional public TokenResult login(LoginRequest loginRequest) { - User newUser = User.builder() - .providerId(loginRequest.providerId()) - .locationConsent(loginRequest.locationConsent()) - .numberOfPersons(loginRequest.numberOfPersons()) - .phoneNumber(loginRequest.phoneNumber()) - .username(loginRequest.username()) - .userType(UserType.valueOf(loginRequest.userType())) - .build(); - - User savedUser = userRepository.save(newUser); - String accessToken = jwtGenerator.generateAccessToken(savedUser.getId()); + User user; + if (!userRepository.existsByProviderId(loginRequest.providerId())) { + user = User.builder() + .providerId(loginRequest.providerId()) + .locationConsent(loginRequest.locationConsent()) + .numberOfPersons(loginRequest.numberOfPersons()) + .phoneNumber(loginRequest.phoneNumber()) + .username(loginRequest.username()) + .userType(UserType.valueOf(loginRequest.userType())) + .build(); + } else { + user = userRepository.findByProviderId(loginRequest.providerId()) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + } + userRepository.save(user); + String accessToken = jwtGenerator.generateAccessToken(user.getId()); String refreshToken = jwtGenerator.generateRefreshToken(); return new TokenResult(accessToken, refreshToken); diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BlobManager.java b/src/main/java/org/khtml/hexagonal/domain/building/application/BlobManager.java similarity index 97% rename from src/main/java/org/khtml/hexagonal/domain/building/BlobManager.java rename to src/main/java/org/khtml/hexagonal/domain/building/application/BlobManager.java index b33ff91..8e8325e 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BlobManager.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/application/BlobManager.java @@ -1,4 +1,4 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.application; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; 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 b36bb50..aecc111 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 @@ -1,10 +1,17 @@ package org.khtml.hexagonal.domain.building.application; import lombok.RequiredArgsConstructor; +import org.khtml.hexagonal.domain.building.ImageType; import org.khtml.hexagonal.domain.building.dto.BuildingUpdate; -import org.khtml.hexagonal.domain.building.*; +import org.khtml.hexagonal.domain.building.entity.Building; +import org.khtml.hexagonal.domain.building.entity.BuildingImage; +import org.khtml.hexagonal.domain.building.entity.Image; +import org.khtml.hexagonal.domain.building.repository.BuildingImageRepository; +import org.khtml.hexagonal.domain.building.repository.BuildingRepository; +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; @@ -12,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; @RequiredArgsConstructor @Transactional(readOnly = true) @@ -64,7 +72,7 @@ public Building updateBuilding(String buildingId, BuildingUpdate buildingUpdate) @Transactional public List registerBuilding(String buildingId, User requestUser, List multipartFiles) throws IOException { List ret = new ArrayList<>(); - for(MultipartFile file : multipartFiles) { + for (MultipartFile file : multipartFiles) { String url = blobManager.storeFile(file.getOriginalFilename(), file.getInputStream(), file.getSize()); User user = userRepository.findById(requestUser.getId()).orElseThrow(() -> new IllegalArgumentException("User not found")); @@ -75,6 +83,7 @@ public List registerBuilding(String buildingId, User requestUser, List registerBuilding(String buildingId, User requestUser, List new IllegalArgumentException("Building not found")); + + building.updateAnalyzedData(buildingUpdate); + } + + @Transactional + public void updateBuildingDescription(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.setDescription(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 new file mode 100644 index 0000000..3d374d1 --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java @@ -0,0 +1,60 @@ +package org.khtml.hexagonal.domain.building.controller; + +import lombok.RequiredArgsConstructor; +import org.khtml.hexagonal.domain.ai.application.GptService; +import org.khtml.hexagonal.domain.building.dto.*; +import org.khtml.hexagonal.domain.auth.JwtValidator; +import org.khtml.hexagonal.domain.building.application.BuildingService; +import org.khtml.hexagonal.domain.user.User; +import org.khtml.hexagonal.global.support.response.ApiResponse; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +@RequestMapping("/api/v1/buildings") +@RequiredArgsConstructor +@RestController +public class BuildingController { + + private final BuildingService buildingService; + private final JwtValidator jwtValidator; + private final GptService gptService; + + @GetMapping("/{building-id}") + public ApiResponse getBuildingDetail( + @PathVariable(name = "building-id") String buildingId + ) { + return ApiResponse.success(BuildingDetailResponse.toResponse(buildingService.getBuilding(buildingId))); + } + + @PostMapping("/{building-id}/register") + public ApiResponse registerBuilding( + @RequestHeader("Authorization") String token, + @RequestParam("images") List multipartFiles, + @PathVariable(name = "building-id") String buildingId + ) throws IOException { + User requestUser = jwtValidator.getUserFromToken(token); + List urls = buildingService.registerBuilding(buildingId, requestUser, multipartFiles); + + BuildingUpdate buildingUpdate = gptService.analyzeBuilding(urls); + buildingService.updateAnalyzedBuilding(buildingId, buildingUpdate); + + return ApiResponse.success(buildingUpdate); + } + + @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); + buildingService.updateBuildingDescription(buildingId, requestUser.getId(), buildingDescriptionRequest.description()); + + return ApiResponse.success(); + } + + +} diff --git a/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDescriptionRequest.java b/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDescriptionRequest.java new file mode 100644 index 0000000..83e6487 --- /dev/null +++ b/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDescriptionRequest.java @@ -0,0 +1,6 @@ +package org.khtml.hexagonal.domain.building.dto; + +public record BuildingDescriptionRequest( + String description +) { +} diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BuildingDetailResponse.java b/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java similarity index 92% rename from src/main/java/org/khtml/hexagonal/domain/building/BuildingDetailResponse.java rename to src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java index 1bd418b..02d8d82 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BuildingDetailResponse.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java @@ -1,4 +1,6 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.dto; + +import org.khtml.hexagonal.domain.building.entity.Building; public record BuildingDetailResponse( String buildingId, diff --git a/src/main/java/org/khtml/hexagonal/domain/building/Building.java b/src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java similarity index 65% rename from src/main/java/org/khtml/hexagonal/domain/building/Building.java rename to src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java index 67f99a9..85aac91 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/Building.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java @@ -1,13 +1,13 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.entity; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.khtml.hexagonal.domain.building.BuildingStatus; +import org.khtml.hexagonal.domain.building.dto.BuildingUpdate; import org.khtml.hexagonal.domain.user.User; -import java.util.List; - @Table(name = "building") @Getter @Setter @@ -18,6 +18,7 @@ public class Building { @Id private String gisBuildingId; + @Enumerated(EnumType.STRING) @Column(name = "building_status") private BuildingStatus buildingStatus = BuildingStatus.NOT_REGISTERED; @@ -105,6 +106,9 @@ public class Building { @Column(name = "description") private String description; + @Column(name = "is_analyzed") + private Boolean isAnalyzed = false; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; @@ -113,4 +117,24 @@ public void updateUser(User user) { this.user = user; } + public void updateAnalyzedData(BuildingUpdate buildingUpdate) { + this.structureReason = buildingUpdate.getStructureReason(); + this.roofMaterial = buildingUpdate.getRoofMaterial(); + this.roofCondition = buildingUpdate.getRoofCondition(); + this.wallMaterial = buildingUpdate.getWallMaterial(); + this.wallCondition = buildingUpdate.getWallCondition(); + this.windowDoorMaterial = buildingUpdate.getWindowDoorMaterial(); + this.windowDoorCondition = buildingUpdate.getWindowDoorCondition(); + this.overallCondition = buildingUpdate.getOverallCondition(); + this.conditionReason = buildingUpdate.getConditionReason(); + this.crackScore = buildingUpdate.getCrackScore(); + this.leakScore = buildingUpdate.getLeakScore(); + this.corrosionScore = buildingUpdate.getCorrosionScore(); + this.agingScore = buildingUpdate.getAgingScore(); + this.totalScore = buildingUpdate.getTotalScore(); + this.repairList = buildingUpdate.getRepairList(); + this.isAnalyzed = true; + this.buildingStatus = BuildingStatus.REGISTERED; + } + } diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BuildingImage.java b/src/main/java/org/khtml/hexagonal/domain/building/entity/BuildingImage.java similarity index 93% rename from src/main/java/org/khtml/hexagonal/domain/building/BuildingImage.java rename to src/main/java/org/khtml/hexagonal/domain/building/entity/BuildingImage.java index dffab5b..943c997 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BuildingImage.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/entity/BuildingImage.java @@ -1,4 +1,4 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.entity; import jakarta.persistence.*; diff --git a/src/main/java/org/khtml/hexagonal/domain/building/Image.java b/src/main/java/org/khtml/hexagonal/domain/building/entity/Image.java similarity index 87% rename from src/main/java/org/khtml/hexagonal/domain/building/Image.java rename to src/main/java/org/khtml/hexagonal/domain/building/entity/Image.java index cc84f0a..b8ad672 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/Image.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/entity/Image.java @@ -1,10 +1,11 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.entity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.khtml.hexagonal.domain.building.ImageType; import org.khtml.hexagonal.domain.common.BaseEntity; import org.khtml.hexagonal.domain.user.User; diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BuildingImageRepository.java b/src/main/java/org/khtml/hexagonal/domain/building/repository/BuildingImageRepository.java similarity index 55% rename from src/main/java/org/khtml/hexagonal/domain/building/BuildingImageRepository.java rename to src/main/java/org/khtml/hexagonal/domain/building/repository/BuildingImageRepository.java index 26afcee..7bda770 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BuildingImageRepository.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/repository/BuildingImageRepository.java @@ -1,5 +1,6 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.repository; +import org.khtml.hexagonal.domain.building.entity.BuildingImage; import org.springframework.data.jpa.repository.JpaRepository; public interface BuildingImageRepository extends JpaRepository { diff --git a/src/main/java/org/khtml/hexagonal/domain/building/BuildingRepository.java b/src/main/java/org/khtml/hexagonal/domain/building/repository/BuildingRepository.java similarity index 75% rename from src/main/java/org/khtml/hexagonal/domain/building/BuildingRepository.java rename to src/main/java/org/khtml/hexagonal/domain/building/repository/BuildingRepository.java index 404c280..c37b0b1 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/BuildingRepository.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/repository/BuildingRepository.java @@ -1,5 +1,6 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.repository; +import org.khtml.hexagonal.domain.building.entity.Building; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/org/khtml/hexagonal/domain/building/ImageRepository.java b/src/main/java/org/khtml/hexagonal/domain/building/repository/ImageRepository.java similarity index 54% rename from src/main/java/org/khtml/hexagonal/domain/building/ImageRepository.java rename to src/main/java/org/khtml/hexagonal/domain/building/repository/ImageRepository.java index 437efa3..0fec944 100644 --- a/src/main/java/org/khtml/hexagonal/domain/building/ImageRepository.java +++ b/src/main/java/org/khtml/hexagonal/domain/building/repository/ImageRepository.java @@ -1,5 +1,6 @@ -package org.khtml.hexagonal.domain.building; +package org.khtml.hexagonal.domain.building.repository; +import org.khtml.hexagonal.domain.building.entity.Image; import org.springframework.data.jpa.repository.JpaRepository; public interface ImageRepository extends JpaRepository { diff --git a/src/main/java/org/khtml/hexagonal/domain/material/Material.java b/src/main/java/org/khtml/hexagonal/domain/material/Material.java index d6ab4ba..44dcfa1 100644 --- a/src/main/java/org/khtml/hexagonal/domain/material/Material.java +++ b/src/main/java/org/khtml/hexagonal/domain/material/Material.java @@ -5,7 +5,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.khtml.hexagonal.domain.building.Building; +import org.khtml.hexagonal.domain.building.entity.Building; import org.khtml.hexagonal.domain.common.BaseEntity; @Table(name = "material") 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 70bd971..1497d1e 100644 --- a/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java +++ b/src/main/java/org/khtml/hexagonal/domain/material/MaterialService.java @@ -2,6 +2,13 @@ import lombok.RequiredArgsConstructor; import org.khtml.hexagonal.domain.building.*; +import org.khtml.hexagonal.domain.building.application.BlobManager; +import org.khtml.hexagonal.domain.building.entity.Building; +import org.khtml.hexagonal.domain.building.entity.BuildingImage; +import org.khtml.hexagonal.domain.building.entity.Image; +import org.khtml.hexagonal.domain.building.repository.BuildingImageRepository; +import org.khtml.hexagonal.domain.building.repository.BuildingRepository; +import org.khtml.hexagonal.domain.building.repository.ImageRepository; import org.khtml.hexagonal.domain.user.User; import org.khtml.hexagonal.domain.user.UserRepository; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/khtml/hexagonal/domain/user/UserRepository.java b/src/main/java/org/khtml/hexagonal/domain/user/UserRepository.java index 2fb15f0..966eb09 100644 --- a/src/main/java/org/khtml/hexagonal/domain/user/UserRepository.java +++ b/src/main/java/org/khtml/hexagonal/domain/user/UserRepository.java @@ -2,5 +2,10 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + + Boolean existsByProviderId(String providerId); + Optional findByProviderId(String providerId); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7a357ed..6bd0ef8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -82,3 +82,10 @@ storage: cacheServerConfiguration: true elideSetAutoCommits: true maintainTimeStats: false + +spring: + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB +