From b950c5384832b2c1a80ca6fb00ab831b30f35348 Mon Sep 17 00:00:00 2001 From: Junha Date: Wed, 7 May 2025 14:07:25 +0900 Subject: [PATCH 01/18] basic html, css, js --- .github/workflows/deploy.yml | 3 +- build.gradle | 1 + .../controller/ViewController.java | 36 ++++++++ src/main/resources/static/css/style.css | 86 +++++++++++++++++++ src/main/resources/static/index.html | 10 --- src/main/resources/static/js/match.js | 62 +++++++++++++ src/main/resources/templates/index.html | 59 +++++++++++++ 7 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 src/main/java/futsal/futsalMatch/controller/ViewController.java create mode 100644 src/main/resources/static/css/style.css delete mode 100644 src/main/resources/static/index.html create mode 100644 src/main/resources/static/js/match.js create mode 100644 src/main/resources/templates/index.html diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5c11f3d..8e39034 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,8 +3,7 @@ name: Deploy To EC2 on: push: branches: - - main - - improve/performance + - integrate-fe-be jobs: deploy: diff --git a/build.gradle b/build.gradle index d0272b9..1523775 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation group: 'org.json', name: 'json', version: '20231013' implementation 'org.apache.httpcomponents.client5:httpclient5:5.2' implementation 'org.jsoup:jsoup:1.18.1' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/futsal/futsalMatch/controller/ViewController.java b/src/main/java/futsal/futsalMatch/controller/ViewController.java new file mode 100644 index 0000000..16d431f --- /dev/null +++ b/src/main/java/futsal/futsalMatch/controller/ViewController.java @@ -0,0 +1,36 @@ +package futsal.futsalMatch.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.time.LocalDate; +import java.time.format.TextStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +@Controller +public class ViewController { + + @GetMapping("/") + public String index(Model model) { + LocalDate today = LocalDate.now(); + List> days = new ArrayList<>(); + + for (int i = 0; i < 14; i++) { + LocalDate date = today.plusDays(i); + String dayOfTheWeek = date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.KOREAN).substring(0, 1); + + days.add(Map.of( + "date", date.toString(), // yyyy-MM-dd + "day-date", String.valueOf(date.getDayOfMonth()), + "day-week", dayOfTheWeek + )); + } + + model.addAttribute("days", days); + return "index"; + } +} diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 0000000..b7c2a18 --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,86 @@ +/* 날짜 네비게이션 바 전체 컨테이너 */ +.date-nav-container { + display: flex; /* 가로 방향으로 아이템 배치 (화살표 + 날짜 버튼) */ + align-items: center; /* 수직 중앙 정렬 */ + justify-content: space-between; /* 좌우 화살표는 고정, 가운데 날짜 영역은 유동 */ + max-width: 1000px; /* 전체 최대 폭 제한 */ + margin: 0 auto 16px auto; /* 가운데 정렬 + 아래 여백 */ + padding: 0 16px; /* 좌우 패딩 */ + gap: 12px; /* 화살표와 버튼 사이 간격 */ +} + +/* 날짜 버튼 영역 감싸는 래퍼 (보이는 영역) */ +.date-button-wrapper { + overflow: hidden; /* 넘치는 날짜 버튼은 가림 */ + width: 880px; /* 정확히 버튼 7개 + gap 기준으로 맞춘 너비 */ +} + +/* 실제 날짜 버튼들이 배치되는 줄 */ +.date-buttons { + display: flex; /* 날짜 버튼들을 가로로 배치 */ + gap: 30px; /* 각 버튼 사이 간격 */ + transition: transform 0.3s ease; /* 좌우 이동 애니메이션 (슬라이드 효과) */ + min-width: fit-content; /* 자식 크기만큼 너비 자동 확장 */ +} + +/* 개별 날짜 버튼 */ +.date-buttons button.day { + display: flex; /* 내부 요소(.day-date, .day-week) 가로 → 세로 정렬 */ + flex-direction: column; + justify-content: center; /* 수직 중앙 정렬 */ + align-items: center; /* 수평 중앙 정렬 */ + width: 100px; /* 버튼 가로 크기 */ + height: 80px; /* 버튼 세로 크기 */ + border: none; /* 테두리 없음 */ + border-radius: 12px; /* 모서리 둥글게 */ + background-color: #eee; /* 기본 회색 배경 */ + cursor: pointer; /* 클릭 가능하게 손가락 커서 */ + font-family: inherit; /* 전체 폰트 상속 */ + line-height: 1.1; /* 줄간격 설정 */ +} + +/* 날짜 숫자 (ex: 6, 10 등) */ +.date-buttons button.day .day-date { + font-size: 15px; /* 글자 크기 */ + font-weight: bold; /* 굵은 글씨 */ + line-height: 1; /* 줄간격 제거로 위아래 밀착 */ +} + +/* 요일 (ex: 화, 수 등) */ +.date-buttons button.day .day-week { + font-size: 12px; /* 글자 크기 작게 */ + opacity: 0.8; /* 살짝 연하게 표시 */ + margin-top: 1px; /* 위 숫자와 약간 띄우기 */ + line-height: 1; /* 줄간격 없음 */ +} + +/* 선택된 날짜 버튼 스타일 */ +.date-buttons button.selected { + background-color: #007bff; /* 파란색 배경 */ + color: white; /* 텍스트 흰색 */ +} + +/* 좌우 화살표 버튼 */ +.arrow { + width: 32px; /* 크기 고정 */ + height: 32px; + font-size: 24px; /* 화살표 기호 크게 보이게 */ + background: none; /* 배경 없음 */ + border: none; /* 테두리 없음 */ + color: #888; /* 기본 회색 */ + cursor: pointer; /* 커서 포인터로 변경 */ +} + +/* 비활성화된 화살표 버튼 */ +.arrow:disabled { + color: #ccc; /* 더 연한 색상으로 비활성 표시 */ + cursor: default; /* 커서 기본으로 변경 (포인터 아님) */ +} + +/* 필터 (지역, 성별, 플랫폼, 마감 가리기) 정렬 */ +.filter-container { + display: flex; /* 가로로 배치 */ + justify-content: flex-end; /* 오른쪽 정렬 */ + gap: 12px; /* 셀렉트박스 사이 여백 */ + margin-bottom: 20px; /* 아래쪽 여백 */ +} diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html deleted file mode 100644 index 7560ce4..0000000 --- a/src/main/resources/static/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -hello! - - \ No newline at end of file diff --git a/src/main/resources/static/js/match.js b/src/main/resources/static/js/match.js new file mode 100644 index 0000000..abbfcc7 --- /dev/null +++ b/src/main/resources/static/js/match.js @@ -0,0 +1,62 @@ +const itemsPerPage = 7; +const itemsPerSlide = 1; +let currentIndex = 0; // 이동 인덱스 + +function getStepSize() { + const allButtons = document.querySelectorAll("#dateButtonList .day"); + if (allButtons.length < 2) return allButtons[0]?.offsetWidth || 0; + + const first = allButtons[0]; + const second = allButtons[1]; + + const step = second.getBoundingClientRect().left - first.getBoundingClientRect().left; + return step; +} + +function updateDatePage() { + const allButtons = document.querySelectorAll("#dateButtonList .day"); + const container = document.querySelector("#dateButtonList"); + + const step = getStepSize(); + const offset = currentIndex * step; + container.style.transform = `translateX(-${offset}px)`; + + const maxIndex = allButtons.length - itemsPerPage; + document.getElementById("prevBtn").disabled = currentIndex <= 0; + document.getElementById("nextBtn").disabled = currentIndex >= maxIndex; +} + +document.getElementById("prevBtn").addEventListener("click", () => { + if (currentIndex > 0) { + currentIndex -= itemsPerSlide; + updateDatePage(); + } +}); + +document.getElementById("nextBtn").addEventListener("click", () => { + const allButtons = document.querySelectorAll("#dateButtonList .day"); + const maxIndex = allButtons.length - itemsPerPage; + if (currentIndex < maxIndex) { + currentIndex += itemsPerSlide; + updateDatePage(); + } +}); + +window.onload = () => { + updateDatePage(); + + document.querySelectorAll(".day").forEach(btn => { + btn.addEventListener("click", () => { + document.querySelectorAll(".day").forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + fetchMatches(btn.dataset.date); + }); + }); + + // 최초 선택 + const first = document.querySelector(".day"); + if (first) { + first.classList.add("selected"); + fetchMatches(first.dataset.date); + } +}; diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..42f41f3 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,59 @@ + + + + + FutsalFinder + + + + +
+ + +
+
+ + + +
+
+ + +
+ +
+ + + + + + + +
+ +
+ +
+ + + + From 636a0638088c9dcb3483dac918db9e094e4d3f68 Mon Sep 17 00:00:00 2001 From: Junha Date: Wed, 7 May 2025 20:19:53 +0900 Subject: [PATCH 02/18] =?UTF-8?q?update:=20PlatformConfig=EC=97=90=20?= =?UTF-8?q?=ED=95=9C=EA=B8=80=20=ED=92=80=EB=84=A4=EC=9E=84,=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=EC=88=9C=EC=9C=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/platform/IamConfig.java | 3 + .../config/platform/PlabConfig.java | 3 + .../config/platform/PlatformConfig.java | 4 + .../config/platform/PuzzleConfig.java | 2 + .../config/platform/UrbanConfig.java | 3 + .../config/platform/WithConfig.java | 3 + src/main/resources/static/css/style.css | 86 ------------------- src/main/resources/static/js/home.js | 31 +++++++ src/main/resources/static/js/match.js | 62 ------------- 9 files changed, 49 insertions(+), 148 deletions(-) delete mode 100644 src/main/resources/static/css/style.css create mode 100644 src/main/resources/static/js/home.js delete mode 100644 src/main/resources/static/js/match.js diff --git a/src/main/java/futsal/futsalMatch/config/platform/IamConfig.java b/src/main/java/futsal/futsalMatch/config/platform/IamConfig.java index b1c754b..bb46387 100644 --- a/src/main/java/futsal/futsalMatch/config/platform/IamConfig.java +++ b/src/main/java/futsal/futsalMatch/config/platform/IamConfig.java @@ -7,6 +7,7 @@ @Component public class IamConfig implements PlatformConfig { private final String platform = "IAM"; + private final String fullNameInKorean = "아이엠그라운드"; private final String date = "start_date"; private final String time = "start_time"; private final String region = "fAddress"; @@ -23,4 +24,6 @@ public class IamConfig implements PlatformConfig { private final String link = "s_match_num"; private final String requestBaseURL = "https://m.iamground.kr/api/v1/s_match/getsmatchlist"; private final String matchLinkBaseURL = "https://m.iamground.kr/futsal/s_match/detail/"; + + private final int priority = 5; } diff --git a/src/main/java/futsal/futsalMatch/config/platform/PlabConfig.java b/src/main/java/futsal/futsalMatch/config/platform/PlabConfig.java index 86eefad..8aa1880 100644 --- a/src/main/java/futsal/futsalMatch/config/platform/PlabConfig.java +++ b/src/main/java/futsal/futsalMatch/config/platform/PlabConfig.java @@ -7,6 +7,7 @@ @Component public class PlabConfig implements PlatformConfig { private final String platform = "PLAB"; + private final String fullNameInKorean = "플랩풋볼"; private final String date = "schedule"; private final String time = "schedule"; private final String region = "area_group_name"; @@ -23,4 +24,6 @@ public class PlabConfig implements PlatformConfig { private final String link = "id"; private final String requestBaseURL = "https://www.plabfootball.com/api/v2/integrated-matches/"; private final String matchLinkBaseURL = "https://www.plabfootball.com/match/"; + + private final int priority = 1; } diff --git a/src/main/java/futsal/futsalMatch/config/platform/PlatformConfig.java b/src/main/java/futsal/futsalMatch/config/platform/PlatformConfig.java index 3c877ff..0737180 100644 --- a/src/main/java/futsal/futsalMatch/config/platform/PlatformConfig.java +++ b/src/main/java/futsal/futsalMatch/config/platform/PlatformConfig.java @@ -4,6 +4,7 @@ public interface PlatformConfig { String getPlatform(); + String getFullNameInKorean(); String getDate(); String getTime(); String getRegion(); @@ -24,5 +25,8 @@ public interface PlatformConfig { default Map getCustomFields() { return Map.of(); } + default int getPriority() { + return 999; + } } diff --git a/src/main/java/futsal/futsalMatch/config/platform/PuzzleConfig.java b/src/main/java/futsal/futsalMatch/config/platform/PuzzleConfig.java index 7a36abc..ad1eab0 100644 --- a/src/main/java/futsal/futsalMatch/config/platform/PuzzleConfig.java +++ b/src/main/java/futsal/futsalMatch/config/platform/PuzzleConfig.java @@ -9,6 +9,7 @@ @Component public class PuzzleConfig implements PlatformConfig { private final String platform = "PUZZLE"; + private final String fullNameInKorean = "퍼즐플레이"; private final String date = "match_date"; private final String time = "match_time"; private final String region = "ground_region"; @@ -32,4 +33,5 @@ public class PuzzleConfig implements PlatformConfig { "GYEONGGI_DONGBU_CODE", "65126e3929b8b579c68f372f" ); + private final int priority = 2; } diff --git a/src/main/java/futsal/futsalMatch/config/platform/UrbanConfig.java b/src/main/java/futsal/futsalMatch/config/platform/UrbanConfig.java index 6ea1656..fe66011 100644 --- a/src/main/java/futsal/futsalMatch/config/platform/UrbanConfig.java +++ b/src/main/java/futsal/futsalMatch/config/platform/UrbanConfig.java @@ -7,6 +7,7 @@ @Component public class UrbanConfig implements PlatformConfig { private final String platform = "URBAN"; + private final String fullNameInKorean = "어반풋볼"; private final String date = "span.date"; private final String time = "li.time > span"; private final String region = ""; @@ -23,4 +24,6 @@ public class UrbanConfig implements PlatformConfig { private final String link = "data_id"; private final String requestBaseURL = "https://urbanfootball.co.kr/result/result_get_data.php"; private final String matchLinkBaseURL = "https://urbanfootball.co.kr/goods/goods_view.html?goods_no="; + + private final int priority = 4; } diff --git a/src/main/java/futsal/futsalMatch/config/platform/WithConfig.java b/src/main/java/futsal/futsalMatch/config/platform/WithConfig.java index d1067ba..87b80a0 100644 --- a/src/main/java/futsal/futsalMatch/config/platform/WithConfig.java +++ b/src/main/java/futsal/futsalMatch/config/platform/WithConfig.java @@ -7,6 +7,7 @@ @Component public class WithConfig implements PlatformConfig { private final String platform = "WITH"; + private final String fullNameInKorean = "위드풋살"; private final String date = "date"; private final String time = "play_time"; private final String region = "mem_area"; @@ -23,4 +24,6 @@ public class WithConfig implements PlatformConfig { private final String link = "idx"; private final String requestBaseURL = "https://withfutsal.com/ajaxProcSocialMatch.php"; private final String matchLinkBaseURL = "https://withfutsal.com/Sub/ground.php?block_idx="; + + private final int priority = 3; } diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css deleted file mode 100644 index b7c2a18..0000000 --- a/src/main/resources/static/css/style.css +++ /dev/null @@ -1,86 +0,0 @@ -/* 날짜 네비게이션 바 전체 컨테이너 */ -.date-nav-container { - display: flex; /* 가로 방향으로 아이템 배치 (화살표 + 날짜 버튼) */ - align-items: center; /* 수직 중앙 정렬 */ - justify-content: space-between; /* 좌우 화살표는 고정, 가운데 날짜 영역은 유동 */ - max-width: 1000px; /* 전체 최대 폭 제한 */ - margin: 0 auto 16px auto; /* 가운데 정렬 + 아래 여백 */ - padding: 0 16px; /* 좌우 패딩 */ - gap: 12px; /* 화살표와 버튼 사이 간격 */ -} - -/* 날짜 버튼 영역 감싸는 래퍼 (보이는 영역) */ -.date-button-wrapper { - overflow: hidden; /* 넘치는 날짜 버튼은 가림 */ - width: 880px; /* 정확히 버튼 7개 + gap 기준으로 맞춘 너비 */ -} - -/* 실제 날짜 버튼들이 배치되는 줄 */ -.date-buttons { - display: flex; /* 날짜 버튼들을 가로로 배치 */ - gap: 30px; /* 각 버튼 사이 간격 */ - transition: transform 0.3s ease; /* 좌우 이동 애니메이션 (슬라이드 효과) */ - min-width: fit-content; /* 자식 크기만큼 너비 자동 확장 */ -} - -/* 개별 날짜 버튼 */ -.date-buttons button.day { - display: flex; /* 내부 요소(.day-date, .day-week) 가로 → 세로 정렬 */ - flex-direction: column; - justify-content: center; /* 수직 중앙 정렬 */ - align-items: center; /* 수평 중앙 정렬 */ - width: 100px; /* 버튼 가로 크기 */ - height: 80px; /* 버튼 세로 크기 */ - border: none; /* 테두리 없음 */ - border-radius: 12px; /* 모서리 둥글게 */ - background-color: #eee; /* 기본 회색 배경 */ - cursor: pointer; /* 클릭 가능하게 손가락 커서 */ - font-family: inherit; /* 전체 폰트 상속 */ - line-height: 1.1; /* 줄간격 설정 */ -} - -/* 날짜 숫자 (ex: 6, 10 등) */ -.date-buttons button.day .day-date { - font-size: 15px; /* 글자 크기 */ - font-weight: bold; /* 굵은 글씨 */ - line-height: 1; /* 줄간격 제거로 위아래 밀착 */ -} - -/* 요일 (ex: 화, 수 등) */ -.date-buttons button.day .day-week { - font-size: 12px; /* 글자 크기 작게 */ - opacity: 0.8; /* 살짝 연하게 표시 */ - margin-top: 1px; /* 위 숫자와 약간 띄우기 */ - line-height: 1; /* 줄간격 없음 */ -} - -/* 선택된 날짜 버튼 스타일 */ -.date-buttons button.selected { - background-color: #007bff; /* 파란색 배경 */ - color: white; /* 텍스트 흰색 */ -} - -/* 좌우 화살표 버튼 */ -.arrow { - width: 32px; /* 크기 고정 */ - height: 32px; - font-size: 24px; /* 화살표 기호 크게 보이게 */ - background: none; /* 배경 없음 */ - border: none; /* 테두리 없음 */ - color: #888; /* 기본 회색 */ - cursor: pointer; /* 커서 포인터로 변경 */ -} - -/* 비활성화된 화살표 버튼 */ -.arrow:disabled { - color: #ccc; /* 더 연한 색상으로 비활성 표시 */ - cursor: default; /* 커서 기본으로 변경 (포인터 아님) */ -} - -/* 필터 (지역, 성별, 플랫폼, 마감 가리기) 정렬 */ -.filter-container { - display: flex; /* 가로로 배치 */ - justify-content: flex-end; /* 오른쪽 정렬 */ - gap: 12px; /* 셀렉트박스 사이 여백 */ - margin-bottom: 20px; /* 아래쪽 여백 */ -} diff --git a/src/main/resources/static/js/home.js b/src/main/resources/static/js/home.js new file mode 100644 index 0000000..559c849 --- /dev/null +++ b/src/main/resources/static/js/home.js @@ -0,0 +1,31 @@ +window.onload = () => { + updateDatePage(); + resizeDayButtons(); + + document.querySelectorAll(".day").forEach(btn => { + btn.addEventListener("click", () => { + document.querySelectorAll(".day").forEach(b => b.classList.remove("selected")); + btn.classList.add("selected"); + fetchMatches(btn.dataset.date); + }); + }); + + // 최초 선택 + const first = document.querySelector(".day"); + if (first) { + first.classList.add("selected"); + fetchMatches(first.dataset.date); + } +}; + +window.addEventListener("resize", () => { + resizeDayButtons(); + updateDatePage(); // 이동 위치도 다시 맞추기 +}); + +window.addEventListener("DOMContentLoaded", () => { + resizeDayButtons(); + updateDatePage(); +}); + + diff --git a/src/main/resources/static/js/match.js b/src/main/resources/static/js/match.js deleted file mode 100644 index abbfcc7..0000000 --- a/src/main/resources/static/js/match.js +++ /dev/null @@ -1,62 +0,0 @@ -const itemsPerPage = 7; -const itemsPerSlide = 1; -let currentIndex = 0; // 이동 인덱스 - -function getStepSize() { - const allButtons = document.querySelectorAll("#dateButtonList .day"); - if (allButtons.length < 2) return allButtons[0]?.offsetWidth || 0; - - const first = allButtons[0]; - const second = allButtons[1]; - - const step = second.getBoundingClientRect().left - first.getBoundingClientRect().left; - return step; -} - -function updateDatePage() { - const allButtons = document.querySelectorAll("#dateButtonList .day"); - const container = document.querySelector("#dateButtonList"); - - const step = getStepSize(); - const offset = currentIndex * step; - container.style.transform = `translateX(-${offset}px)`; - - const maxIndex = allButtons.length - itemsPerPage; - document.getElementById("prevBtn").disabled = currentIndex <= 0; - document.getElementById("nextBtn").disabled = currentIndex >= maxIndex; -} - -document.getElementById("prevBtn").addEventListener("click", () => { - if (currentIndex > 0) { - currentIndex -= itemsPerSlide; - updateDatePage(); - } -}); - -document.getElementById("nextBtn").addEventListener("click", () => { - const allButtons = document.querySelectorAll("#dateButtonList .day"); - const maxIndex = allButtons.length - itemsPerPage; - if (currentIndex < maxIndex) { - currentIndex += itemsPerSlide; - updateDatePage(); - } -}); - -window.onload = () => { - updateDatePage(); - - document.querySelectorAll(".day").forEach(btn => { - btn.addEventListener("click", () => { - document.querySelectorAll(".day").forEach(b => b.classList.remove("selected")); - btn.classList.add("selected"); - fetchMatches(btn.dataset.date); - }); - }); - - // 최초 선택 - const first = document.querySelector(".day"); - if (first) { - first.classList.add("selected"); - fetchMatches(first.dataset.date); - } -}; From c69e07805c9f05e0dff16e94005e932e167f329e Mon Sep 17 00:00:00 2001 From: Junha Date: Wed, 7 May 2025 20:21:14 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=EB=82=A0=EC=A7=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B0=8F=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A0=9C=EA=B3=B5=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ViewController.java | 31 ++++++--------- .../futsalMatch/controller/dto/DayInfo.java | 7 ++++ .../controller/dto/PlatformInfo.java | 4 ++ .../futsalMatch/service/CalendarService.java | 38 +++++++++++++++++++ .../service/PlatformConfigService.java | 31 +++++++++++++++ 5 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 src/main/java/futsal/futsalMatch/controller/dto/DayInfo.java create mode 100644 src/main/java/futsal/futsalMatch/controller/dto/PlatformInfo.java create mode 100644 src/main/java/futsal/futsalMatch/service/CalendarService.java create mode 100644 src/main/java/futsal/futsalMatch/service/PlatformConfigService.java diff --git a/src/main/java/futsal/futsalMatch/controller/ViewController.java b/src/main/java/futsal/futsalMatch/controller/ViewController.java index 16d431f..35d0fca 100644 --- a/src/main/java/futsal/futsalMatch/controller/ViewController.java +++ b/src/main/java/futsal/futsalMatch/controller/ViewController.java @@ -1,36 +1,29 @@ package futsal.futsalMatch.controller; +import futsal.futsalMatch.service.CalendarService; +import futsal.futsalMatch.service.PlatformConfigService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import java.time.LocalDate; -import java.time.format.TextStyle; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.time.ZoneId; +import java.time.ZonedDateTime; @Controller +@RequiredArgsConstructor public class ViewController { + private final CalendarService calendarService; + private final PlatformConfigService platformConfigService; + @GetMapping("/") public String index(Model model) { - LocalDate today = LocalDate.now(); - List> days = new ArrayList<>(); - - for (int i = 0; i < 14; i++) { - LocalDate date = today.plusDays(i); - String dayOfTheWeek = date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.KOREAN).substring(0, 1); - - days.add(Map.of( - "date", date.toString(), // yyyy-MM-dd - "day-date", String.valueOf(date.getDayOfMonth()), - "day-week", dayOfTheWeek - )); - } + LocalDate today = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate(); + model.addAttribute("days", calendarService.getNextTwoWeeksDays(today)); + model.addAttribute("platforms", platformConfigService.getPlatformInfos()); - model.addAttribute("days", days); return "index"; } } diff --git a/src/main/java/futsal/futsalMatch/controller/dto/DayInfo.java b/src/main/java/futsal/futsalMatch/controller/dto/DayInfo.java new file mode 100644 index 0000000..7603e35 --- /dev/null +++ b/src/main/java/futsal/futsalMatch/controller/dto/DayInfo.java @@ -0,0 +1,7 @@ +package futsal.futsalMatch.controller.dto; + +import java.time.LocalDate; + +public record DayInfo(LocalDate date, Integer dayOfMonth, String dayWeek, Boolean isHoliday) { +} + diff --git a/src/main/java/futsal/futsalMatch/controller/dto/PlatformInfo.java b/src/main/java/futsal/futsalMatch/controller/dto/PlatformInfo.java new file mode 100644 index 0000000..0ef7f87 --- /dev/null +++ b/src/main/java/futsal/futsalMatch/controller/dto/PlatformInfo.java @@ -0,0 +1,4 @@ +package futsal.futsalMatch.controller.dto; + +public record PlatformInfo(String platform, String fullNameInKorean) { +} diff --git a/src/main/java/futsal/futsalMatch/service/CalendarService.java b/src/main/java/futsal/futsalMatch/service/CalendarService.java new file mode 100644 index 0000000..43c5570 --- /dev/null +++ b/src/main/java/futsal/futsalMatch/service/CalendarService.java @@ -0,0 +1,38 @@ +package futsal.futsalMatch.service; + +import futsal.futsalMatch.controller.dto.DayInfo; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.TextStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +@Service +public class CalendarService { + + //TODO Cache 사용 + /*private final Map> cache = new ConcurrentHashMap<>(); + + public List getNextTwoWeeksDays(LocalDate from) { + cache.keySet().removeIf(date -> date.isBefore(from)); + return cache.computeIfAbsent(from, this::generateNextTwoWeeksDays); + }*/ + + public List getNextTwoWeeksDays(LocalDate from) { + return generateNextTwoWeeksDays(from); + } + + public List generateNextTwoWeeksDays(LocalDate from) { + List days = new ArrayList<>(); + for(int i = 0; i < 14; i++) { + LocalDate date = from.plusDays(i); + String dayOfTheWeek = date.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.KOREAN).substring(0, 1); + Boolean isHoliday = false; //TODO 추후 구현 - 외부 API 연동 + + days.add(new DayInfo(date, date.getDayOfMonth(), dayOfTheWeek, isHoliday)); + } + return days; + } +} diff --git a/src/main/java/futsal/futsalMatch/service/PlatformConfigService.java b/src/main/java/futsal/futsalMatch/service/PlatformConfigService.java new file mode 100644 index 0000000..9a988c1 --- /dev/null +++ b/src/main/java/futsal/futsalMatch/service/PlatformConfigService.java @@ -0,0 +1,31 @@ +package futsal.futsalMatch.service; + +import futsal.futsalMatch.config.platform.PlatformConfig; +import futsal.futsalMatch.controller.dto.PlatformInfo; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PlatformConfigService { + + private final List configs; + private final List platformInfos = new ArrayList<>(); + + @PostConstruct + public void init() { + configs.sort(Comparator.comparing(PlatformConfig::getPriority)); + for(PlatformConfig config : configs) { + platformInfos.add(new PlatformInfo(config.getPlatform(), config.getFullNameInKorean())); + } + } + + public List getPlatformInfos() { + return platformInfos; + } +} From 801b0d6cef90873395e54db39cc30b13163974a9 Mon Sep 17 00:00:00 2001 From: Junha Date: Wed, 7 May 2025 20:21:45 +0900 Subject: [PATCH 04/18] update: develop front-end page --- src/main/resources/static/css/date.css | 99 ++++++++++++++++++++++++ src/main/resources/static/css/filter.css | 89 +++++++++++++++++++++ src/main/resources/static/js/date.js | 59 ++++++++++++++ src/main/resources/static/js/filter.js | 48 ++++++++++++ src/main/resources/static/js/home.js | 24 +++--- src/main/resources/templates/index.html | 79 ++++++++++++++----- 6 files changed, 365 insertions(+), 33 deletions(-) create mode 100644 src/main/resources/static/css/date.css create mode 100644 src/main/resources/static/css/filter.css create mode 100644 src/main/resources/static/js/date.js create mode 100644 src/main/resources/static/js/filter.js diff --git a/src/main/resources/static/css/date.css b/src/main/resources/static/css/date.css new file mode 100644 index 0000000..13236d5 --- /dev/null +++ b/src/main/resources/static/css/date.css @@ -0,0 +1,99 @@ +/* 날짜 네비게이션 바 전체 컨테이너 */ +.date-nav-container { + display: flex; /* 가로 방향으로 아이템 배치 (화살표 + 날짜 버튼) */ + align-items: center; /* 수직 중앙 정렬 */ + justify-content: space-between; /* 좌우 화살표는 고정, 가운데 날짜 영역은 유동 */ + max-width: 1000px; /* 전체 최대 폭 제한 */ + margin: 0 auto 16px auto; /* 가운데 정렬 + 아래 여백 */ + padding: 0 16px; /* 좌우 패딩 */ + gap: 12px; /* 화살표와 버튼 사이 간격 */ +} + +/* 날짜 버튼 영역 감싸는 래퍼 (보이는 영역) */ +.date-button-wrapper { + overflow: hidden; /* 넘치는 날짜 버튼은 가림 */ + width: 880px; /* 정확히 버튼 7개 + gap 기준으로 맞춘 너비 */ +} + +/* 실제 날짜 버튼들이 배치되는 줄 */ +.date-buttons { + display: flex; /* 날짜 버튼들을 가로로 배치 */ + gap: 30px; /* 각 버튼 사이 간격 */ + transition: transform 0.2s ease; /* 좌우 이동 애니메이션 (슬라이드 효과) */ + min-width: fit-content; /* 자식 크기만큼 너비 자동 확장 */ +} + +/* 개별 날짜 버튼 */ +.date-buttons button.day { + display: flex; /* 내부 요소(.day-date, .day-week) 가로 → 세로 정렬 */ + flex-direction: column; + justify-content: center; /* 수직 중앙 정렬 */ + align-items: center; /* 수평 중앙 정렬 */ + width: 100px; /* 버튼 가로 크기 */ + height: 80px; /* 버튼 세로 크기 */ + border: none; /* 테두리 없음 */ + + border-radius: 9999px; /* ✅ 완전한 타원형 / 캡슐형 */ + background-color: transparent; /* 배경 투명 */ + cursor: pointer; /* 클릭 가능하게 손가락 커서 */ + font-family: inherit; /* 전체 폰트 상속 */ + line-height: 1.1; /* 줄간격 설정 */ + padding: 0; +} + +/* 날짜 숫자 (ex: 6, 10 등) */ +.date-buttons button.day .day-date { + font-size: 15px; /* 글자 크기 */ + font-weight: bold; /* 굵은 글씨 */ + line-height: 1; /* 줄간격 제거로 위아래 밀착 */ +} + +/* 요일 (ex: 화, 수 등) */ +.date-buttons button.day .day-week { + font-size: 12px; /* 글자 크기 작게 */ + opacity: 0.8; /* 살짝 연하게 표시 */ + margin-top: 1px; /* 위 숫자와 약간 띄우기 */ + line-height: 1; /* 줄간격 없음 */ +} + +/* 토요일: 파란색 */ +.date-buttons button.day.saturday { + color: #0000FF; + font-weight: bold; +} + +/* 일요일: 빨간색 */ +.date-buttons button.day.sunday { + color: #ff0000; + font-weight: bold; +} + +/* 선택된 날짜 버튼 스타일 */ +.date-buttons button.day.selected { + background-color: #007bff; /* 파란색 배경 */ + color: white; /* 텍스트 흰색 */ +} + +/* 좌우 화살표 버튼 */ +.arrow { + width: 32px; /* 크기 고정 */ + height: 32px; + font-size: 24px; /* 화살표 기호 크게 보이게 */ + background: none; /* 배경 없음 */ + border: none; /* 테두리 없음 */ + color: #888; /* 기본 회색 */ + cursor: pointer; /* 커서 포인터로 변경 */ +} + +/* 비활성화된 화살표 버튼 */ +.arrow:disabled { + color: #ccc; /* 더 연한 색상으로 비활성 표시 */ + cursor: default; /* 커서 기본으로 변경 (포인터 아님) */ +} + + + + + + + diff --git a/src/main/resources/static/css/filter.css b/src/main/resources/static/css/filter.css new file mode 100644 index 0000000..9467f64 --- /dev/null +++ b/src/main/resources/static/css/filter.css @@ -0,0 +1,89 @@ +/* 필터 (지역, 성별, 플랫폼, 마감 가리기) 정렬 */ +.filter-container { + display: flex; /* 가로로 배치 */ + justify-content: flex-end; /* 오른쪽 정렬 */ + gap: 12px; /* 셀렉트박스 사이 여백 */ + margin-bottom: 20px; /* 아래쪽 여백 */ +} + +/* 마감 가리기 기본 상태: 회색 */ +.hide-btn { + border: 2px solid #B0B8C1; + border-radius: 20px; + padding: 6px 16px; + background-color: white; + color: #6B7684; + font-size: 14px; + cursor: pointer; + transition: all 0.25s ease; + font-weight: 500; +} + +/* 마감 가리기 활성 상태: 파란색 */ +.hide-btn.active { + border-color: #0064FF; + color: #0064FF; +} + +/****************/ +.modal-overlay { + display: none; /* 기본은 숨김 */ + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.5); /* 반투명 배경 */ + z-index: 999; + justify-content: center; + align-items: center; +} + +.modal-overlay.active { + display: flex; +} + +.modal-content { + background: white; + border-radius: 20px; + width: 90%; + max-width: 400px; + padding: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: bold; + font-size: 18px; + margin-bottom: 20px; +} + +.close-btn { + background: none; + border: none; + font-size: 20px; + cursor: pointer; +} + +.modal-body label { + display: flex; + align-items: center; + font-size: 16px; + padding: 8px 0; +} + +.modal-footer { + margin-top: 20px; +} + +.apply-btn { + width: 100%; + padding: 10px; + background-color: #007bff; + color: white; + font-weight: bold; + font-size: 15px; + border: none; + border-radius: 12px; + cursor: pointer; +} \ No newline at end of file diff --git a/src/main/resources/static/js/date.js b/src/main/resources/static/js/date.js new file mode 100644 index 0000000..b6007db --- /dev/null +++ b/src/main/resources/static/js/date.js @@ -0,0 +1,59 @@ +const itemsPerPage = 7; +const itemsPerSlide = 1; +let currentIndex = 0; // 이동 인덱스 + +function getStepSize() { + const allButtons = document.querySelectorAll("#dateButtonList .day"); + if (allButtons.length < 2) return allButtons[0]?.offsetWidth || 0; + + const first = allButtons[0]; + const second = allButtons[1]; + + const step = second.getBoundingClientRect().left - first.getBoundingClientRect().left; + return step; +} + +function updateDatePage() { + const allButtons = document.querySelectorAll("#dateButtonList .day"); + const container = document.querySelector("#dateButtonList"); + + const step = getStepSize(); + const offset = currentIndex * step; + container.style.transform = `translateX(-${offset}px)`; + + const maxIndex = allButtons.length - itemsPerPage; + document.getElementById("prevBtn").disabled = currentIndex <= 0; + document.getElementById("nextBtn").disabled = currentIndex >= maxIndex; +} + +document.getElementById("prevBtn").addEventListener("click", () => { + if (currentIndex > 0) { + currentIndex -= itemsPerSlide; + updateDatePage(); + } +}); + +document.getElementById("nextBtn").addEventListener("click", () => { + const allButtons = document.querySelectorAll("#dateButtonList .day"); + const maxIndex = allButtons.length - itemsPerPage; + if (currentIndex < maxIndex) { + currentIndex += itemsPerSlide; + updateDatePage(); + } +}); + +function resizeDayButtons() { + const wrapper = document.querySelector(".date-button-wrapper"); + const buttons = document.querySelectorAll("#dateButtonList .day"); + + if (!wrapper || buttons.length === 0) return; + + const wrapperWidth = wrapper.offsetWidth; + const gap = 30; // 버튼 사이 간격 (CSS의 .date-buttons gap과 동일) + const totalGap = gap * (7 - 1); // 7개의 버튼 사이에 6개의 gap + const buttonWidth = Math.floor((wrapperWidth - totalGap) / 7); + + buttons.forEach(btn => { + btn.style.width = `${buttonWidth}px`; + }); +} \ No newline at end of file diff --git a/src/main/resources/static/js/filter.js b/src/main/resources/static/js/filter.js new file mode 100644 index 0000000..e8c1d40 --- /dev/null +++ b/src/main/resources/static/js/filter.js @@ -0,0 +1,48 @@ +document.addEventListener("DOMContentLoaded", () => { + // 열기 버튼 + document.querySelectorAll("[data-modal-target]").forEach(btn => { + btn.addEventListener("click", () => { + const selector = btn.getAttribute("data-modal-target"); + const modal = document.querySelector(selector); + modal?.classList.add("active"); + }); + }); + + // 닫기 버튼 + document.querySelectorAll(".modal-overlay .close-btn").forEach(btn => { + btn.addEventListener("click", () => { + const modal = btn.closest(".modal-overlay"); + modal?.classList.remove("active"); + }); + }); + + // 바깥 클릭 시 닫기 + document.querySelectorAll(".modal-overlay").forEach(modal => { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + modal.classList.remove("active"); + } + }); + }); + + // 적용 버튼 + document.querySelectorAll(".modal-overlay .apply-btn").forEach(btn => { + btn.addEventListener("click", () => { + const modal = btn.closest(".modal-overlay"); + const values = Array.from(modal.querySelectorAll("input[type=checkbox]:checked")) + .map(cb => cb.value); + + console.log(`[${modal.id}] 선택된 항목:`, values); + // 여기에 실제 필터 적용 로직 추가 가능 + modal.classList.remove("active"); + }); + }); +}) + +document.getElementById("hideFullToggle").addEventListener("click", function () { + this.classList.toggle("active"); + + const isActive = this.classList.contains("active"); + // 실제 필터링 로직이 있다면 여기서 호출 + // 예: toggleFullMatches(isActive); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/home.js b/src/main/resources/static/js/home.js index 559c849..294f22f 100644 --- a/src/main/resources/static/js/home.js +++ b/src/main/resources/static/js/home.js @@ -1,7 +1,9 @@ -window.onload = () => { - updateDatePage(); +function initializeDateUI() { resizeDayButtons(); + updateDatePage(); +} +function setupDateSelection() { document.querySelectorAll(".day").forEach(btn => { btn.addEventListener("click", () => { document.querySelectorAll(".day").forEach(b => b.classList.remove("selected")); @@ -9,23 +11,21 @@ window.onload = () => { fetchMatches(btn.dataset.date); }); }); +} - // 최초 선택 +function selectFirstDateIfNone() { const first = document.querySelector(".day"); if (first) { first.classList.add("selected"); fetchMatches(first.dataset.date); } -}; - -window.addEventListener("resize", () => { - resizeDayButtons(); - updateDatePage(); // 이동 위치도 다시 맞추기 -}); +} +// 실행 시점 window.addEventListener("DOMContentLoaded", () => { - resizeDayButtons(); - updateDatePage(); + initializeDateUI(); + setupDateSelection(); + selectFirstDateIfNone(); }); - +window.addEventListener("resize", initializeDateUI); diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 42f41f3..f285086 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -3,7 +3,8 @@ FutsalFinder - + + @@ -13,9 +14,11 @@
-
@@ -26,34 +29,68 @@
- + + + - + + + + + - +
- + + + From 60f40f9db28eb6c44d15a413b5d169635bd66b50 Mon Sep 17 00:00:00 2001 From: Junha Date: Wed, 7 May 2025 20:41:06 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=EB=82=A0=EC=A7=9C=20=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/css/date.css | 39 +++++++++++++++++--------- src/main/resources/static/js/date.js | 20 ++++++------- src/main/resources/static/js/home.js | 2 +- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/main/resources/static/css/date.css b/src/main/resources/static/css/date.css index 13236d5..b91495f 100644 --- a/src/main/resources/static/css/date.css +++ b/src/main/resources/static/css/date.css @@ -11,8 +11,18 @@ /* 날짜 버튼 영역 감싸는 래퍼 (보이는 영역) */ .date-button-wrapper { - overflow: hidden; /* 넘치는 날짜 버튼은 가림 */ - width: 880px; /* 정확히 버튼 7개 + gap 기준으로 맞춘 너비 */ + display: flex; /* flex 유지 */ + overflow-x: auto; /* 수평 스크롤 허용 */ + scroll-snap-type: x mandatory; /* 좌우 스냅 설정 */ + -webkit-overflow-scrolling: touch; /* 모바일 부드러운 스크롤 */ + scroll-behavior: smooth; /* 부드러운 이동 */ + + scrollbar-width: none; /* Firefox용 스크롤 숨김 */ + -ms-overflow-style: none; /* IE용 스크롤 숨김 */ +} + +.date-button-wrapper::-webkit-scrollbar { + display: none; /* Chrome, Safari */ } /* 실제 날짜 버튼들이 배치되는 줄 */ @@ -25,19 +35,20 @@ /* 개별 날짜 버튼 */ .date-buttons button.day { - display: flex; /* 내부 요소(.day-date, .day-week) 가로 → 세로 정렬 */ + flex: 0 0 auto; /* 스냅을 위해 고정 너비 */ + scroll-snap-align: start; /* 버튼의 시작 지점에 스냅 */ + display: flex; flex-direction: column; - justify-content: center; /* 수직 중앙 정렬 */ - align-items: center; /* 수평 중앙 정렬 */ - width: 100px; /* 버튼 가로 크기 */ - height: 80px; /* 버튼 세로 크기 */ - border: none; /* 테두리 없음 */ - - border-radius: 9999px; /* ✅ 완전한 타원형 / 캡슐형 */ - background-color: transparent; /* 배경 투명 */ - cursor: pointer; /* 클릭 가능하게 손가락 커서 */ - font-family: inherit; /* 전체 폰트 상속 */ - line-height: 1.1; /* 줄간격 설정 */ + justify-content: center; + align-items: center; + width: 100px; + height: 80px; + border: none; + border-radius: 9999px; + background-color: transparent; + cursor: pointer; + font-family: inherit; + line-height: 1.1; padding: 0; } diff --git a/src/main/resources/static/js/date.js b/src/main/resources/static/js/date.js index b6007db..10ac640 100644 --- a/src/main/resources/static/js/date.js +++ b/src/main/resources/static/js/date.js @@ -13,23 +13,21 @@ function getStepSize() { return step; } -function updateDatePage() { - const allButtons = document.querySelectorAll("#dateButtonList .day"); - const container = document.querySelector("#dateButtonList"); - +function scrollToIndex(index) { + const wrapper = document.querySelector(".date-button-wrapper"); const step = getStepSize(); - const offset = currentIndex * step; - container.style.transform = `translateX(-${offset}px)`; + const offset = index * step; - const maxIndex = allButtons.length - itemsPerPage; - document.getElementById("prevBtn").disabled = currentIndex <= 0; - document.getElementById("nextBtn").disabled = currentIndex >= maxIndex; + wrapper.scrollTo({ + left: offset, + behavior: 'smooth' + }); } document.getElementById("prevBtn").addEventListener("click", () => { if (currentIndex > 0) { currentIndex -= itemsPerSlide; - updateDatePage(); + scrollToIndex(currentIndex) } }); @@ -38,7 +36,7 @@ document.getElementById("nextBtn").addEventListener("click", () => { const maxIndex = allButtons.length - itemsPerPage; if (currentIndex < maxIndex) { currentIndex += itemsPerSlide; - updateDatePage(); + scrollToIndex(currentIndex) } }); diff --git a/src/main/resources/static/js/home.js b/src/main/resources/static/js/home.js index 294f22f..a23059d 100644 --- a/src/main/resources/static/js/home.js +++ b/src/main/resources/static/js/home.js @@ -1,6 +1,6 @@ function initializeDateUI() { resizeDayButtons(); - updateDatePage(); + scrollToIndex(0); } function setupDateSelection() { From c2b90df570839fc1eb90c78e1c175581de9d1c34 Mon Sep 17 00:00:00 2001 From: Junha Date: Wed, 7 May 2025 22:17:24 +0900 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=88=84=EB=A5=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=9C=BC=EB=A9=B4=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/js/filter.js | 85 +++++++++++++++++++++---- src/main/resources/templates/index.html | 2 +- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/main/resources/static/js/filter.js b/src/main/resources/static/js/filter.js index e8c1d40..5e80125 100644 --- a/src/main/resources/static/js/filter.js +++ b/src/main/resources/static/js/filter.js @@ -1,48 +1,109 @@ +const filterState = {}; // ex: { gender: { all: [...], unchecked: [...] }, platform: { ... } } + + +// 필터 상태 초기화 (최초 1회) +function initFilterState(modalId) { + const modal = document.getElementById(modalId); + const checkboxes = modal.querySelectorAll("input[type=checkbox]"); + const all = Array.from(checkboxes).map(cb => cb.value); + const unchecked = all.filter(value => !modal.querySelector(`input[value="${value}"]`).checked); + + filterState[modalId] = { all, unchecked }; +} + +// 체크박스 렌더링 (모달 열기 시) +function renderFilterState(modalId) { + const state = filterState[modalId]; + const modal = document.getElementById(modalId); + if (!state) return; + + //unchecked 에 포함되지 않은 값들만 체크한다. + modal.querySelectorAll("input[type=checkbox]").forEach(cb => { + cb.checked = !state.unchecked.includes(cb.value); + }); +} + +// 선택된 상태 저장 (적용하기 버튼) +function saveFilterState(modalId) { + const modal = document.getElementById(modalId); + const unchecked = []; + + modal.querySelectorAll("input[type=checkbox]").forEach(cb => { + if (!cb.checked) unchecked.push(cb.value); + }); + + filterState[modalId].unchecked = unchecked; +} + document.addEventListener("DOMContentLoaded", () => { - // 열기 버튼 + // 필터 모달 초기화 (모달 id 기준) + // 모든 .modal-overlay를 자동 탐색하여 초기화 + document.querySelectorAll(".modal-overlay").forEach(modal => { + initFilterState(modal.id); + }); + + // 모달 열기 버튼 document.querySelectorAll("[data-modal-target]").forEach(btn => { btn.addEventListener("click", () => { const selector = btn.getAttribute("data-modal-target"); const modal = document.querySelector(selector); - modal?.classList.add("active"); + if (modal) { + renderFilterState(modal.id); // 상태 복원 + modal.classList.add("active"); + //CSS .modal-overlay.active -> 화면에 display 되도록, modal-overlay는 선택시 나타나는 전체 회색배경. + //이 배경이 active 되면 내부 modal-content도 함께 나타난다. + } }); }); - // 닫기 버튼 + // 모달 닫기 버튼 document.querySelectorAll(".modal-overlay .close-btn").forEach(btn => { btn.addEventListener("click", () => { const modal = btn.closest(".modal-overlay"); - modal?.classList.remove("active"); + if (modal) { + //닫기 버튼 클릭시 체크 박스 상태는 저장하지 않는다. + modal.classList.remove("active"); + } }); }); - // 바깥 클릭 시 닫기 + // 모달 바깥 클릭 닫기 document.querySelectorAll(".modal-overlay").forEach(modal => { modal.addEventListener("click", (e) => { if (e.target === modal) { + //바깥 클릭으로 닫을시 체크 박스 상태는 저장하지 않는다. modal.classList.remove("active"); } }); }); - // 적용 버튼 + // 적용하기 버튼 클릭 → 저장 document.querySelectorAll(".modal-overlay .apply-btn").forEach(btn => { btn.addEventListener("click", () => { const modal = btn.closest(".modal-overlay"); - const values = Array.from(modal.querySelectorAll("input[type=checkbox]:checked")) - .map(cb => cb.value); + if (modal) { + saveFilterState(modal.id); // 상태 저장 + modal.classList.remove("active"); - console.log(`[${modal.id}] 선택된 항목:`, values); - // 여기에 실제 필터 적용 로직 추가 가능 - modal.classList.remove("active"); + // 필요 시 여기에 실제 필터링 호출 + console.log(`[${modal.id}] 필터 적용됨:`, getActiveValues(modal.id)); + } }); }); }) +// 현재 적용된 항목만 가져오는 헬퍼 함수 +function getActiveValues(modalId) { + const state = filterState[modalId]; + if (!state) return []; + return state.all.filter(value => !state.unchecked.includes(value)); +} + document.getElementById("hideFullToggle").addEventListener("click", function () { this.classList.toggle("active"); const isActive = this.classList.contains("active"); // 실제 필터링 로직이 있다면 여기서 호출 // 예: toggleFullMatches(isActive); -}); \ No newline at end of file +}); + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index f285086..56cdeee 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -50,7 +50,7 @@

- +