diff --git a/HISTORY_TRACKING_IDEAS.md b/HISTORY_TRACKING_IDEAS.md new file mode 100644 index 0000000..24245b3 --- /dev/null +++ b/HISTORY_TRACKING_IDEAS.md @@ -0,0 +1,307 @@ +# ๐ ์์ ํ์คํ ๋ฆฌ ์ ์ฅ ๋ฐฉ์ + +## ๐ฏ ๋ชฉ์ +- ๊ฐ ํ์ด์ง์์ ์์ ํ ๋ด์ฉ์ ๋ณ๊ฒฝ ์ด๋ ฅ ์ถ์ +- ๋๊ฐ, ์ธ์ , ๋ฌด์์, ์ ๋ณ๊ฒฝํ๋์ง ๊ธฐ๋ก +- ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ฐ ๊ฐ์ฌ ์ถ์ (Audit Trail) ์ ๊ณต + +## ๐๏ธ DB ๊ตฌ์กฐ ๋ฐฉ์ + +### ๋ฐฉ์ 1: ๋จ์ผ ํ์คํ ๋ฆฌ ํ ์ด๋ธ (๊ถ์ฅ) +```sql +CREATE TABLE issue_history ( + id SERIAL PRIMARY KEY, + issue_id INTEGER NOT NULL REFERENCES issues(id), + changed_by_id INTEGER NOT NULL REFERENCES users(id), + changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + change_type VARCHAR(50) NOT NULL, -- 'UPDATE', 'STATUS_CHANGE', 'COMPLETION_REQUEST' ๋ฑ + page_source VARCHAR(50) NOT NULL, -- 'INBOX', 'MANAGEMENT', 'DASHBOARD' ๋ฑ + field_name VARCHAR(100), -- ๋ณ๊ฒฝ๋ ํ๋๋ช + old_value TEXT, -- ์ด์ ๊ฐ (JSON ํํ๋ก ์ ์ฅ ๊ฐ๋ฅ) + new_value TEXT, -- ์๋ก์ด ๊ฐ (JSON ํํ๋ก ์ ์ฅ ๊ฐ๋ฅ) + change_reason TEXT, -- ๋ณ๊ฒฝ ์ฌ์ (์ ํ์ฌํญ) + session_id VARCHAR(100), -- ๋์ผ ์ธ์ ์์ ์ฌ๋ฌ ํ๋ ๋ณ๊ฒฝ ์ ๊ทธ๋ฃนํ + ip_address INET, -- ๋ณ๊ฒฝ์ IP ์ฃผ์ + user_agent TEXT -- ๋ธ๋ผ์ฐ์ ์ ๋ณด +); + +-- ์ธ๋ฑ์ค ์์ฑ +CREATE INDEX idx_issue_history_issue_id ON issue_history(issue_id); +CREATE INDEX idx_issue_history_changed_at ON issue_history(changed_at); +CREATE INDEX idx_issue_history_changed_by ON issue_history(changed_by_id); +``` + +### ๋ฐฉ์ 2: ํ์ด์ง๋ณ ํ์คํ ๋ฆฌ ํ ์ด๋ธ +```sql +-- ์์ ํจ ํ์คํ ๋ฆฌ +CREATE TABLE inbox_history ( + id SERIAL PRIMARY KEY, + issue_id INTEGER NOT NULL REFERENCES issues(id), + action_type VARCHAR(50), -- 'REVIEW', 'DISPOSE', 'STATUS_CHANGE' + old_data JSONB, + new_data JSONB, + changed_by_id INTEGER NOT NULL REFERENCES users(id), + changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- ๊ด๋ฆฌํจ ํ์คํ ๋ฆฌ +CREATE TABLE management_history ( + id SERIAL PRIMARY KEY, + issue_id INTEGER NOT NULL REFERENCES issues(id), + action_type VARCHAR(50), -- 'UPDATE', 'COMPLETION_REQUEST', 'APPROVAL' + old_data JSONB, + new_data JSONB, + changed_by_id INTEGER NOT NULL REFERENCES users(id), + changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +## ๐ง ๊ตฌํ ๋ฐฉ์ + +### 1๏ธโฃ ๋ฐฑ์๋ ๊ตฌํ (FastAPI) + +#### ํ์คํ ๋ฆฌ ์๋น์ค ํด๋์ค +```python +# services/history_service.py +from typing import Dict, Any, Optional +import json +from datetime import datetime +from sqlalchemy.orm import Session + +class HistoryService: + @staticmethod + def log_change( + db: Session, + issue_id: int, + changed_by_id: int, + change_type: str, + page_source: str, + old_data: Dict[str, Any], + new_data: Dict[str, Any], + change_reason: Optional[str] = None, + session_id: Optional[str] = None + ): + """๋ณ๊ฒฝ ์ด๋ ฅ ๊ธฐ๋ก""" + + # ๋ณ๊ฒฝ๋ ํ๋๋ค ์ฐพ๊ธฐ + changed_fields = [] + for field, new_value in new_data.items(): + old_value = old_data.get(field) + if old_value != new_value: + history_entry = IssueHistory( + issue_id=issue_id, + changed_by_id=changed_by_id, + change_type=change_type, + page_source=page_source, + field_name=field, + old_value=json.dumps(old_value) if old_value else None, + new_value=json.dumps(new_value) if new_value else None, + change_reason=change_reason, + session_id=session_id + ) + db.add(history_entry) + changed_fields.append(field) + + db.commit() + return changed_fields + + @staticmethod + def get_issue_history(db: Session, issue_id: int, limit: int = 50): + """์ด์์ ๋ณ๊ฒฝ ์ด๋ ฅ ์กฐํ""" + return db.query(IssueHistory)\ + .filter(IssueHistory.issue_id == issue_id)\ + .order_by(IssueHistory.changed_at.desc())\ + .limit(limit)\ + .all() +``` + +#### API ์๋ํฌ์ธํธ์์ ํ์คํ ๋ฆฌ ๊ธฐ๋ก +```python +# routers/management.py +from services.history_service import HistoryService + +@router.put("/{issue_id}") +async def update_issue( + issue_id: int, + update_request: ManagementUpdateRequest, + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + # ๊ธฐ์กด ์ด์ ๋ฐ์ดํฐ ๋ฐฑ์ + issue = db.query(Issue).filter(Issue.id == issue_id).first() + old_data = { + "final_description": issue.final_description, + "solution": issue.solution, + "responsible_department": issue.responsible_department, + "responsible_person": issue.responsible_person, + "expected_completion_date": issue.expected_completion_date.isoformat() if issue.expected_completion_date else None + } + + # ์ ๋ฐ์ดํธ ์ํ + update_data = update_request.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(issue, field, value) + + db.commit() + db.refresh(issue) + + # ์๋ก์ด ๋ฐ์ดํฐ + new_data = { + "final_description": issue.final_description, + "solution": issue.solution, + "responsible_department": issue.responsible_department, + "responsible_person": issue.responsible_person, + "expected_completion_date": issue.expected_completion_date.isoformat() if issue.expected_completion_date else None + } + + # ํ์คํ ๋ฆฌ ๊ธฐ๋ก + session_id = str(uuid.uuid4()) + HistoryService.log_change( + db=db, + issue_id=issue_id, + changed_by_id=current_user.id, + change_type="UPDATE", + page_source="MANAGEMENT", + old_data=old_data, + new_data=new_data, + session_id=session_id + ) + + return {"message": "์ ๋ฐ์ดํธ ์๋ฃ", "changed_fields": list(update_data.keys())} +``` + +### 2๏ธโฃ ํ๋ก ํธ์๋ ๊ตฌํ + +#### ํ์คํ ๋ฆฌ ์กฐํ ๋ชจ๋ฌ +```javascript +// ํ์คํ ๋ฆฌ ์กฐํ ํจ์ +async function showIssueHistory(issueId) { + try { + const response = await fetch(`/api/issues/${issueId}/history`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}` + } + }); + + if (response.ok) { + const history = await response.json(); + openHistoryModal(issueId, history); + } + } catch (error) { + console.error('ํ์คํ ๋ฆฌ ์กฐํ ์คํจ:', error); + } +} + +// ํ์คํ ๋ฆฌ ๋ชจ๋ฌ ์์ฑ +function openHistoryModal(issueId, history) { + const modalContent = ` +
${entry.old_value || '์์'}
+${entry.new_value || '์์'}
+์๋ฃ ์ฌ์ง ์์
'} +${issue.completion_comment || '์ฝ๋ฉํธ ์์'}
+${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}
+