From df8ae62b0e5194b9de0f3b5ecd42c618ac58f3d0 Mon Sep 17 00:00:00 2001 From: leeyj Date: Thu, 16 Apr 2026 11:27:25 +0900 Subject: [PATCH] Fix date filtering bug in heatmap/calendar and sync selection state --- app/routes/memo.py | 2 ++ docs/Bug/20260416_date_filter_failure.md | 21 ++++++++++++++++++ static/app.js | 5 ++++- static/css/components/heatmap.css | 9 ++++++++ static/js/AppService.js | 6 +++++ static/js/api.js | 3 ++- static/js/components/CalendarManager.js | 5 +++++ static/js/components/HeatmapManager.js | 28 ++++++++++++++++++++++-- 8 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 docs/Bug/20260416_date_filter_failure.md diff --git a/app/routes/memo.py b/app/routes/memo.py index a4f3b73..aaee8aa 100644 --- a/app/routes/memo.py +++ b/app/routes/memo.py @@ -17,6 +17,8 @@ def get_memos(): group = request.args.get('group', 'all') query = request.args.get('query', '') date = request.args.get('date', '') + if date in ('null', 'undefined'): + date = '' conn = get_db() c = conn.cursor() diff --git a/docs/Bug/20260416_date_filter_failure.md b/docs/Bug/20260416_date_filter_failure.md new file mode 100644 index 0000000..a9cb1b3 --- /dev/null +++ b/docs/Bug/20260416_date_filter_failure.md @@ -0,0 +1,21 @@ +# 버그 리포트: 히트맵 및 달력 날짜 필터링 실패 + +## 버그 내용 +- **현상**: 히트맵이나 달력에서 특정 날짜를 클릭했을 때, 해당 날짜의 메모만 필터링되어야 하나 전체 메모가 그대로 노출되는 현상. +- **원인**: + 1. 프론트엔드 API 호출 시 `date` 파라미터가 누락됨 (`static/js/api.js`). + 2. 히트맵 컴포넌트에 클릭 이벤트 리스너가 구현되지 않음 (`static/js/components/HeatmapManager.js`). + 3. 달력과 히트맵 간의 선택 상태 동기화 로직 부재. + +## 조치 사항 +1. **API 수정**: `static/js/api.js`의 `fetchMemos` 함수가 `filters.date`를 지원하도록 수정하고, `null` 값이 `"null"` 문자열로 전송되지 않도록 빈 문자열 처리 추가. +2. **백엔드 보완**: `app/routes/memo.py`에서 `date` 값이 `"null"` 또는 `"undefined"`로 들어올 경우 예외 처리 추가. +3. **히트맵 개선**: + - 각 날짜 셀에 클릭 이벤트 추가. + - `setSelectedDate` 메서드를 추가하여 외부에서 선택 상태를 주입할 수 있도록 함. + - 선택된 날짜에 대한 시각적 강조 스타일 추가 (`static/css/components/heatmap.css`). +4. **상태 동기화**: `AppService.js`의 `setFilter` 로직에서 날짜가 변경될 때 달력과 히트맵의 선택 상태를 동시에 업데이트하도록 수정. + +## 향후 주의사항 +- 필터링 기능을 추가하거나 수정할 때는 `AppService.state`와 실제 UI(달력, 히트맵, 사이드바 등) 간의 데이터 흐름이 일치하는지 확인해야 함. +- 새로운 API 요청 파라미터를 추가할 때는 `api.js` 모듈에서 해당 파라미터가 올바르게 인코딩되어 전달되는지 반드시 검증할 것. diff --git a/static/app.js b/static/app.js index e590d9e..6e12720 100644 --- a/static/app.js +++ b/static/app.js @@ -22,7 +22,10 @@ document.addEventListener('DOMContentLoaded', async () => { // 작성기 초기화 (저장 성공 시 데이터 새로고침 콜백 등록) ComposerManager.init(() => AppService.refreshData(updateSidebarCallback)); - HeatmapManager.init('heatmapContainer'); // 히트맵 초기화 + // 히트맵 초기화 + HeatmapManager.init('heatmapContainer', (date) => { + AppService.setFilter({ date }, updateSidebarCallback); + }); DrawerManager.init(); Visualizer.init('graphContainer'); UI.initSidebarToggle(); diff --git a/static/css/components/heatmap.css b/static/css/components/heatmap.css index 4d332c0..df0a80c 100644 --- a/static/css/components/heatmap.css +++ b/static/css/components/heatmap.css @@ -77,6 +77,15 @@ cursor: pointer; } +.heatmap-cell.selected { + transform: scale(1.4); + z-index: 15; + outline: 2px solid white; + box-shadow: 0 0 10px var(--accent); + filter: brightness(1.5); +} + + .heatmap-cell.out { opacity: 0; pointer-events: none; diff --git a/static/js/AppService.js b/static/js/AppService.js index f095863..fdc2d1f 100644 --- a/static/js/AppService.js +++ b/static/js/AppService.js @@ -99,6 +99,12 @@ export const AppService = { if (date !== undefined && this.state.currentFilterDate !== date) { this.state.currentFilterDate = date; changed = true; + + // UI 동기화 + CalendarManager.setSelectedDate(date); + if (HeatmapManager.setSelectedDate) { + HeatmapManager.setSelectedDate(date); + } } if (query !== undefined && this.state.currentSearchQuery !== query) { this.state.currentSearchQuery = query; diff --git a/static/js/api.js b/static/js/api.js index 0b1ca03..3c21ae6 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -18,7 +18,8 @@ export const API = { async fetchMemos(filters = {}) { const { limit = 20, offset = 0, group = 'all', query = '' } = filters; - const params = new URLSearchParams({ limit, offset, group, query }); + const date = filters.date || ''; // null이나 undefined를 빈 문자열로 변환 + const params = new URLSearchParams({ limit, offset, group, query, date }); return await this.request(`/api/memos?${params.toString()}`); }, async fetchHeatmapData(days = 365) { diff --git a/static/js/components/CalendarManager.js b/static/js/components/CalendarManager.js index 6bb364a..3eb1cdf 100644 --- a/static/js/components/CalendarManager.js +++ b/static/js/components/CalendarManager.js @@ -34,6 +34,11 @@ export const CalendarManager = { this.render(); }, + setSelectedDate(date) { + this.selectedDate = date; + this.render(); + }, + bindEvents() { const header = document.getElementById('calendarHeader'); if (header) { diff --git a/static/js/components/HeatmapManager.js b/static/js/components/HeatmapManager.js index 5b6bd03..7fbe100 100644 --- a/static/js/components/HeatmapManager.js +++ b/static/js/components/HeatmapManager.js @@ -8,9 +8,13 @@ export const HeatmapManager = { container: null, data: [], // [{date: 'YYYY-MM-DD', count: N}, ...] currentRange: 365, // 기본 365일 + selectedDate: null, + onDateSelect: null, - init(containerId) { + init(containerId, onDateSelect) { this.container = document.getElementById(containerId); + this.onDateSelect = onDateSelect; + if (!this.container) { console.warn('[Heatmap] Container not found:', containerId); return; @@ -23,6 +27,11 @@ export const HeatmapManager = { } }, + setSelectedDate(date) { + this.selectedDate = date; + this.render(); + }, + /** * 데이터를 서버에서 가져와 렌더링합니다. */ @@ -95,13 +104,14 @@ export const HeatmapManager = { const level = this.calculateLevel(count); const isOutOfRange = currentDate < startDate || currentDate > today; + const isSelected = this.selectedDate === dateStr; const tooltip = I18nManager.t('tooltip_heatmap_stat') .replace('{date}', dateStr) .replace('{count}', count); html += ` -
@@ -144,5 +154,19 @@ export const HeatmapManager = { this.refresh(); }; } + + // 날짜 셀 클릭 이벤트 추가 + this.container.querySelectorAll('.heatmap-cell[data-date]').forEach(cell => { + cell.onclick = (e) => { + const date = cell.dataset.date; + if (this.selectedDate === date) { + this.selectedDate = null; // 해제 + } else { + this.selectedDate = date; // 선택 + } + this.render(); // 다시 그려서 선택 효과 표시 + if (this.onDateSelect) this.onDateSelect(this.selectedDate); + }; + }); } };