mirror of
https://github.com/sotam0316/brain_dogfood.git
synced 2026-04-25 03:48:38 +09:00
Fix date filtering bug in heatmap/calendar and sync selection state
This commit is contained in:
@@ -17,6 +17,8 @@ def get_memos():
|
|||||||
group = request.args.get('group', 'all')
|
group = request.args.get('group', 'all')
|
||||||
query = request.args.get('query', '')
|
query = request.args.get('query', '')
|
||||||
date = request.args.get('date', '')
|
date = request.args.get('date', '')
|
||||||
|
if date in ('null', 'undefined'):
|
||||||
|
date = ''
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|||||||
@@ -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` 모듈에서 해당 파라미터가 올바르게 인코딩되어 전달되는지 반드시 검증할 것.
|
||||||
+4
-1
@@ -22,7 +22,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// 작성기 초기화 (저장 성공 시 데이터 새로고침 콜백 등록)
|
// 작성기 초기화 (저장 성공 시 데이터 새로고침 콜백 등록)
|
||||||
ComposerManager.init(() => AppService.refreshData(updateSidebarCallback));
|
ComposerManager.init(() => AppService.refreshData(updateSidebarCallback));
|
||||||
|
|
||||||
HeatmapManager.init('heatmapContainer'); // 히트맵 초기화
|
// 히트맵 초기화
|
||||||
|
HeatmapManager.init('heatmapContainer', (date) => {
|
||||||
|
AppService.setFilter({ date }, updateSidebarCallback);
|
||||||
|
});
|
||||||
DrawerManager.init();
|
DrawerManager.init();
|
||||||
Visualizer.init('graphContainer');
|
Visualizer.init('graphContainer');
|
||||||
UI.initSidebarToggle();
|
UI.initSidebarToggle();
|
||||||
|
|||||||
@@ -77,6 +77,15 @@
|
|||||||
cursor: pointer;
|
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 {
|
.heatmap-cell.out {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export const AppService = {
|
|||||||
if (date !== undefined && this.state.currentFilterDate !== date) {
|
if (date !== undefined && this.state.currentFilterDate !== date) {
|
||||||
this.state.currentFilterDate = date;
|
this.state.currentFilterDate = date;
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
||||||
|
// UI 동기화
|
||||||
|
CalendarManager.setSelectedDate(date);
|
||||||
|
if (HeatmapManager.setSelectedDate) {
|
||||||
|
HeatmapManager.setSelectedDate(date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (query !== undefined && this.state.currentSearchQuery !== query) {
|
if (query !== undefined && this.state.currentSearchQuery !== query) {
|
||||||
this.state.currentSearchQuery = query;
|
this.state.currentSearchQuery = query;
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@ export const API = {
|
|||||||
|
|
||||||
async fetchMemos(filters = {}) {
|
async fetchMemos(filters = {}) {
|
||||||
const { limit = 20, offset = 0, group = 'all', query = '' } = 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()}`);
|
return await this.request(`/api/memos?${params.toString()}`);
|
||||||
},
|
},
|
||||||
async fetchHeatmapData(days = 365) {
|
async fetchHeatmapData(days = 365) {
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export const CalendarManager = {
|
|||||||
this.render();
|
this.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setSelectedDate(date) {
|
||||||
|
this.selectedDate = date;
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
const header = document.getElementById('calendarHeader');
|
const header = document.getElementById('calendarHeader');
|
||||||
if (header) {
|
if (header) {
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ export const HeatmapManager = {
|
|||||||
container: null,
|
container: null,
|
||||||
data: [], // [{date: 'YYYY-MM-DD', count: N}, ...]
|
data: [], // [{date: 'YYYY-MM-DD', count: N}, ...]
|
||||||
currentRange: 365, // 기본 365일
|
currentRange: 365, // 기본 365일
|
||||||
|
selectedDate: null,
|
||||||
|
onDateSelect: null,
|
||||||
|
|
||||||
init(containerId) {
|
init(containerId, onDateSelect) {
|
||||||
this.container = document.getElementById(containerId);
|
this.container = document.getElementById(containerId);
|
||||||
|
this.onDateSelect = onDateSelect;
|
||||||
|
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
console.warn('[Heatmap] Container not found:', containerId);
|
console.warn('[Heatmap] Container not found:', containerId);
|
||||||
return;
|
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 level = this.calculateLevel(count);
|
||||||
|
|
||||||
const isOutOfRange = currentDate < startDate || currentDate > today;
|
const isOutOfRange = currentDate < startDate || currentDate > today;
|
||||||
|
const isSelected = this.selectedDate === dateStr;
|
||||||
|
|
||||||
const tooltip = I18nManager.t('tooltip_heatmap_stat')
|
const tooltip = I18nManager.t('tooltip_heatmap_stat')
|
||||||
.replace('{date}', dateStr)
|
.replace('{date}', dateStr)
|
||||||
.replace('{count}', count);
|
.replace('{count}', count);
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="heatmap-cell ${isOutOfRange ? 'out' : `lvl-${level}`}"
|
<div class="heatmap-cell ${isOutOfRange ? 'out' : `lvl-${level}`} ${isSelected ? 'selected' : ''}"
|
||||||
data-date="${dateStr}"
|
data-date="${dateStr}"
|
||||||
data-count="${count}"
|
data-count="${count}"
|
||||||
title="${tooltip}">
|
title="${tooltip}">
|
||||||
@@ -144,5 +154,19 @@ export const HeatmapManager = {
|
|||||||
this.refresh();
|
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);
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user