-
Notifications
You must be signed in to change notification settings - Fork 6
[PIPELINE] 분석, 보고서, 추천 진행 및 결과물 출력 #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
be32aab
169b90f
1b8bb9d
99e42db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
||
|
|
@@ -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); | ||
|
|
||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
🤖 Prompt for AI Agents |
||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
간단한 방어 코드로 예방해 주세요. - 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const regions = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 서울특별시 (25구) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전역 클릭 이벤트에서도 위 이슈와 동일하게, 초기 페이지에서 - if (!locationInput.contains(e.target) && !dropdownList.contains(e.target)) {
+ if (!locationInput || (!locationInput.contains(e.target) && !dropdownList.contains(e.target))) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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, …)에 다시 전달하여 이중 메모리 사용이 발생합니다.VisionServiceImpl에 byte 배열 입력을 받는 오버로드를 추가하면 효율적입니다.🤖 Prompt for AI Agents