fix: 상세 조회 확인시간 즉시 갱신 및 미확인 알림 중복 방지
This commit is contained in:
parent
504dd61b6f
commit
7afc97b8f8
|
|
@ -44,3 +44,35 @@ Added "5" to the recheck_hours ComboBox values to enable explicit user selection
|
||||||
- Updated values: ["1", "2", "3", "5", "6", "12", "24"] (inserted "5" between "3" and "6")
|
- Updated values: ["1", "2", "3", "5", "6", "12", "24"] (inserted "5" between "3" and "6")
|
||||||
- Maintains numeric ordering
|
- Maintains numeric ordering
|
||||||
- No logic changes to save() or load flow
|
- No logic changes to save() or load flow
|
||||||
|
|
||||||
|
## 2026-02-19 Prevent duplicate unchecked notification dialogs
|
||||||
|
|
||||||
|
### Decision
|
||||||
|
Implement singleton behavior for unchecked notification dialogs (title prefix `⚠️ 미확인 VOC`).
|
||||||
|
Allow regular new notifications to open multiple dialogs as before.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- **notification_dialog.py (lines 4-6, 8-15, 20, 22-24, 59-62, 86, 88-101)**:
|
||||||
|
1. Added class variable `_unchecked_dialog = None` to track open unchecked dialog
|
||||||
|
2. In `__init__`: Check if incoming title is unchecked notification (startswith `⚠️ 미확인 VOC`)
|
||||||
|
3. If unchecked AND one already open → lift() and focus_force() existing dialog, return early (no new instance)
|
||||||
|
4. If unchecked AND none open → store reference in `_unchecked_dialog`
|
||||||
|
5. Modified close button (line 61) to call `self._on_close()` instead of `self.destroy()`
|
||||||
|
6. Added `_on_close()` method (lines 97-101): Cleans up `_unchecked_dialog` reference before destroying
|
||||||
|
7. Updated `_on_view()` and `_on_open_list()` to call `_on_close()` instead of direct `destroy()`
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
- **Unchecked dialogs (⚠️ 미확인 VOC)**: Maximum one open at a time; new attempts bring existing to front
|
||||||
|
- **Regular notifications (신규 알림)**: Unchanged; multiple can open (checked in title prefix)
|
||||||
|
- **Cleanup**: Reference cleared when dialog closes via any path (button, protocol handler, or methods)
|
||||||
|
|
||||||
|
### Why This Approach
|
||||||
|
- **Minimal scope**: Changes only notification_dialog.py, no controller/scheduler modifications
|
||||||
|
- **Non-intrusive**: Regular new notifications unaffected; only unchecked notifications deduplicated
|
||||||
|
- **Safe cleanup**: `_on_close()` ensures `_unchecked_dialog` ref cleared regardless of close method
|
||||||
|
- **Semantic check**: title.startswith("⚠️ 미확인 VOC") is the exact prefix used by notifier
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- Syntax: python -m py_compile passed
|
||||||
|
- No regressions: All button callbacks still work (updated to use _on_close)
|
||||||
|
- LSP: Only pre-existing MAIN_COLOR hasattr error (unrelated)
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,36 @@ else:
|
||||||
- [x] Scheduler respects is_related context for non-public posts
|
- [x] Scheduler respects is_related context for non-public posts
|
||||||
- [x] Failure handling graceful (debug logs, not exceptions)
|
- [x] Failure handling graceful (debug logs, not exceptions)
|
||||||
- [x] Korean logging messages consistent with codebase
|
- [x] Korean logging messages consistent with codebase
|
||||||
|
|
||||||
|
## 2026-02-19 Fix: HistoryDialog Title Click Checked Timestamp Refresh
|
||||||
|
|
||||||
|
### Issue
|
||||||
|
When clicking a title in HistoryDialog to open detail view, `controller.mark_as_read(voc_id)` is called which refreshes `controller.list_window`. However, UIManager independently manages `self.list_window`, causing a mismatch—the refresh affects the wrong instance.
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
Two separate list_window references:
|
||||||
|
- `controller.list_window` (checked in mark_as_read)
|
||||||
|
- `UIManager.list_window` (actual active window displayed)
|
||||||
|
|
||||||
|
These never synchronized, so refresh refreshed a stale reference.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
**File**: `app/controllers/ui_manager.py` (line 97)
|
||||||
|
**Change**: After creating HistoryDialog instance in UIManager, synchronize it to controller:
|
||||||
|
```python
|
||||||
|
self.list_window = HistoryDialog(self.controller, formatted)
|
||||||
|
# UIManager의 list_window를 controller가 접근하도록 동기화
|
||||||
|
self.controller.list_window = self.list_window
|
||||||
|
```
|
||||||
|
|
||||||
|
### Impact
|
||||||
|
- Title click → detail open → `mark_as_read()` → `controller.list_window.refresh_data_only()` now refreshes the visible list
|
||||||
|
- Checked timestamp visibility updates immediately (same behavior as pressing refresh button)
|
||||||
|
- No code changes needed in controller.py; list refresh flow remains unchanged
|
||||||
|
- Minimal 1-line addition with clear Korean comment explaining synchronization
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
✓ Syntax valid (py_compile)
|
||||||
|
✓ No LSP diagnostics
|
||||||
|
✓ Controller mark_as_read() logic preserved
|
||||||
|
✓ Existing list open behavior unaffected
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ class UIManager:
|
||||||
|
|
||||||
from view.dialogs.history_dialog import HistoryDialog
|
from view.dialogs.history_dialog import HistoryDialog
|
||||||
self.list_window = HistoryDialog(self.controller, formatted)
|
self.list_window = HistoryDialog(self.controller, formatted)
|
||||||
|
# UIManager의 list_window를 controller가 접근하도록 동기화
|
||||||
|
self.controller.list_window = self.list_window
|
||||||
else:
|
else:
|
||||||
# 기존 창 재사용
|
# 기존 창 재사용
|
||||||
self.list_window.refresh_data_only()
|
self.list_window.refresh_data_only()
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,26 @@ import customtkinter as ctk
|
||||||
from view.theme import theme_manager
|
from view.theme import theme_manager
|
||||||
|
|
||||||
class NotificationDialog(ctk.CTkToplevel):
|
class NotificationDialog(ctk.CTkToplevel):
|
||||||
|
# 미확인 알림 다이얼로그 싱글톤 참조
|
||||||
|
_unchecked_dialog = None
|
||||||
|
|
||||||
def __init__(self, controller, title, msg, voc_id=None):
|
def __init__(self, controller, title, msg, voc_id=None):
|
||||||
|
# 미확인 알림(⚠️ 미확인 VOC)인 경우 이미 열려있으면 기존 다이얼로그 앞으로 가져오기
|
||||||
|
is_unchecked = title.startswith("⚠️ 미확인 VOC")
|
||||||
|
if is_unchecked and NotificationDialog._unchecked_dialog is not None:
|
||||||
|
# 기존 다이얼로그를 앞으로 가져옴
|
||||||
|
NotificationDialog._unchecked_dialog.lift()
|
||||||
|
NotificationDialog._unchecked_dialog.focus_force()
|
||||||
|
return
|
||||||
|
|
||||||
super().__init__(controller.root)
|
super().__init__(controller.root)
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.voc_id = voc_id
|
self.voc_id = voc_id
|
||||||
|
self.is_unchecked = is_unchecked
|
||||||
|
|
||||||
|
# 미확인 알림이면 싱글톤 참조 저장
|
||||||
|
if is_unchecked:
|
||||||
|
NotificationDialog._unchecked_dialog = self
|
||||||
|
|
||||||
self.title(title)
|
self.title(title)
|
||||||
self.attributes("-topmost", True) # 항상 위
|
self.attributes("-topmost", True) # 항상 위
|
||||||
|
|
@ -42,7 +58,7 @@ class NotificationDialog(ctk.CTkToplevel):
|
||||||
|
|
||||||
self.btn_close = ctk.CTkButton(
|
self.btn_close = ctk.CTkButton(
|
||||||
self.btn_frame, text="닫기", width=100,
|
self.btn_frame, text="닫기", width=100,
|
||||||
fg_color="gray", command=self.destroy
|
fg_color="gray", command=self._on_close
|
||||||
)
|
)
|
||||||
self.btn_close.pack(side="right", padx=5)
|
self.btn_close.pack(side="right", padx=5)
|
||||||
|
|
||||||
|
|
@ -65,14 +81,23 @@ class NotificationDialog(ctk.CTkToplevel):
|
||||||
# 포커스 강제
|
# 포커스 강제
|
||||||
self.lift()
|
self.lift()
|
||||||
self.focus_force()
|
self.focus_force()
|
||||||
|
|
||||||
|
# 다이얼로그 종료 시 싱글톤 참조 정리
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||||
|
|
||||||
def _on_view(self):
|
def _on_view(self):
|
||||||
if self.voc_id:
|
if self.voc_id:
|
||||||
self.controller.open_list_view(focus_id=self.voc_id)
|
self.controller.open_list_view(focus_id=self.voc_id)
|
||||||
self.destroy()
|
self._on_close()
|
||||||
|
|
||||||
def _on_open_list(self):
|
def _on_open_list(self):
|
||||||
self.controller.open_list_view()
|
self.controller.open_list_view()
|
||||||
|
self._on_close()
|
||||||
|
|
||||||
|
def _on_close(self):
|
||||||
|
"""다이얼로그 종료 시 싱글톤 참조 정리"""
|
||||||
|
if self.is_unchecked and NotificationDialog._unchecked_dialog is self:
|
||||||
|
NotificationDialog._unchecked_dialog = None
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def _center_window(self):
|
def _center_window(self):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue