Skip to content

Commit d322d9d

Browse files
committed
[FEAT]: Material Gpt Api 구현
1 parent 2355ccb commit d322d9d

File tree

9 files changed

+167
-67
lines changed

9 files changed

+167
-67
lines changed

src/main/java/org/khtml/hexagonal/domain/ai/application/GptService.java renamed to src/main/java/org/khtml/hexagonal/domain/ai/application/GptManager.java

Lines changed: 87 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import org.khtml.hexagonal.domain.building.dto.BuildingUpdate;
88
import org.khtml.hexagonal.domain.building.dto.ImageRequest;
99
import org.khtml.hexagonal.domain.building.dto.MaterialInfo;
10+
import org.khtml.hexagonal.domain.building.dto.MaterialResult;
1011
import org.springframework.beans.factory.annotation.Value;
1112
import org.springframework.http.*;
13+
import org.springframework.stereotype.Component;
1214
import org.springframework.stereotype.Service;
1315
import org.springframework.transaction.annotation.Transactional;
1416
import org.springframework.web.client.RestTemplate;
@@ -18,10 +20,9 @@
1820
import java.util.List;
1921
import java.util.Map;
2022

21-
@Transactional(readOnly = true)
2223
@RequiredArgsConstructor
23-
@Service
24-
public class GptService {
24+
@Component
25+
public class GptManager {
2526

2627
@Value("${openai.api.url}")
2728
private String openAiApiUrl;
@@ -30,41 +31,52 @@ public class GptService {
3031
private String openAiApiKey;
3132

3233
public BuildingUpdate analyzeBuilding(List<String> urls) throws JsonProcessingException {
33-
BuildingUpdate buildingUpdate;
34-
ImageRequest imageRequest = new ImageRequest();
35-
List<ImageRequest.Content> contentList = new ArrayList<>();
34+
ImageRequest imageRequest = getImageRequest(urls);
35+
return analyzeHouseImages(imageRequest);
36+
}
3637

37-
imageRequest.setRole("user");
38+
public MaterialResult analyzeMaterial(List<String> urls) throws JsonProcessingException {
39+
ImageRequest imageRequest = getImageRequest(urls);
40+
Map<String, Object> result = analyzeMaterialImages(imageRequest);
3841

39-
for (String url : urls) {
40-
ImageRequest.Content content = new ImageRequest.Content();
41-
ImageRequest.ImageUrl imageUrl = new ImageRequest.ImageUrl();
42+
String material = null;
43+
String usage = null;
4244

43-
imageUrl.setUrl(url);
44-
content.setType("image_url");
45-
content.setImage_url(imageUrl);
46-
contentList.add(content);
45+
List<Map<String, Object>> choices = (List<Map<String, Object>>) result.get("choices");
46+
if (choices != null && !choices.isEmpty()) {
47+
Map<String, Object> firstChoice = choices.get(0);
48+
Map<String, Object> message = (Map<String, Object>) firstChoice.get("message");
49+
if (message != null) {
50+
String content = (String) message.get("content");
51+
52+
// content 값을 JSON으로 파싱
53+
ObjectMapper objectMapper = new ObjectMapper();
54+
Map<String, Object> jsonData = objectMapper.readValue(content, new TypeReference<Map<String, Object>>() {
55+
});
4756

57+
// 필요한 데이터 추출
58+
material = (String) jsonData.get("material");
59+
usage = (String) jsonData.get("usage");
60+
}
4861
}
4962

50-
imageRequest.setContent(contentList);
51-
return analyzeHouseImages(imageRequest);
63+
return new MaterialResult(material, usage);
5264
}
5365

5466
public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonProcessingException {
5567

5668
// 시스템 메시지
5769
String systemMessage = """
58-
70+
5971
너는 오래된 건물의 상태를 평가하는 챗봇 AI다. 제공된 사진을 분석하여 건물의 상태를 진단하고, 결과를 한글로 작성된 JSON 형식으로 출력해야 한다.
6072
이 JSON은 아래 예시와 같이 각 필드가 특정 영어 변수에 매핑되도록 구성되어야 한다.
61-
73+
6274
건물 구조 분석 (structureReason):
63-
75+
6476
구조: 건물의 구조가 전통적인지 현대적인지 판단한다.
6577
이유: 판단의 이유를 설명한다.
6678
건축 요소 평가 (roof, walls, windowsAndDoors):
67-
79+
6880
지붕 형태 (roof):
6981
재료: 지붕에 사용된 재료를 확인한다.
7082
상태: 지붕의 상태를 평가한다.
@@ -75,27 +87,27 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP
7587
재료: 창문과 문에 사용된 재료를 확인한다.
7688
상태: 창문과 문의 상태를 평가한다.
7789
건물 상태 평가 (overallCondition):
78-
90+
7991
평가: 건물의 전반적인 상태를 구체적으로 평가한다.
8092
이유: 해당 평가의 이유를 제시한다.
8193
상세 점수화 (detailedScores):
82-
94+
8395
균열 여부 (cracks): 균열의 존재 여부와 심각성을 100점 만점으로 점수화한다.
8496
누수 여부 (leaks): 누수의 존재 여부와 심각성을 100점 만점으로 점수화한다.
8597
부식 여부 (corrosion): 부식의 정도를 100점 만점으로 점수화한다.
8698
노후화 정도 (aging): 건물의 노후화를 100점 만점으로 점수화한다.
8799
총점수 (totalScore): 위의 점수를 합산하여 반올림한 총점을 계산한다.
88100
보수 필요성 판단 (repairNeeds):
89-
101+
90102
조명: 사진 속 조명이 형광등이라면 LED로 교체가 필요한지, 이미 LED라면 교체가 불필요한지 판단한다.
91103
창호 보강, 도배, 장판 교체: 창호 보강, 도배, 장판 교체의 필요 여부를 판단한다.
92-
104+
93105
판단하기 어려운 부분, 예를 들어 실내 사진인데 지붕 재료, 지붕 상태 등을 판단해야 하는 경우에는 해당 부분을 ""으로 처리한다.
94106
repairlist의 경우 반드시 ,로 구분한다. repairlist가 없는 경우에는 ""으로 처리한다.
95-
96-
107+
108+
97109
예시 JSON은 다음과 같다.
98-
110+
99111
{
100112
"structureReason": "구조 평가 이유",
101113
"roofMaterial": "지붕 재료",
@@ -113,10 +125,10 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP
113125
"totalScore": 20,
114126
"repairList": "LED 교체, 창호 보강, 도배, 장판 교체"
115127
}
116-
117-
118-
119-
128+
129+
130+
131+
120132
""";
121133

122134
// OpenAI API에 보낼 요청 데이터 작성
@@ -182,7 +194,8 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP
182194

183195
// JSON 데이터를 Map으로 파싱
184196
ObjectMapper objectMapper = new ObjectMapper();
185-
Map<String, Object> jsonData = objectMapper.readValue(jsonContent, new TypeReference<Map<String, Object>>() {});
197+
Map<String, Object> jsonData = objectMapper.readValue(jsonContent, new TypeReference<Map<String, Object>>() {
198+
});
186199

187200
// BuildingUpdate DTO로 데이터 매핑
188201
BuildingUpdate buildingUpdate = new BuildingUpdate();
@@ -211,27 +224,27 @@ public BuildingUpdate analyzeHouseImages(ImageRequest imageRequest) throws JsonP
211224
return buildingUpdate;
212225
}
213226

214-
public ResponseEntity<Map<String, Object>> analyzeMaterialImages(ImageRequest imageRequest) throws JsonProcessingException {
227+
public Map<String, Object> analyzeMaterialImages(ImageRequest imageRequest) throws JsonProcessingException {
215228

216229
// 시스템 메시지
217230
String systemMessage = """
218-
너는 사진을 받으면 해당 사진에 나오는 부자재의 종류들과 그 사용법들을 반환하는 챗봇 AI다.
219-
220-
제공된 사진을 분석하여 부자재의 종류와 사용법을 양식에 맞게 작성된 JSON 형식으로 출력해야 한다.
221-
222-
반드시 양식에 맞게 작성해야 하며, 다른 방식으로의 응답은 절대 허용하지 않는다.
223-
224-
mateial과 usage는 여러개가 올 수 있지만 리스트 형식으로는 '절대' 출력하면 안된다.
225-
226-
mateiral과 usage가 여러 가지 이상으로 판단될 시, 반드시 ,로만 구분하여 한 개의 string으로 출력해야 한다.
227-
228-
예시 JSON은 다음과 같다.
229-
230-
{
231-
"material" : "나무 판자, 쇠 파이프"
232-
"usage" : "벽 만들기, 천장 고치기"
233-
}
234-
""";
231+
너는 사진을 받으면 해당 사진에 나오는 부자재의 종류들과 그 사용법들을 반환하는 챗봇 AI다.
232+
233+
제공된 사진을 분석하여 부자재의 종류와 사용법을 양식에 맞게 작성된 JSON 형식으로 출력해야 한다.
234+
235+
반드시 양식에 맞게 작성해야 하며, 다른 방식으로의 응답은 절대 허용하지 않는다.
236+
237+
mateial과 usage는 여러개가 올 수 있지만 리스트 형식으로는 '절대' 출력하면 안된다.
238+
239+
mateiral과 usage가 여러 가지 이상으로 판단될 시, 반드시 ,로만 구분하여 한 개의 string으로 출력해야 한다.
240+
241+
예시 JSON은 다음과 같다.
242+
243+
{
244+
"material" : "나무 판자, 쇠 파이프",
245+
"usage" : "벽 만들기, 천장 고치기"
246+
}
247+
""";
235248

236249
// OpenAI API에 보낼 요청 데이터 작성
237250
Map<String, Object> requestBody = new HashMap<>();
@@ -292,17 +305,38 @@ public ResponseEntity<Map<String, Object>> analyzeMaterialImages(ImageRequest im
292305
String content = (String) ((Map<String, Object>) ((Map<String, Object>) ((List<Object>) responseBody.get("choices")).get(0)).get("message")).get("content");
293306

294307
// "```json"과 "```"을 제거하여 실제 JSON 데이터만 추출
295-
String jsonContent = content.replaceAll("```json\\n|```", "").trim();
308+
String jsonContent = content.replaceAll("(?s)```json\\s*|```", "").trim();
296309

297310
// JSON 데이터를 Map으로 파싱
298311
ObjectMapper objectMapper = new ObjectMapper();
299-
Map<String, Object> jsonData = objectMapper.readValue(jsonContent, new TypeReference<Map<String, Object>>() {});
312+
Map<String, Object> jsonData = objectMapper.readValue(jsonContent, new TypeReference<Map<String, Object>>() {
313+
});
300314
// 필요한 데이터 사용
301315
MaterialInfo materialInfo = new MaterialInfo((String) jsonData.get("material"), (String) jsonData.get("usage"));
302316
System.out.println(materialInfo);
303317

304318
// 리턴값 반환
305-
return ResponseEntity.ok(responseBody);
319+
return responseBody;
320+
}
321+
322+
private static ImageRequest getImageRequest(List<String> urls) {
323+
ImageRequest imageRequest = new ImageRequest();
324+
List<ImageRequest.Content> contentList = new ArrayList<>();
325+
326+
imageRequest.setRole("user");
327+
328+
for (String url : urls) {
329+
ImageRequest.Content content = new ImageRequest.Content();
330+
ImageRequest.ImageUrl imageUrl = new ImageRequest.ImageUrl();
331+
332+
imageUrl.setUrl(url);
333+
content.setType("image_url");
334+
content.setImage_url(imageUrl);
335+
contentList.add(content);
336+
}
337+
338+
imageRequest.setContent(contentList);
339+
return imageRequest;
306340
}
307341

308342
}

src/main/java/org/khtml/hexagonal/domain/building/application/BuildingService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public void updateBuildingDescription(String buildingId, Long userId, String des
118118
throw new IllegalArgumentException(ErrorType.DEFAULT_ERROR.getMessage());
119119
}
120120

121-
building.setDescription(description);
121+
building.setBuildingDescription(description);
122122
}
123123

124124
}

src/main/java/org/khtml/hexagonal/domain/building/controller/BuildingController.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.khtml.hexagonal.domain.building.controller;
22

33
import lombok.RequiredArgsConstructor;
4-
import org.khtml.hexagonal.domain.ai.application.GptService;
4+
import org.khtml.hexagonal.domain.ai.application.GptManager;
55
import org.khtml.hexagonal.domain.building.dto.*;
66
import org.khtml.hexagonal.domain.auth.JwtValidator;
77
import org.khtml.hexagonal.domain.building.application.BuildingService;
@@ -20,7 +20,7 @@ public class BuildingController {
2020

2121
private final BuildingService buildingService;
2222
private final JwtValidator jwtValidator;
23-
private final GptService gptService;
23+
private final GptManager gptManager;
2424

2525
@GetMapping("/{building-id}")
2626
public ApiResponse<BuildingDetailResponse> getBuildingDetail(
@@ -38,7 +38,7 @@ public ApiResponse<?> registerBuilding(
3838
User requestUser = jwtValidator.getUserFromToken(token);
3939
List<String> urls = buildingService.registerBuilding(buildingId, requestUser, multipartFiles);
4040

41-
BuildingUpdate buildingUpdate = gptService.analyzeBuilding(urls);
41+
BuildingUpdate buildingUpdate = gptManager.analyzeBuilding(urls);
4242
buildingService.updateAnalyzedBuilding(buildingId, buildingUpdate);
4343

4444
return ApiResponse.success(buildingUpdate);
@@ -56,5 +56,4 @@ public ApiResponse<?> registerBuilding(
5656
return ApiResponse.success();
5757
}
5858

59-
6059
}

src/main/java/org/khtml/hexagonal/domain/building/dto/BuildingDetailResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public static BuildingDetailResponse toResponse(Building building) {
2222
return new BuildingDetailResponse(
2323
building.getGisBuildingId(),
2424
building.getLegalDistrictName() + " " + building.getLandLotNumber(),
25-
building.getDescription(),
25+
building.getBuildingDescription(),
2626
building.getUser() == null ? null : building.getUser().getPhoneNumber(),
2727
building.getCrackScore(),
2828
building.getLeakScore(),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.khtml.hexagonal.domain.building.dto;
2+
3+
public record MaterialResult(
4+
String material,
5+
String usage
6+
) {
7+
}

src/main/java/org/khtml/hexagonal/domain/building/entity/Building.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,14 @@ public class Building {
103103
@Column(name = "repair_list")
104104
private String repairList;
105105

106-
@Column(name = "description")
107-
private String description;
106+
@Column(name = "building_description")
107+
private String buildingDescription;
108+
109+
@Column(name = "material_description")
110+
private String materialDescription;
111+
112+
@Column(name = "material_usage")
113+
private String materialUsage;
108114

109115
@Column(name = "is_analyzed")
110116
private Boolean isAnalyzed = false;
@@ -117,6 +123,10 @@ public void updateUser(User user) {
117123
this.user = user;
118124
}
119125

126+
public void updateMaterialUsage(String materialUsage) {
127+
this.materialUsage = materialUsage;
128+
}
129+
120130
public void updateAnalyzedData(BuildingUpdate buildingUpdate) {
121131
this.structureReason = buildingUpdate.getStructureReason();
122132
this.roofMaterial = buildingUpdate.getRoofMaterial();

src/main/java/org/khtml/hexagonal/domain/material/MaterialController.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import lombok.RequiredArgsConstructor;
44
import org.khtml.hexagonal.domain.auth.JwtValidator;
5+
import org.khtml.hexagonal.domain.building.dto.BuildingDescriptionRequest;
56
import org.khtml.hexagonal.domain.user.User;
67
import org.khtml.hexagonal.global.support.response.ApiResponse;
78
import org.springframework.web.bind.annotation.*;
@@ -30,4 +31,16 @@ public ApiResponse<?> registerBuilding(
3031
return ApiResponse.success();
3132
}
3233

34+
@PostMapping("/{building-id}/register/description")
35+
public ApiResponse<?> registerBuilding(
36+
@RequestHeader("Authorization") String token,
37+
@PathVariable(name = "building-id") String buildingId,
38+
@RequestBody BuildingDescriptionRequest buildingDescriptionRequest
39+
) throws IOException {
40+
User requestUser = jwtValidator.getUserFromToken(token);
41+
materialService.updateMaterialDescription(buildingId, requestUser.getId(), buildingDescriptionRequest.description());
42+
43+
return ApiResponse.success();
44+
}
45+
3346
}

src/main/java/org/khtml/hexagonal/domain/material/MaterialRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
import org.springframework.data.jpa.repository.JpaRepository;
44

55
public interface MaterialRepository extends JpaRepository<Material, Long> {
6+
7+
Boolean existsByName(String name);
8+
69
}

0 commit comments

Comments
 (0)