diff --git a/src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java b/src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java index 1e258b2..97a5ad6 100644 --- a/src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java +++ b/src/main/java/io/github/petty/pipeline/controller/UnifiedFlowController.java @@ -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; @@ -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 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 반환 + } +} \ No newline at end of file diff --git a/src/main/resources/static/assets/logo.png b/src/main/resources/static/assets/logo.png new file mode 100644 index 0000000..924bd46 Binary files /dev/null and b/src/main/resources/static/assets/logo.png differ diff --git a/src/main/resources/static/css/common.css b/src/main/resources/static/css/common.css new file mode 100644 index 0000000..b4bbb2a --- /dev/null +++ b/src/main/resources/static/css/common.css @@ -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; +} \ No newline at end of file diff --git a/src/main/resources/static/css/header.css b/src/main/resources/static/css/header.css new file mode 100644 index 0000000..25614bd --- /dev/null +++ b/src/main/resources/static/css/header.css @@ -0,0 +1,110 @@ +/* Header Styles */ +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 25px; + background-color: var(--card-bg-color); + box-shadow: var(--box-shadow-light); + position: sticky; + top: 0; + z-index: 1000; + border-bottom-left-radius: var(--border-radius-lg); + border-bottom-right-radius: var(--border-radius-lg); + position: relative; +} + +.logo-container { + display: flex; + align-items: center; + gap: 8px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.logo-container img { + height: 40px; + width: auto; + vertical-align: middle; +} + +header h1 { + color: var(--accent-color); + font-size: 1.8em; + margin: 0; +} + +header a { + text-decoration: none; + color: var(--secondary-text-color); + font-weight: bold; + padding: 8px 12px; + border-radius: var(--border-radius-md); + transition: background-color 0.3s ease, color 0.3s ease; +} + +header a:hover { + background-color: var(--point-color); + color: white; +} + +/* Menu Icon (Hamburger) Styles */ +.menu-icon { + width: 30px; + height: 20px; + display: flex; + flex-direction: column; + justify-content: space-between; + cursor: pointer; + z-index: 1001; + position: relative; +} + +.menu-icon span { + display: block; + width: 100%; + height: 3px; + background-color: var(--text-color); + border-radius: 2px; + transition: all 0.3s ease; +} + +/* Responsive Design for Header */ +@media (max-width: 768px) { + header { + padding: 15px 20px; + } + + .logo-container { + left: 50%; + transform: translate(-50%, -50%); + } + + .logo-container img { + height: 35px; + } + + header h1 { + font-size: 1.5em; + } + + header div[style] { /* 로그인/회원가입 메뉴 숨김 */ + display: none !important; + } +} + +@media (max-width: 480px) { + header { + padding: 10px 15px; + } + + .logo-container img { + height: 30px; + } + + header h1 { + font-size: 1.3em; + } +} \ No newline at end of file diff --git a/src/main/resources/static/css/loading.css b/src/main/resources/static/css/loading.css new file mode 100644 index 0000000..fb73c7c --- /dev/null +++ b/src/main/resources/static/css/loading.css @@ -0,0 +1,64 @@ +/* 로딩 스핀과 메시지 스타일 */ +.loading-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: calc(100vh - 120px); /* 헤더와 푸터 높이 고려 */ + text-align: center; + padding: 20px; +} + +.spinner { + border: 8px solid #f3f3f3; /* Light grey */ + border-top: 8px solid var(--accent-color); /* Pink */ + border-radius: 50%; + width: 60px; + height: 60px; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 1.8em; + color: var(--point-color); + margin-top: 10px; +} + +.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; + width: 80%; /* 로딩 페이지에 맞게 넓이 조정 */ + max-width: 600px; +} + +.result-block h2 { + color: var(--point-color); + font-size: 1.8em; + margin-top: 0; + margin-bottom: 20px; + text-align: center; +} + +.result-block 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; + min-height: 150px; /* 타이핑 텍스트가 들어갈 최소 높이 */ +} \ No newline at end of file diff --git a/src/main/resources/static/css/navbar.css b/src/main/resources/static/css/navbar.css new file mode 100644 index 0000000..24315d5 --- /dev/null +++ b/src/main/resources/static/css/navbar.css @@ -0,0 +1,41 @@ +/* Side Menu Styles */ +.side-menu { + position: fixed; + top: 0; + left: -250px; /* 초기에는 숨김 */ + width: 220px; + height: 100%; + background-color: var(--point-color); + padding-top: 80px; /* 헤더 아래로 시작 */ + box-shadow: 5px 0 15px rgba(0, 0, 0, 0.1); + transition: left 0.3s ease; + z-index: 999; + border-top-right-radius: var(--border-radius-lg); + border-bottom-right-radius: var(--border-radius-lg); + box-sizing: border-box; +} + +.side-menu.open { + left: 0; /* 메뉴 열릴 때 */ +} + +.side-menu nav { + display: flex; + flex-direction: column; + padding: 20px; +} + +.side-menu a { + color: white; /* 사이드 메뉴 텍스트는 흰색으로 고정 */ + text-decoration: none; + padding: 15px 20px; + margin-bottom: 10px; + border-radius: var(--border-radius-md); + transition: background-color 0.3s ease; + font-size: 1.1em; + font-weight: bold; +} + +.side-menu a:hover { + background-color: rgba(255, 255, 255, 0.2); +} \ No newline at end of file diff --git a/src/main/resources/static/js/flow.js b/src/main/resources/static/js/flow.js new file mode 100644 index 0000000..643afaf --- /dev/null +++ b/src/main/resources/static/js/flow.js @@ -0,0 +1,140 @@ +// 사이드 메뉴 토글 및 외부 클릭 닫기 JS +function toggleMenu(event) { + event.stopPropagation(); + const menu = document.getElementById('sideMenu'); + menu.classList.toggle('open'); +} + +function closeMenuOnClickOutside(event) { + const menu = document.getElementById('sideMenu'); + const icon = document.querySelector('.menu-icon'); // 햄버거 아이콘 + // 메뉴가 열려있고, 클릭된 요소가 메뉴 안에도 햄버거 아이콘 안에도 없을 때만 닫음 + if (menu && menu.classList.contains('open') && !menu.contains(event.target) && !icon.contains(event.target)) { + menu.classList.remove('open'); + } +} + + +// 지역 드롭다운 JS (vision_report.html에서 사용) +document.addEventListener("DOMContentLoaded", function () { + const locationInput = document.getElementById('location'); + if (locationInput) { // locationInput이 있는 페이지에서만 실행 + const dropdownContainer = locationInput.parentElement; // input을 감싸는 div + 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 = 'var(--card-bg-color)'; + dropdownList.style.border = '1px solid var(--input-border-color)'; + dropdownList.style.zIndex = 100; + dropdownList.style.display = 'none'; + dropdownContainer.appendChild(dropdownList); + + const regions = [ + // 서울특별시 (25구) + "서울특별시 강남구", "서울특별시 강동구", "서울특별시 강북구", "서울특별시 강서구", "서울특별시 관악구", "서울특별시 광진구", "서울특별시 구로구", "서울특별시 금천구", "서울특별시 노원구", + "서울특별시 도봉구", "서울특별시 동대문구", "서울특별시 동작구", "서울특별시 마포구", "서울특별시 서대문구", "서울특별시 서초구", "서울특별시 성동구", "서울특별시 성북구", + "서울특별시 송파구", "서울특별시 양천구", "서울특별시 영등포구", "서울특별시 용산구", "서울특별시 은평구", "서울특별시 종로구", "서울특별시 중구", "서울특별시 중랑구", + + // 부산광역시 (15구 1군) + "부산광역시 중구", "부산광역시 서구", "부산광역시 동구", "부산광역시 영도구", "부산광역시 부산진구", "부산광역시 동래구", "부산광역시 남구", "부산광역시 북구", "부산광역시 해운대구", + "부산광역시 사하구", "부산광역시 금정구", "부산광역시 강서구", "부산광역시 연제구", "부산광역시 수영구", "부산광역시 사상구", "부산광역시 기장군", + + // 대구광역시 (7구 2군) + "대구광역시 중구", "대구광역시 동구", "대구광역시 서구", "대구광역시 남구", "대구광역시 북구", "대구광역시 수성구", "대구광역시 달서구", "대구광역시 달성군", "대구광역시 군위군", + + // 인천광역시 (8구 2군) + "인천광역시 중구", "인천광역시 동구", "인천광역시 미추홀구", "인천광역시 연수구", "인천광역시 남동구", "인천광역시 부평구", "인천광역시 계양구", "인천광역시 서구", + "인천광역시 강화군", "인천광역시 옹진군", + + // 광주광역시 (5구) + "광주광역시 동구", "광주광역시 서구", "광주광역시 남구", "광주광역시 북구", "광주광역시 광산구", + + // 대전광역시 (5구) + "대전광역시 동구", "대전광역시 중구", "대전광역시 서구", "대전광역시 유성구", "대전광역시 대덕구", + + // 울산광역시 (4구 1군) + "울산광역시 중구", "울산광역시 남구", "울산광역시 동구", "울산광역시 북구", "울산광역시 울주군", + + // 세종특별자치시 (1) + "세종특별자치시", + + // 경기도 (28시 3군) + "경기도 수원시", "경기도 고양시", "경기도 용인시", "경기도 성남시", "경기도 부천시", "경기도 화성시", "경기도 안산시", "경기도 남양주시", "경기도 안양시", "경기도 평택시", + "경기도 시흥시", "경기도 파주시", "경기도 의정부시", "경기도 김포시", "경기도 광주시", "경기도 광명시", "경기도 군포시", "경기도 하남시", "경기도 오산시", "경기도 양주시", + "경기도 이천시", "경기도 구리시", "경기도 안성시", "경기도 포천시", "경기도 의왕시", "경기도 양평군", "경기도 여주시", "경기도 동두천시", "경기도 가평군", "경기도 과천시", + "경기도 연천군", + + // 강원특별자치도 (7시 11군) + "강원특별자치도 춘천시", "강원특별자치도 원주시", "강원특별자치도 강릉시", "강원특별자치도 동해시", "강원특별자치도 태백시", "강원특별자치도 속초시", "강원특별자치도 삼척시", + "강원특별자치도 홍천군", "강원특별자치도 횡성군", "강원특별자치도 영월군", "강원특별자치도 평창군", "강원특별자치도 정선군", "강원특별자치도 철원군", "강원특별자치도 화천군", + "강원특별자치도 양구군", "강원특별자치도 인제군", "강원특별자치도 고성군", "강원특별자치도 양양군", + + // 충청북도 (3시 8군) + "충청북도 청주시", "충청북도 충주시", "충청북도 제천시", "충청북도 보은군", "충청북도 옥천군", "충청북도 영동군", "충청북도 증평군", "충청북도 진천군", "충청북도 괴산군", + "충청북도 음성군", "충청북도 단양군", + + // 충청남도 (8시 7군) + "충청남도 천안시", "충청남도 공주시", "충청남도 보령시", "충청남도 아산시", "충청남도 서산시", "충청남도 논산시", "충청남도 계룡시", "충청남도 당진시", "충청남도 금산군", + "충청남도 부여군", "충청남도 서천군", "충청남도 청양군", "충청남도 홍성군", "충청남도 예산군", "충청남도 태안군", + + // 전북특별자치도 (6시 8군) + "전북특별자치도 전주시", "전북특별자치도 군산시", "전북특별자치도 익산시", "전북특별자치도 정읍시", "전북특별자치도 남원시", "전북특별자치도 김제시", "전북특별자치도 완주군", + "전북특별자치도 진안군", "전북특별자치도 무주군", "전북특별자치도 장수군", "전북특별자치도 임실군", "전북특별자치도 순창군", "전북특별자치도 고창군", "전북특별자치도 부안군", + + // 전라남도 (6시 17군) + "전라남도 목포시", "전라남도 여수시", "전라남도 순천시", "전라남도 나주시", "전라남도 광양시", "전라남도 담양군", "전라남도 곡성군", "전라남도 구례군", "전라남도 고흥군", + "전라남도 보성군", "전라남도 화순군", "전라남도 장흥군", "전라남도 강진군", "전라남도 해남군", "전라남도 영암군", "전라남도 무안군", "전라남도 함평군", "전라남도 영광군", + "전라남도 장성군", "전라남도 완도군", "전라남도 진도군", "전라남도 신안군", + + // 경상북도 (10시 13군) + "경상북도 포항시", "경상북도 경주시", "경상북도 김천시", "경상북도 안동시", "경상북도 구미시", "경상북도 영주시", "경상북도 영천시", "경상북도 상주시", "경상북도 문경시", + "경상북도 경산시", "경상북도 의성군", "경상북도 청송군", "경상북도 영양군", "경상북도 영덕군", "경상북도 청도군", "경상북도 고령군", "경상북도 성주군", "경상북도 칠곡군", + "경상북도 예천군", "경상북도 봉화군", "경상북도 울진군", "경상북도 울릉군", + + // 경상남도 (8시 10군) + "경상남도 창원시", "경상남도 진주시", "경상남도 통영시", "경상남도 사천시", "경상남도 김해시", "경상남도 밀양시", "경상남도 거제시", "경상남도 양산시", "경상남도 의령군", + "경상남도 함안군", "경상남도 창녕군", "경상남도 고성군", "경상남도 남해군", "경상남도 하동군", "경상남도 산청군", "경상남도 함양군", "경상남도 거창군", "경상남도 합천군", + + // 제주특별자치도 (2행정시) + "제주특별자치도 제주시", "제주특별자치도 서귀포시" + ]; + + locationInput.addEventListener('input', function () { + const query = this.value.trim().toLowerCase(); + dropdownList.innerHTML = ''; + if (!query) { + dropdownList.style.display = 'none'; + return; + } + + const filtered = regions.filter(region => region.toLowerCase().includes(query)); + 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'; + }); + dropdownList.appendChild(li); + }); + + dropdownList.style.display = 'block'; + }); + + document.addEventListener('click', function (e) { + if (!locationInput.contains(e.target) && !dropdownList.contains(e.target)) { + dropdownList.style.display = 'none'; + } + }); + } +}); \ No newline at end of file diff --git a/src/main/resources/templates/analyze.html b/src/main/resources/templates/analyze.html new file mode 100644 index 0000000..f6ae080 --- /dev/null +++ b/src/main/resources/templates/analyze.html @@ -0,0 +1,43 @@ + + + + + PETTY - 반려동물 분석 + + + + + + + +
+
+ +
+

🐾 반려동물 여행지 추천

+ +
+ + + + + + + +
+ +
+

오류 발생

+

+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/header.html b/src/main/resources/templates/fragments/header.html new file mode 100644 index 0000000..a132532 --- /dev/null +++ b/src/main/resources/templates/fragments/header.html @@ -0,0 +1,20 @@ + + + +
+ +
+ PETTY Logo +

PETTY

+
+
+ 로그인 + 회원가입 +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html new file mode 100644 index 0000000..7142f00 --- /dev/null +++ b/src/main/resources/templates/fragments/navbar.html @@ -0,0 +1,12 @@ + + + +
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/interim_loading.html b/src/main/resources/templates/interim_loading.html new file mode 100644 index 0000000..19a3483 --- /dev/null +++ b/src/main/resources/templates/interim_loading.html @@ -0,0 +1,55 @@ + + + + + PETTY - 분석 중... + + + + + + +
+
+ +
+
+
반려동물 분석 결과를 생성하고 있습니다...
+ +
+

중간 분석 결과

+

+    
+
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/recommend_loading.html b/src/main/resources/templates/recommend_loading.html new file mode 100644 index 0000000..453c165 --- /dev/null +++ b/src/main/resources/templates/recommend_loading.html @@ -0,0 +1,37 @@ + + + + + PETTY - 추천 생성 중... + + + + + + + +
+
+ +
+
+
여행지 추천을 생성하고 있습니다...
+
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/recommendation_result.html b/src/main/resources/templates/recommendation_result.html new file mode 100644 index 0000000..c8cfbe7 --- /dev/null +++ b/src/main/resources/templates/recommendation_result.html @@ -0,0 +1,35 @@ + + + + + PETTY - 추천 여행지 + + + + + + +
+
+ +
+

🐾 추천 여행지

+ +
+

추천 여행지

+

+    
+ +
+

오류 발생

+

+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/unifiedFlow.html b/src/main/resources/templates/unifiedFlow.html index 66c1911..872e163 100644 --- a/src/main/resources/templates/unifiedFlow.html +++ b/src/main/resources/templates/unifiedFlow.html @@ -1,70 +1,87 @@ - + - 통합 여행 추천 파이프라인 - + PETTY - 통합 여행 추천 파이프라인 + + + - -

🐾 반려동물 여행지 추천

- - -
- - - - - - - -
- - -
-

중간 분석 결과

-

-
- - -
-

최종 Vision 분석 보고서

-

-  
- - - -
- -
- - - - - -
-
+ + +
+ +
+ +
+

🐾 반려동물 여행지 추천

- -
-

추천 여행지

-

-
+
+ + + + + + + +
- -
-

오류 발생

-

-
+
+

중간 분석 결과

+

+  
+ +
+

최종 Vision 분석 보고서

+

+    
+ + + +
+ +
+ + + + + +
+
+ +
+

추천 여행지

+

+  
+ +
+

오류 발생

+

+
+
+ + - + \ No newline at end of file diff --git a/src/main/resources/templates/vision_report.html b/src/main/resources/templates/vision_report.html new file mode 100644 index 0000000..ddcabac --- /dev/null +++ b/src/main/resources/templates/vision_report.html @@ -0,0 +1,48 @@ + + + + + PETTY - Vision 보고서 + + + + + + +
+
+ +
+

🐾 최종 Vision 분석 보고서

+ +
+

최종 Vision 분석 보고서

+

+        
+ + + +
+ +
+ + + + + +
+
+ +
+

오류 발생

+

+
+
+ + + + + \ No newline at end of file