Skip to content

Commit f0550b4

Browse files
committed
refact: index관련 fragment조회 및 AJAX 호출
1 parent 0eaeff1 commit f0550b4

2 files changed

Lines changed: 93 additions & 36 deletions

File tree

src/main/java/com/knoc/global/controller/IndexController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,13 @@ public String index(@ModelAttribute SeniorSearchCondition condition, Model model
3535
model.addAttribute("tossClientKey", tossClientKey);
3636
return "index";
3737
}
38+
39+
@Operation(summary = "시니어 목록 fragment 조회", description = "검색 조건에 따른 시니어 목록을 HTML fragment으로 반환 / AJAX 필터링에 사용한다.")
40+
@GetMapping("/seniors/fragment")
41+
public String searchFragment(@ModelAttribute SeniorSearchCondition condition, Model model) {
42+
model.addAttribute("seniors", seniorProfileService.searchProfiles(condition));
43+
model.addAttribute("condition", condition);
44+
model.addAttribute("popularSkills", POPULAR_SKILLS);
45+
return "index :: seniorSection"; // 시니어 섹션 fragment만 반환
46+
}
3847
}

src/main/resources/templates/index.html

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ <h1 class="display-5 fw-bold mb-4">성장을 이끌어줄 현업 <span class="te
2020
</p>
2121

2222
<!-- 키워드 검색 -->
23-
<form th:action="@{/}" method="get" id="searchForm">
23+
<form id="searchForm" onsubmit="event.preventDefault(); loadSeniors();">
2424

2525
<!-- ── 검색창 + 토글 + 검색버튼 ──────────────── -->
2626
<div class="row justify-content-center mb-3">
@@ -62,8 +62,8 @@ <h1 class="display-5 fw-bold mb-4">성장을 이끌어줄 현업 <span class="te
6262
<!-- 헤더 -->
6363
<div class="d-flex justify-content-between align-items-center mb-4">
6464
<span class="fw-semibold text-white">상세 필터</span>
65-
<a th:href="@{/}" class="text-secondary text-decoration-none"
66-
style="font-size:0.83rem;">초기화</a>
65+
<a href="#" class="text-secondary text-decoration-none"
66+
style="font-size:0.83rem;" onclick="resetFilters()">초기화</a>
6767
</div>
6868

6969
<div class="row g-4">
@@ -116,7 +116,7 @@ <h1 class="display-5 fw-bold mb-4">성장을 이끌어줄 현업 <span class="te
116116

117117
<!-- 적용 버튼 -->
118118
<div class="d-flex justify-content-end mt-4">
119-
<button type="submit" class="btn btn-mint rounded-pill px-4 fw-bold">적용</button>
119+
<button type="button" class="btn btn-mint rounded-pill px-4 fw-bold" onclick="loadSeniors()">적용</button>
120120
</div>
121121
</div>
122122
</div>
@@ -127,10 +127,13 @@ <h1 class="display-5 fw-bold mb-4">성장을 이끌어줄 현업 <span class="te
127127
<div class="d-flex justify-content-center gap-2 flex-wrap mt-2">
128128
<span class="text-secondary me-1" style="font-size:0.9rem;">🔥 인기 기술스택</span>
129129
<th:block th:each="s : ${popularSkills}">
130-
<a th:href="@{/(skill=${s}, careerYears=${condition.careerYears}, maxPrice=${condition.maxPrice}, keyword=${condition.keyword})}"
131-
class="badge rounded-pill py-2 px-3 text-decoration-none"
132-
th:classappend="${condition.skill == s} ? ' bg-mint text-dark' : ' badge-dark'"
133-
th:text="${s}"></a>
130+
<button type="button"
131+
class="badge rounded-pill py-2 px-3 border-0"
132+
th:classappend="${condition.skill == s} ? ' bg-mint text-dark' : ' badge-dark'"
133+
th:attr="data-skill=${s}"
134+
th:text="${s}"
135+
onclick="filterBySkill(this.dataset.skill)">
136+
</button>
134137
</th:block>
135138
</div>
136139

@@ -142,57 +145,102 @@ <h1 class="display-5 fw-bold mb-4">성장을 이끌어줄 현업 <span class="te
142145
<script th:inline="javascript">
143146
const MAX_PRICE = 200000;
144147

148+
// 페이지 로드 시 Thymeleaf에서 현재 선택된 skill 값을 초기화
149+
let currentSkill = /*[[${condition.skill}]]*/ '';
150+
151+
// ── 슬라이더 표시 업데이트 ───────────────────────────
145152
function updateCareerDisplay(val) {
146153
const v = parseInt(val);
147-
const display = document.getElementById('careerDisplay');
148-
if (v === 0) {
149-
display.textContent = '전체';
150-
document.getElementById('careerSlider').name = '';
151-
} else {
152-
display.textContent = v + '년 이상';
153-
document.getElementById('careerSlider').name = 'careerYears';
154-
}
154+
document.getElementById('careerDisplay').textContent = v === 0 ? '전체' : v + '년 이상';
155155
}
156156

