Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ public String processPipeline(
// 실제 VisionServiceImpl 에서 사용하는 요소
@RequestParam("file") MultipartFile file,
@RequestParam("petName") String pet,
@RequestParam("info") String info,
Model model

) {
try {
// String prompt = togetherPromptBuilder.buildPrompt(visionReport, location);
String visionReport = visionService.analyze(file, pet);

String jsonPrompt = togetherPromptBuilder.buildPrompt(visionReport, location);
String jsonPrompt = togetherPromptBuilder.buildPrompt(visionReport, location, info);

log.info(jsonPrompt);
ObjectMapper objectMapper = new ObjectMapper();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import jakarta.servlet.http.HttpSession;
import java.util.Map;

@Slf4j
@Controller
@RequestMapping("/flow") // 기존 controller들과 충돌 방지
@RequestMapping("/flow")
@RequiredArgsConstructor
public class UnifiedFlowController {

Expand All @@ -36,34 +37,51 @@ public String page() {
public String analyze(
@RequestParam("file") MultipartFile file,
@RequestParam("petName") String petName,
@RequestParam("location") String location,
Model model
Model model,
HttpSession session
) {
try {
// 1. 중간 종 추론 결과
String interim = visionUseCase.interim(file.getBytes(), petName);

// 2. Vision 보고서 생성
String visionReport = visionService.analyze(file, petName);
log.info("📄 Vision Report: {}", visionReport);
log.info("📌 location = {}", location);

// 3. 프롬프트 생성 및 추천 요청
String jsonPrompt = togetherPromptBuilder.buildPrompt(visionReport, location);
log.info("📌 location = {}", location);
Map<String, String> promptMapper = new ObjectMapper().readValue(jsonPrompt, new TypeReference<>() {});
RecommendResponseDTO recommendation = recommendService.recommend(promptMapper);

Comment on lines 44 to 46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

file.getBytes() 호출로 메모리 두 번 사용

이미지 파일이 큰 경우 getBytes() 로 전체 바이트 배열을 생성하면서, 동일 파일을 visionService.analyze(file, …) 에 다시 전달하여 이중 메모리 사용이 발생합니다.

-String interim = visionUseCase.interim(file.getBytes(), petName);
-String visionReport = visionService.analyze(file, petName);
+byte[] imageBytes = file.getBytes();
+String interim = visionUseCase.interim(imageBytes, petName);
+String visionReport = visionService.analyze(imageBytes, petName); // analyze 메서드를 byte[] 버전으로 오버로드하거나 기존 시그니처 수정 권장

VisionServiceImpl 에 byte 배열 입력을 받는 오버로드를 추가하면 효율적입니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java
around lines 44 to 46, the code calls file.getBytes() to pass a byte array to
visionUseCase.interim, then passes the original file again to
visionService.analyze, causing duplicate memory usage for large files. To fix
this, add an overloaded method in VisionServiceImpl that accepts a byte array
input, then reuse the byte array from file.getBytes() for both calls to avoid
loading the file twice in memory.

// 4. 화면에 전달
model.addAttribute("interim", interim);
model.addAttribute("visionReport", visionReport);
model.addAttribute("recommendation", recommendation);
model.addAttribute("petName", petName);

session.setAttribute("visionReport", visionReport);
} catch (Exception e) {
log.error("❌ 분석 중 오류 발생", e);
model.addAttribute("error", "분석 및 추천 중 오류가 발생했습니다.");
log.error("❌ interim 분석 중 오류", e);
model.addAttribute("error", "중간 분석 중 오류 발생");
}
return "unifiedFlow";
}

@PostMapping("/report")
public String report(
@RequestParam("petName") String petName,
@RequestParam("location") String location,
@RequestParam("info") String info,
Model model,
HttpSession session
) {
Comment on lines +59 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

@PostMapping("/report") 파라미터 검증 및 예외 분리 필요

  • location, petName 이 빈 문자열일 때도 그대로 서비스 로직으로 전달됩니다. @NotBlank@Validated 를 적용하면 컨트롤러 진입 전에 검증 가능합니다.
  • catch (Exception e) 로 모든 예외를 묶으면 원인 파악이 어려워집니다. IOException, JsonProcessingException, 서비스 예외 등을 구분해 로그와 사용자 메시지를 분리해 주세요.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java
around lines 59 to 66, add @NotBlank annotations to the petName and location
parameters to ensure they are not empty before entering the service logic, and
annotate the controller class or method with @Validated to enable validation.
Also, replace the generic catch (Exception e) block with multiple specific catch
blocks for IOException, JsonProcessingException, and service-specific exceptions
to separate logging and user messages for each error type, improving error
clarity and handling.

try {
String visionReport = (String) session.getAttribute("visionReport");
if (visionReport == null) {
model.addAttribute("error", "세션에 Vision 보고서가 없습니다. 다시 분석을 시작해 주세요.");
return "unifiedFlow";
}

String jsonPrompt = togetherPromptBuilder.buildPrompt(visionReport, location, info);
Map<String, String> promptMapper = new ObjectMapper().readValue(jsonPrompt, new TypeReference<>() {});
RecommendResponseDTO recommendation = recommendService.recommend(promptMapper);

model.addAttribute("visionReport", visionReport); // 다시 보여주기 위해 필요
model.addAttribute("recommendation", recommendation);
model.addAttribute("petName", petName);
} catch (Exception e) {
log.error("❌ 추천 생성 중 오류", e);
model.addAttribute("error", "추천 생성 중 오류 발생");
}
return "unifiedFlow";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import com.fasterxml.jackson.core.JsonProcessingException;

public interface PromptGeneratorService {
String generatePrompt(String extractedPetInfoJson, String location) throws JsonProcessingException;
String generatePrompt(String extractedPetInfoJson, String location, String info) throws JsonProcessingException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ public class PromptGeneratorServiceImpl implements PromptGeneratorService {
ObjectMapper mapper = new ObjectMapper();

@Override
public String generatePrompt(String extractedPetInfoJson, String location) throws JsonProcessingException {
public String generatePrompt(String extractedPetInfoJson, String location, String info) throws JsonProcessingException {
// 필수 정보 예외
if (extractedPetInfoJson.isEmpty()) {
throw new IllegalArgumentException("에러! 필수 정보가 없습니다.");
} else if (location.isEmpty()) {
throw new IllegalArgumentException("에러! 사용자 위치 정보가 없습니다.");
}

// 추가 정보 : 비어도 상관 없음
if (info.isEmpty()) {
info = "입력된 추가 요청 사항 없음";
}

// 기본 문자열
String petInfoString = """
{
Expand All @@ -32,17 +38,12 @@ public String generatePrompt(String extractedPetInfoJson, String location) throw
Map<String, Object> petInfoMap = mapper.readValue(petInfoString, new TypeReference<>() {});
// location 항목 추가
petInfoMap.put("location", "%s".formatted(location));
// info 항목 추가
petInfoMap.put("info", "%s".formatted(info));
// JSON 문자열로 변환
String finalString = mapper.writeValueAsString(petInfoMap);
log.info(finalString);

return finalString;

// return String.format("""
// {
// %s,
// "location": "%s"
// }
// """, extractedPetInfoJson, location);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ public class TogetherPromptBuilder {
private final TogetherService togetherService;
private final PromptGeneratorService promptGeneratorService;

public String buildPrompt(String visionReport, String location) throws Exception {
public String buildPrompt(String visionReport, String location, String info) throws Exception {
try {
if (visionReport.isEmpty()) {
log.info("Vision API 에서 필수 정보를 받아오지 못했습니다.");
throw new IllegalStateException("Vision API 에서 필수 정보를 받아오지 못했습니다.");
}
String extractedPetInfoJson = togetherService.answer(
visionReport + " -> 이 문장에서 반려동물의 이름(name), 종(species), 무게(weight), 맹수 여부(is_danger(only true or false))를 JSON 형식으로 작성 + " +
"만약 반려동물의 종과 무게를 보았을 때, 입마개가 필요할 것 같다면 맹수 여부를 'true'로 작성 + " + "고양이는 맹수 여부를 false로 작성 " +
"만약 반려동물의 종과 무게를 보았을 때, 사람을 물어 중상을 입히거나 사망에 이르게 할 수 있는 경우 맹수 여부를 'true'로 작성 + " + "고양이는 맹수 여부를 false로 작성 " +
"무게는 kg 단위를 반드시 포함 " + "no markdown " + "-> 양식에 맞춰서 작성 " + """
"name": "???",
"species": "???",
"weight": "???",
"is_danger": "???"
""" + " -> 부가 설명이나 그 어떠한 텍스트 없이 양식의 빈 항목을 채워서 답변할 것."
);
return promptGeneratorService.generatePrompt(extractedPetInfoJson, location);
return promptGeneratorService.generatePrompt(extractedPetInfoJson, location, info);

} catch (RuntimeException e) {
log.info("TogetherPromptBuilder 내부 오류.");
Expand Down
67 changes: 47 additions & 20 deletions src/main/resources/templates/unifiedFlow.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,77 @@
body { font-family: Arial; margin: 40px; background: #f4f4f4; }
form, .result-block { background: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px; }
input[type=text], input[type=file] { width: 100%; padding: 10px; margin-top: 10px; border-radius: 5px; border: 1px solid #ccc; }
input[type=submit] { padding: 10px 20px; background: #007BFF; color: #fff; border-radius: 5px; cursor: pointer; border: none; }
input[type=submit]:hover { background: #0056b3; }
input[type=submit], button { padding: 10px 20px; background: #007BFF; color: #fff; border-radius: 5px; cursor: pointer; border: none; }
input[type=submit]:hover, button:hover { background: #0056b3; }
pre { background: #eee; padding: 10px; border-radius: 5px; overflow-x: auto; }
.dropdown { position: relative; }
.dropdown-list { display: none; position: absolute; top: 100%; width: 100%; max-height: 200px; overflow-y: auto; background: #fff; border: 1px solid #ccc; z-index: 100; }
.dropdown-list li { padding: 8px; cursor: pointer; }
.dropdown-list li:hover { background-color: #f0f0f0; }
</style>
</head>
<body>
<h1>🐾 반려동물 여행지 추천</h1>

<!-- 분석 시작 폼 -->
<form th:action="@{/flow/analyze}" method="post" enctype="multipart/form-data">
<label for="petName">반려동물 이름</label>
<input type="text" name="petName" id="petName" required />

<label for="file">반려동물 이미지</label>
<input type="file" name="file" id="file" accept="image/*" required />

<label for="locationInput">여행 희망 지역</label>
<div class="dropdown">
<input type="text" name="location" id="locationInput" placeholder="지역 키워드 검색" autocomplete="off" required />
<ul id="dropdownList" class="dropdown-list"></ul>
</div>

<input type="submit" value="여행지 추천받기" />
<input type="submit" value="내 반려동물 분석하기" />
</form>

<div class="result-block" th:if="${interim}">
<!-- interim 결과 -->
<div class="result-block" th:if="${interim}" id="interimBlock">
<h2>중간 분석 결과</h2>
<pre th:text="${interim}"></pre>
</div>

<div class="result-block" th:if="${visionReport}">
<!-- Vision 보고서 -->
<div class="result-block" th:if="${visionReport}" id="visionBlock">
<h2>최종 Vision 분석 보고서</h2>
<pre th:text="${visionReport}"></pre>
<form th:action="@{/flow/report}" method="post">
<input type="hidden" name="petName" th:value="${petName}" />

<label for="location">여행 희망 지역</label>
<div style="position: relative;">
<input type="text" name="location" id="location" placeholder="지역 키워드 검색" required />
</div>

<label for="info">추가 요청 사항</label>
<input type="text" name="info" id="info" />

<input type="submit" value="여행지 추천받기" />
</form>
</div>

<div class="result-block" th:if="${recommendation}">
<!-- 여행지 추천 -->
<div class="result-block" th:if="${recommendation}" id="recommendBlock">
<h2>추천 여행지</h2>
<pre th:text="${recommendation}"></pre>
</div>

<!-- 오류 -->
<div class="result-block" th:if="${error}">
<h2>오류 발생</h2>
<p th:text="${error}" style="color: red;"></p>
</div>

<script>
document.addEventListener("DOMContentLoaded", function () {
const locationInput = document.getElementById('locationInput');
const dropdownList = document.getElementById('dropdownList');
const locationInput = document.getElementById('location');
const dropdownList = document.createElement('ul');
dropdownList.className = 'dropdown-list';
dropdownList.style.position = 'absolute';
dropdownList.style.width = '100%';
dropdownList.style.maxHeight = '200px';
dropdownList.style.overflowY = 'auto';
dropdownList.style.backgroundColor = '#fff';
dropdownList.style.border = '1px solid #ccc';
dropdownList.style.zIndex = 100;
dropdownList.style.display = 'none';
locationInput.parentElement.appendChild(dropdownList);

Comment on lines +69 to 81
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

locationInput가 존재하지 않는 초기 페이지 진입 시 스크립트 오류 발생 가능성

visionReport가 아직 없는 최초 렌더링 단계에서는 id="location" 인 요소가 DOM 에 존재하지 않습니다.
하지만 스크립트는 DOMContentLoaded 직후 getElementById('location') 결과를 바로 사용 하고 있으며, null 에서 parentElement.appendChild·addEventListener 호출이 일어나면 런타임 JS 오류로 페이지가 비정상 종료됩니다.

간단한 방어 코드로 예방해 주세요.

-  const locationInput = document.getElementById('location');
-  const dropdownList = document.createElement('ul');
+  const locationInput = document.getElementById('location');
+  if (!locationInput) {
+    // location 입력이 없는 초기 화면에서는 자동완성 스크립트를 건너뜁니다.
+    return;
+  }
+  const dropdownList = document.createElement('ul');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const locationInput = document.getElementById('location');
const dropdownList = document.createElement('ul');
dropdownList.className = 'dropdown-list';
dropdownList.style.position = 'absolute';
dropdownList.style.width = '100%';
dropdownList.style.maxHeight = '200px';
dropdownList.style.overflowY = 'auto';
dropdownList.style.backgroundColor = '#fff';
dropdownList.style.border = '1px solid #ccc';
dropdownList.style.zIndex = 100;
dropdownList.style.display = 'none';
locationInput.parentElement.appendChild(dropdownList);
const locationInput = document.getElementById('location');
if (!locationInput) {
// location 입력이 없는 초기 화면에서는 자동완성 스크립트를 건너뜁니다.
return;
}
const dropdownList = document.createElement('ul');
dropdownList.className = 'dropdown-list';
dropdownList.style.position = 'absolute';
dropdownList.style.width = '100%';
dropdownList.style.maxHeight = '200px';
dropdownList.style.overflowY = 'auto';
dropdownList.style.backgroundColor = '#fff';
dropdownList.style.border = '1px solid #ccc';
dropdownList.style.zIndex = 100;
dropdownList.style.display = 'none';
locationInput.parentElement.appendChild(dropdownList);
🤖 Prompt for AI Agents
In src/main/resources/templates/unifiedFlow.html around lines 69 to 81, the code
assumes the element with id 'location' exists and directly accesses its
parentElement and adds event listeners, which causes runtime errors if the
element is not present on initial page load. To fix this, add a null check after
getting the element by id 'location' and only proceed with creating and
appending the dropdownList and adding event listeners if the element is not
null, preventing script errors on pages without the 'location' element.

const regions = [
// 서울특별시 (25구)
Expand Down Expand Up @@ -133,14 +152,22 @@ <h2>오류 발생</h2>
locationInput.addEventListener('input', function () {
const query = this.value.trim().toLowerCase();
dropdownList.innerHTML = '';
if (!query) return dropdownList.style.display = 'none';
if (!query) {
dropdownList.style.display = 'none';
return;
}

const filtered = regions.filter(region => region.toLowerCase().includes(query));
if (filtered.length === 0) return dropdownList.style.display = 'none';
if (filtered.length === 0) {
dropdownList.style.display = 'none';
return;
}

filtered.forEach(region => {
const li = document.createElement('li');
li.textContent = region;
li.style.padding = '8px';
li.style.cursor = 'pointer';
li.addEventListener('click', () => {
locationInput.value = region;
dropdownList.style.display = 'none';
Expand All @@ -152,7 +179,7 @@ <h2>오류 발생</h2>
});

document.addEventListener('click', function (e) {
if (!document.querySelector('.dropdown').contains(e.target)) {
if (!locationInput.contains(e.target) && !dropdownList.contains(e.target)) {
dropdownList.style.display = 'none';
}
});
Comment on lines +182 to 185
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

전역 클릭 이벤트에서도 locationInput 존재 여부 확인 필요

위 이슈와 동일하게, 초기 페이지에서 locationInputnull 인 경우 contains 호출이 불가합니다. 아래처럼 가드를 추가해 주세요.

-  if (!locationInput.contains(e.target) && !dropdownList.contains(e.target)) {
+  if (!locationInput || (!locationInput.contains(e.target) && !dropdownList.contains(e.target))) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!locationInput.contains(e.target) && !dropdownList.contains(e.target)) {
dropdownList.style.display = 'none';
}
});
document.addEventListener('click', function(e) {
if (!locationInput || (!locationInput.contains(e.target) && !dropdownList.contains(e.target))) {
dropdownList.style.display = 'none';
}
});
🤖 Prompt for AI Agents
In src/main/resources/templates/unifiedFlow.html around lines 182 to 185, the
code calls contains on locationInput without checking if locationInput is null,
which can cause errors on initial page load. Add a guard condition to verify
locationInput exists before calling contains on it to prevent runtime
exceptions.

Expand Down