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,13 +6,14 @@
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;
import io.github.petty.vision.service.VisionServiceImpl; // VisionServiceImpl 사용 여부 확인 필요 (VisionUseCase와 중복될 수 있음)
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import jakarta.servlet.http.HttpSession;
import java.util.Map;
Expand All @@ -24,64 +25,154 @@
public class UnifiedFlowController {

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

@GetMapping
public String page() {
return "unifiedFlow";
// 1. 반려동물 분석 페이지 (초기 진입)
@GetMapping("/analyze")
public String analyzePage() {
return "analyze"; // analyze.html 반환
}

// 2. '내 반려동물 분석하기' 버튼 클릭 시
@PostMapping("/analyze")
public String analyze(
public String performAnalysis(
@RequestParam("file") MultipartFile file,
@RequestParam("petName") String petName,
Model model,
RedirectAttributes redirectAttributes, // RedirectAttributes 사용
HttpSession session
) {
try {
// interim 및 visionReport는 시간이 걸리는 작업이므로
// 실제 구현에서는 비동기 처리 또는 로딩 페이지에서 Ajax 호출로 처리하는 것이 일반적입니다.
// 여기서는 단순화를 위해 analyze POST 요청에서 미리 결과를 계산하고 전달합니다.

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

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

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

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

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

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

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

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

String visionReport = (String) session.getAttribute("visionReport");

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

model.addAttribute("visionReport", visionReport);
model.addAttribute("petName", petName); // petName 전달

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

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

// 프롬프트 빌딩 및 추천 서비스 호출 (시간 소요)
// 실제 구현에서는 비동기 처리 또는 로딩 페이지에서 Ajax 호출로 처리하는 것이 일반적입니다.
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);
// 추천 결과는 세션에 저장
session.setAttribute("recommendationResult", recommendation);

// 다음 페이지로 필요한 데이터 전달
redirectAttributes.addFlashAttribute("petName", petName);
redirectAttributes.addFlashAttribute("location", location);
redirectAttributes.addFlashAttribute("info", info); // info도 전달 (로깅 등 필요할 경우)

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

// 6. 여행지 추천 로딩 페이지 (recommend_loading.html)
@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 반환
}

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

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

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

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

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

return "recommendation_result"; // recommendation_result.html 반환
}
}
Binary file added src/main/resources/static/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
189 changes: 189 additions & 0 deletions src/main/resources/static/css/common.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/* 폰트 정의 */
@font-face {
font-family: 'HakgyoansimDunggeunmisoTTF-B';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2408-5@1.0/HakgyoansimDunggeunmisoTTF-B.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}

/* 공통 CSS 변수 */
:root {
--background-color: #FFF0DC; /* 전체 배경색 */
--text-color: #4B352A; /* 기본 텍스트 색상 */
--point-color: #9EBC8A; /* 주요 강조 색상 (버튼, 푸터, 사이드 메뉴 등) */
--accent-color: #D76C82; /* 엑센트 색상 (로고, 제목 등) */
--secondary-text-color: #7A5B4C; /* 보조 텍스트 색상 */
--card-bg-color: #FFFFFF; /* 카드/폼 배경색 */
--input-border-color: #D3B8AE; /* 입력 필드 테두리 색상 */
--button-hover-color: #c45b73; /* 버튼 호버 색상 */
--border-radius-lg: 20px; /* 큰 둥근 모서리 */
--border-radius-md: 12px; /* 중간 둥근 모서리 */
--border-radius-sm: 8px; /* 작은 둥근 모서리 (인풋, 버튼) */
--box-shadow-light: 0 4px 15px rgba(0, 0, 0, 0.08);
--box-shadow-cute: 0 8px 20px rgba(215, 108, 130, 0.2);
}

body {
font-family: 'HakgyoansimDunggeunmisoTTF-B', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 0;
line-height: 1.6;
overflow-x: hidden;
display: flex;
flex-direction: column;
min-height: 100vh;
}

/* Main Content Area Styles */
main {
flex-grow: 1;
padding: 40px 25px;
max-width: 800px;
margin: 20px auto;
box-sizing: border-box;
}

h1 {
color: var(--accent-color);
font-size: 2.5em;
text-align: center;
margin-bottom: 40px;
margin-top: 20px;
}

form, .result-block {
background: var(--card-bg-color);
padding: 25px;
border-radius: var(--border-radius-lg);
box-shadow: var(--box-shadow-light);
margin-bottom: 30px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}

form:hover, .result-block:hover {
transform: translateY(-5px);
box-shadow: var(--box-shadow-cute);
}

label {
display: block;
margin-bottom: 8px;
margin-top: 15px;
font-weight: bold;
color: var(--secondary-text-color);
}

input[type=text],
input[type=file],
input[type=email],
textarea {
width: calc(100% - 22px);
padding: 10px;
margin-top: 5px;
border-radius: var(--border-radius-sm);
border: 1px solid var(--input-border-color);
box-sizing: border-box;
font-family: 'HakgyoansimDunggeunmisoTTF-B', sans-serif;
font-size: 1em;
color: var(--text-color);
}

input[type=text]:focus,
input[type=file]:focus,
textarea:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 5px rgba(215, 108, 130, 0.5);
}

input[type=submit],
button {
padding: 12px 25px;
background: var(--accent-color);
color: white;
border-radius: 30px;
cursor: pointer;
border: none;
font-family: 'HakgyoansimDunggeunmisoTTF-B', sans-serif;
font-size: 1.1em;
margin-top: 25px;
transition: background-color 0.3s ease, transform 0.2s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
}

input[type=submit]:hover,
button:hover {
background: var(--button-hover-color);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}

h2 {
color: var(--point-color);
font-size: 1.8em;
margin-top: 0;
margin-bottom: 20px;
text-align: center;
}

pre {
background: var(--background-color);
padding: 15px;
border-radius: var(--border-radius-sm);
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
border: 1px solid var(--input-border-color);
color: var(--text-color);
font-family: monospace;
font-size: 0.95em;
}

.dropdown-list {
list-style: none;
padding: 0;
margin: 0;
border-top: none;
border-bottom-left-radius: var(--border-radius-sm);
border-bottom-right-radius: var(--border-radius-sm);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

.dropdown-list li {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #eee;
color: var(--text-color);
font-family: 'HakgyoansimDunggeunmisoTTF-B', sans-serif;
}

.dropdown-list li:last-child {
border-bottom: none;
}

.dropdown-list li:hover {
background-color: var(--background-color);
color: var(--accent-color);
}

/* Footer Styles */
footer {
background-color: var(--point-color);
color: white;
text-align: center;
padding: 30px 20px;
margin-top: auto;
border-top-left-radius: var(--border-radius-lg);
border-top-right-radius: var(--border-radius-lg);
font-size: 1.1em;
line-height: 1.8;
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.08);
}

footer .paw {
font-size: 1.3em;
color: var(--accent-color);
margin: 0 5px;
}
Loading