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 @@ -6,7 +6,7 @@
import io.github.petty.llm.service.RecommendService;
import io.github.petty.pipeline.support.TogetherPromptBuilder;
import io.github.petty.vision.port.in.VisionUseCase;
import io.github.petty.vision.service.VisionServiceImpl; // VisionServiceImpl 사용 여부 확인 필요 (VisionUseCase와 중복될 수 있음)
import io.github.petty.vision.service.VisionServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
Expand All @@ -25,18 +25,13 @@
public class UnifiedFlowController {

private final VisionUseCase visionUseCase;
private final VisionServiceImpl visionService; // 이 서비스가 VisionUseCase의 구현체라면, 하나만 사용하거나 역할을 명확히 해야 합니다.
private final VisionServiceImpl visionService;
private final TogetherPromptBuilder togetherPromptBuilder;
private final RecommendService recommendService;

// 1. 반려동물 분석 페이지 (초기 진입)
@GetMapping("/analyze")
public String analyzePage(HttpSession session) {
// 새로운 분석 시작 시 이전 세션 데이터 정리
log.info("새로운 분석 시작 - 이전 세션 데이터 정리");
session.removeAttribute("recommendationResult");
session.removeAttribute("visionReport");
session.removeAttribute("lastAccessTime");
public String analyzePage() {
return "analyze"; // analyze.html 반환
}

Expand All @@ -45,90 +40,96 @@ public String analyzePage(HttpSession session) {
public String performAnalysis(
@RequestParam("file") MultipartFile file,
@RequestParam("petName") String petName,
RedirectAttributes redirectAttributes, // RedirectAttributes 사용
RedirectAttributes redirectAttributes,
HttpSession session
) {
try {
// 세션 한 번 더 제거
session.removeAttribute("recommendationResult");
session.removeAttribute("visionReport");
session.removeAttribute("lastAccessTime");
// interim 및 visionReport는 시간이 걸리는 작업이므로
// 실제 구현에서는 비동기 처리 또는 로딩 페이지에서 Ajax 호출로 처리하는 것이 일반적입니다.
// 여기서는 단순화를 위해 analyze POST 요청에서 미리 결과를 계산하고 전달합니다.

String interim = visionUseCase.interim(file.getBytes(), petName);
String visionReport = visionService.analyze(file, petName); // visionService.analyze 역할 확인 필요
String visionReport = visionService.analyze(file, petName);

// 다음 페이지로 flash attribute로 전달 (URL에 노출되지 않음)
// 다음 페이지로 flash attribute로 전달
redirectAttributes.addFlashAttribute("interim", interim);
redirectAttributes.addFlashAttribute("petName", petName);

// 최종 visionReport는 세션에 저장하여 다음 단계에서 재사용
session.setAttribute("visionReport", visionReport);
session.setAttribute("petName", petName);

// 이미지를 Base64로 인코딩하여 세션에 저장
try {
byte[] imageBytes = file.getBytes();
String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
} catch (Exception e) {
log.warn("이미지 저장 실패", e);
}
Comment on lines +59 to +65
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

이미지 인코딩 로직이 중복되어 있습니다.

VisionController의 /species 엔드포인트에서 이미 이미지를 Base64로 인코딩하여 세션에 저장하고 있습니다. 여기서 다시 동일한 작업을 수행하는 것은 불필요한 중복입니다.

이미 세션에 저장된 이미지를 재사용하거나, 공통 유틸리티 메서드로 추출하는 것을 권장합니다.

-            // 이미지를 Base64로 인코딩하여 세션에 저장
-            try {
-                byte[] imageBytes = file.getBytes();
-                String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
-                session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
-            } catch (Exception e) {
-                log.warn("이미지 저장 실패", e);
-            }
+            // VisionController에서 이미 저장한 이미지가 없는 경우에만 저장
+            if (session.getAttribute("petImageBase64") == null) {
+                try {
+                    byte[] imageBytes = file.getBytes();
+                    String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
+                    session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
+                } catch (Exception e) {
+                    log.error("이미지 저장 실패", e);
+                    throw new RuntimeException("이미지 처리 중 오류가 발생했습니다.", e);
+                }
+            }
📝 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
try {
byte[] imageBytes = file.getBytes();
String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
} catch (Exception e) {
log.warn("이미지 저장 실패", e);
}
// VisionController에서 이미 저장한 이미지가 없는 경우에만 저장
if (session.getAttribute("petImageBase64") == null) {
try {
byte[] imageBytes = file.getBytes();
String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
} catch (Exception e) {
log.error("이미지 저장 실패", e);
throw new RuntimeException("이미지 처리 중 오류가 발생했습니다.", e);
}
}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java
around lines 59 to 65, the image Base64 encoding logic is duplicated from
VisionController's /species endpoint. To fix this, remove the redundant encoding
here and instead reuse the Base64 image string already stored in the session.
Alternatively, extract the encoding logic into a shared utility method and call
that method from both controllers to avoid duplication.


return "redirect:/flow/showInterimLoading"; // 중간 분석 로딩 페이지로 리다이렉트
return "redirect:/flow/showInterimLoading";
} catch (Exception e) {
log.error("❌ 반려동물 분석 중 오류", e);
redirectAttributes.addFlashAttribute("error", "반려동물 분석 중 오류 발생: " + e.getMessage());
return "redirect:/flow/analyze"; // 오류 발생 시 다시 분석 페이지로
return "redirect:/flow/analyze";
}
}

// 3. 중간 분석 로딩 페이지 (interim_loading.html)
// 3. 중간 분석 로딩 페이지
@GetMapping("/showInterimLoading")
public String showInterimLoading(
@ModelAttribute("interim") String interim, // FlashAttribute로 받은 interim
@ModelAttribute("petName") String petName, // FlashAttribute로 받은 petName
@ModelAttribute("interim") String interim,
@ModelAttribute("petName") String petName,
Model model) {

// interim이 flash attribute로 전달되지 않은 경우 (예: 새로고침)
if (interim == null || interim.isEmpty()) {
model.addAttribute("interim", "데이터를 불러오는 중이거나, 이전 요청이 완료되지 않았습니다. 잠시만 기다려 주세요.");
}
model.addAttribute("petName", petName); // petName도 전달
model.addAttribute("petName", petName);

return "interim_loading"; // interim_loading.html 반환
return "interim_loading";
}

// 4. 최종 Vision 보고서 페이지 (vision_report.html) - interim_loading.html의 JS에서 호출됨
// 4. 최종 Vision 보고서 페이지 - 세션 기반으로 수정
@GetMapping("/showVisionReport")
public String showVisionReport(Model model, HttpSession session,
@ModelAttribute("petName") String petName) { // petName 전달 받기

public String showVisionReport(Model model, HttpSession session) {
String visionReport = (String) session.getAttribute("visionReport");
String petName = (String) session.getAttribute("petName");
String petImageBase64 = (String) session.getAttribute("petImageBase64");

if (visionReport == null) {
log.warn("⚠️ 세션에 Vision 보고서가 없습니다");
model.addAttribute("error", "세션에 Vision 보고서가 없습니다. 다시 분석을 시작해 주세요.");
return "analyze"; // Vision 보고서 없으면 분석 시작 페이지로
return "visionUpload"; // vision 업로드 페이지로 이동
}

model.addAttribute("visionReport", visionReport);
model.addAttribute("petName", petName); // petName 전달
model.addAttribute("petName", petName);
if (petImageBase64 != null) {
model.addAttribute("petImageUrl", petImageBase64);
}

log.info("✅ Vision 보고서 표시 - petName: {}, reportLength: {}",
petName, visionReport.length());

return "vision_report"; // vision_report.html 반환
return "vision_report";
}

// 5. '여행지 추천 받기' 버튼 클릭 시 (vision_report.html에서 POST 요청)
// 5. '여행지 추천 받기' 버튼 클릭 시
@PostMapping("/report")
public String generateRecommendation(
@RequestParam("petName") String petName,
@RequestParam("location") String location,
@RequestParam("info") String info,
// @RequestParam("is_danger") String isDanger,
RedirectAttributes redirectAttributes,
HttpSession session
) {
try {
// 세션에서 visionReport 가져오기
String visionReport = (String) session.getAttribute("visionReport");
if (visionReport == null) {
log.warn("⚠️ 세션에 Vision 보고서가 없습니다 - petName: {}", petName);
redirectAttributes.addFlashAttribute("error", "세션에 Vision 보고서가 없습니다. 다시 분석을 시작해 주세요.");
return "redirect:/flow/analyze"; // Vision 보고서 없으면 분석 시작 페이지로
return "redirect:/flow/analyze";
}

// 프롬프트 빌딩 및 추천 서비스 호출 (시간 소요)
// 실제 구현에서는 비동기 처리 또는 로딩 페이지에서 Ajax 호출로 처리하는 것이 일반적입니다.
// 프롬프트 빌딩 및 추천 서비스 호출
String jsonPrompt = togetherPromptBuilder.buildPrompt(visionReport, location, info);
Map<String, String> promptMapper = new ObjectMapper().readValue(jsonPrompt, new TypeReference<>() {});
RecommendResponseDTO recommendation = recommendService.recommend(promptMapper);
Expand All @@ -139,50 +140,52 @@ public String generateRecommendation(
// 다음 페이지로 필요한 데이터 전달
redirectAttributes.addFlashAttribute("petName", petName);
redirectAttributes.addFlashAttribute("location", location);
redirectAttributes.addFlashAttribute("info", info); // info도 전달 (로깅 등 필요할 경우)
redirectAttributes.addFlashAttribute("info", info);

return "redirect:/flow/showRecommendLoading"; // 추천 로딩 페이지로 리다이렉트
log.info("✅ 추천 생성 완료 - petName: {}, location: {}", petName, location);

return "redirect:/flow/showRecommendLoading";
} catch (Exception e) {
log.error("❌ 추천 생성 중 오류", e);
redirectAttributes.addFlashAttribute("error", "추천 생성 중 오류 발생: " + e.getMessage());
// 오류 발생 시 다시 Vision 보고서 페이지로 (데이터는 세션에서 가져와야 함)
return "redirect:/flow/showVisionReport";
}
}

// 6. 여행지 추천 로딩 페이지 (recommend_loading.html)
// 6. 여행지 추천 로딩 페이지
@GetMapping("/showRecommendLoading")
public String showRecommendLoading(
@ModelAttribute("petName") String petName,
@ModelAttribute("location") String location,
@ModelAttribute("info") String info,
Model model) {
// 로딩 메시지만 보여주는 페이지

model.addAttribute("petName", petName);
model.addAttribute("location", location);
model.addAttribute("info", info);
return "recommend_loading"; // recommend_loading.html 반환
return "recommend_loading";
}

// 7. 추천 여행지 결과 페이지 (recommendation_result.html) - recommend_loading.html의 JS에서 호출됨
// 7. 추천 여행지 결과 페이지
@GetMapping("/showRecommendationResult")
public String showRecommendationResult(Model model, HttpSession session) {

RecommendResponseDTO recommendation = (RecommendResponseDTO) session.getAttribute("recommendationResult");

if (recommendation == null) {
log.warn("⚠️ 세션에 추천 여행지 결과가 없습니다");
model.addAttribute("error", "세션에 추천 여행지 결과가 없습니다. 다시 시도해 주세요.");
return "analyze"; // 결과 없으면 분석 시작 페이지로
return "visionUpload";
}

model.addAttribute("recommendation", recommendation);
// 필요에 따라 petName, location 등도 세션에서 가져와 모델에 추가할 수 있습니다.
// model.addAttribute("petName", session.getAttribute("petName"));
model.addAttribute("recommendationResponse", recommendation); // 템플릿 호환성

// 사용 후 세션에서 제거 (선택 사항)
session.removeAttribute("recommendationResult");
session.removeAttribute("visionReport");

// // 사용 후 세션에서 제거 (선택 사항, 메모리 관리)
// session.removeAttribute("recommendationResult");
// session.removeAttribute("visionReport");
log.info("✅ 추천 결과 표시 완료");

return "recommendation_result"; // recommendation_result.html 반환
return "recommendation_result";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
import io.github.petty.vision.helper.ImageValidator;
import io.github.petty.vision.helper.ImageValidator.ValidationResult;
import io.github.petty.vision.port.in.VisionUseCase;
import io.github.petty.vision.service.VisionServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.http.HttpStatus;

import jakarta.servlet.http.HttpSession;
import java.io.IOException;

@Slf4j
@Controller
@RequestMapping("/vision")
@RequiredArgsConstructor
public class VisionController {
private final VisionUseCase vision;
private final VisionServiceImpl visionService;
Comment on lines 23 to +24
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

아키텍처 문제: 추상화와 구현체를 동시에 의존하고 있습니다.

컨트롤러가 VisionUseCase(추상화)와 VisionServiceImpl(구현체)를 모두 의존하는 것은 의존성 역전 원칙(DIP)을 위반합니다. 이는 결합도를 높이고 테스트를 어렵게 만듭니다.

VisionUseCase 인터페이스에 필요한 모든 메서드를 정의하고, 컨트롤러는 인터페이스만 의존하도록 리팩토링하는 것을 권장합니다.

🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/adapter/in/VisionController.java at
lines 23-24, the controller depends on both the abstraction VisionUseCase and
the concrete implementation VisionServiceImpl, violating the dependency
inversion principle. Remove the dependency on VisionServiceImpl and ensure the
controller only depends on the VisionUseCase interface. Refactor the code to use
VisionUseCase exclusively for all required methods, improving modularity and
testability.

private final ImageValidator imageValidator;

@GetMapping("/upload")
Expand All @@ -28,14 +33,30 @@ public String page() {
@ResponseBody
public String getSpeciesInterim(
@RequestParam("file") MultipartFile file,
@RequestParam("petName") String petName
@RequestParam("petName") String petName,
HttpSession session
) throws IOException {
// 파일 유효성 검사
ValidationResult vr = imageValidator.validate(file);
if (!vr.isValid()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage());
}

// UnifiedFlowController와 호환되도록 세션에 데이터 저장
session.setAttribute("petName", petName);

// 파일을 임시로 저장 (나중에 analyze에서 사용)
try {
byte[] imageBytes = file.getBytes();
session.setAttribute("tempImageBytes", imageBytes);

// 이미지를 Base64로 인코딩하여 세션에 저장
String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
} catch (IOException e) {
log.warn("이미지 저장 실패", e);
}
Comment on lines +45 to +58
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

메모리 사용량 최적화가 필요합니다.

현재 구현은 동일한 이미지를 두 가지 형태(원본 바이트와 Base64 문자열)로 세션에 저장하고 있어 메모리를 비효율적으로 사용합니다. 대용량 이미지의 경우 세션 메모리 부족 문제가 발생할 수 있습니다.

다음 중 하나의 방법을 고려해보세요:

  1. Base64 인코딩된 데이터만 저장하고 필요시 디코딩
  2. 파일 시스템이나 캐시 서버에 임시 저장 후 참조 ID만 세션에 저장
  3. 이미지 크기 제한 설정
-        // 파일을 임시로 저장 (나중에 analyze에서 사용)
-        try {
-            byte[] imageBytes = file.getBytes();
-            session.setAttribute("tempImageBytes", imageBytes);
-
-            // 이미지를 Base64로 인코딩하여 세션에 저장
-            String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
-            session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
-        } catch (IOException e) {
-            log.warn("이미지 저장 실패", e);
-        }
+        // 이미지를 Base64로 인코딩하여 세션에 저장 (메모리 효율성을 위해 한 번만 저장)
+        try {
+            byte[] imageBytes = file.getBytes();
+            String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
+            session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
+        } catch (IOException e) {
+            log.warn("이미지 저장 실패", e);
+            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 처리 중 오류가 발생했습니다.");
+        }
📝 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
// UnifiedFlowController와 호환되도록 세션에 데이터 저장
session.setAttribute("petName", petName);
// 파일을 임시로 저장 (나중에 analyze에서 사용)
try {
byte[] imageBytes = file.getBytes();
session.setAttribute("tempImageBytes", imageBytes);
// 이미지를 Base64로 인코딩하여 세션에 저장
String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
} catch (IOException e) {
log.warn("이미지 저장 실패", e);
}
// UnifiedFlowController와 호환되도록 세션에 데이터 저장
session.setAttribute("petName", petName);
// 이미지를 Base64로 인코딩하여 세션에 저장 (메모리 효율성을 위해 한 번만 저장)
try {
byte[] imageBytes = file.getBytes();
String imageBase64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
session.setAttribute("petImageBase64", "data:image/jpeg;base64," + imageBase64);
} catch (IOException e) {
log.warn("이미지 저장 실패", e);
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR,
"이미지 처리 중 오류가 발생했습니다."
);
}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/vision/adapter/in/VisionController.java around
lines 45 to 58, the code stores the same image in the session both as raw bytes
and as a Base64 string, causing inefficient memory use. To fix this, choose one
approach: either store only the Base64-encoded string in the session and decode
it when needed, or save the image temporarily on the file system or a cache
server and store only a reference ID in the session. Additionally, consider
implementing an image size limit to prevent excessive memory consumption.


// 기존 서비스 호출
return vision.interim(file.getBytes(), petName);
}
Expand All @@ -44,15 +65,30 @@ public String getSpeciesInterim(
@ResponseBody
public String analyze(
@RequestParam("file") MultipartFile file,
@RequestParam("petName") String petName
@RequestParam("petName") String petName,
HttpSession session
) {
// 파일 유효성 검사
ValidationResult vr = imageValidator.validate(file);
if (!vr.isValid()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, vr.getMessage());
}

// 기존 서비스 호출
return vision.analyze(file, petName);
try {
// Vision 분석 결과 생성
String visionReport = visionService.analyze(file, petName);

// UnifiedFlowController와 호환되도록 세션에 결과 저장
session.setAttribute("visionReport", visionReport);
session.setAttribute("petName", petName);

log.info("✅ Vision 분석 완료 - petName: {}, reportLength: {}",
petName, visionReport != null ? visionReport.length() : 0);

return visionReport;
} catch (Exception e) {
log.error("❌ Vision 분석 실패", e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "분석 중 오류가 발생했습니다: " + e.getMessage());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private String detailedPrompt(String pet, String species) {
- 감정·행동
- 기타 특이사항(목줄·배경 등)

보호자가 이해하기 쉬운 문장으로 요약해줘.
보호자가 이해하기 쉬운 문장과 사용자 친화적으로 요약해줘.
""",
pet,
species == null || species.isBlank() ? "알 수 없음" : species
Expand Down
Loading