157157
function updatePriceDisplay(val) {
158158
const v = parseInt(val);
159-
const display = document.getElementById('priceDisplay');
160-
if (v >= MAX_PRICE) {
161-
display.textContent = '제한 없음';
162-
document.getElementById('priceSlider').name = '';
163-
} else {
164-
display.textContent = '₩' + v.toLocaleString('ko-KR') + ' 이하';
165-
document.getElementById('priceSlider').name = 'maxPrice';
166-
}
159+
document.getElementById('priceDisplay').textContent =
160+
v >= MAX_PRICE ? '제한 없음' : '₩' + v.toLocaleString('ko-KR') + ' 이하';
161+
}
162+
163+
// ── 현재 필터 값을 쿼리 파라미터로 수집 ─────────────
164+
function getFilterParams() {
165+
const keyword = document.querySelector('input[name="keyword"]').value;
166+
const career = parseInt(document.getElementById('careerSlider').value);
167+
const price = parseInt(document.getElementById('priceSlider').value);
168+
169+
const params = new URLSearchParams();
170+
if (keyword) params.set('keyword', keyword);
171+
if (career > 0) params.set('careerYears', career);
172+
if (price < MAX_PRICE) params.set('maxPrice', price);
173+
if (currentSkill) params.set('skill', currentSkill);
174+
return params;
175+
}
176+
177+
// ── AJAX로 시니어 목록만 교체 ────────────────────────
178+
function loadSeniors() {
179+
const params = getFilterParams();
180+
history.pushState(null, '', '/?' + params.toString());
181+
182+
fetch('/seniors/fragment?' + params.toString())
183+
.then(res => res.text())
184+
.then(html => {
185+
document.getElementById('seniorSection').outerHTML = html;
186+
})
187+
.catch(err => console.error('시니어 목록 로딩 실패:', err));
188+
}
189+
190+
// ── 기술스택 배지 토글 ───────────────────────────────
191+
function filterBySkill(skill) {
192+
currentSkill = (currentSkill === skill) ? '' : skill;
193+
194+
document.querySelectorAll('[data-skill]').forEach(btn => {
195+
const isActive = btn.dataset.skill === currentSkill;
196+
btn.classList.toggle('bg-mint', isActive);
197+
btn.classList.toggle('text-dark', isActive);
198+
btn.classList.toggle('badge-dark', !isActive);
199+
});
200+
201+
loadSeniors();
202+
}
203+
204+
// ── 필터 전체 초기화 ─────────────────────────────────
205+
function resetFilters() {
206+
document.querySelector('input[name="keyword"]').value = '';
207+
document.getElementById('careerSlider').value = 0;
208+
document.getElementById('priceSlider').value = MAX_PRICE;
209+
updateCareerDisplay(0);
210+
updatePriceDisplay(MAX_PRICE);
211+
currentSkill = '';
212+
document.querySelectorAll('[data-skill]').forEach(btn => {
213+
btn.classList.remove('bg-mint', 'text-dark');
214+
btn.classList.add('badge-dark');
215+
});
216+
loadSeniors();
167217
}
168218

169219
document.addEventListener('DOMContentLoaded', function () {
170220
const careerSlider = document.getElementById('careerSlider');
171-
updateCareerDisplay(careerSlider.value);
172-
careerSlider.addEventListener('change', function () {
173-
document.getElementById('searchForm').submit();
174-
});
221+
const priceSlider = document.getElementById('priceSlider');
175222

176-
const priceSlider = document.getElementById('priceSlider');
223+
updateCareerDisplay(careerSlider.value);
177224
updatePriceDisplay(priceSlider.value);
178-
priceSlider.addEventListener('change', function () {
179-
document.getElementById('searchForm').submit();
180-
});
225+
226+
// 슬라이더 조작 완료 시 AJAX 조회
227+
careerSlider.addEventListener('change', loadSeniors);
228+
priceSlider.addEventListener('change', loadSeniors);
181229

182230
// 필터가 적용된 상태면 패널 자동 열기
183231
const hasFilter = parseInt(careerSlider.value) > 0
184-
|| parseInt(priceSlider.value) < MAX_PRICE;
232+
|| parseInt(priceSlider.value) < MAX_PRICE
233+
|| currentSkill !== '';
185234
if (hasFilter) {
186-
const panel = document.getElementById('filterPanel');
187-
new bootstrap.Collapse(panel, { toggle: false }).show();
235+
new bootstrap.Collapse(document.getElementById('filterPanel'), { toggle: false }).show();
188236
document.getElementById('filterToggleBtn').classList.add('active');
189237
}
190238
});
191239
</script>
192240
</th:block>
193241

194242
<!-- ── 시니어 목록 섹션 ──────────────────────────────── -->
195-
<section class="container py-5">
243+
<section class="container py-5" id="seniorSection" th:fragment="seniorSection">
196244
<div class="d-flex justify-content-between align-items-end mb-4">
197245
<div>
198246
<h3 class="fw-bold">당신에게 딱 맞는 시니어 멘토</h3>

0 commit comments

Comments
 (0)