From c68045322707314cee0d8846f55f6ce5032cde27 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Sun, 26 Oct 2025 13:11:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=ED=95=A8=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=8B=AC=20=EB=B0=8F?= =?UTF-8?q?=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=B6=94=EC=A0=81=20?= =?UTF-8?q?=EB=B0=A9=EC=95=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿ”ง ํ†ตํ•ฉ ์ˆ˜์ • ๋ชจ๋‹ฌ: - ๋ชจ๋“  ์ง„ํ–‰ ์ค‘ ์ƒํƒœ์—์„œ 'ํ™•์ธ' ๋ฒ„ํŠผ์œผ๋กœ ๋ชจ๋‹ฌ ์—ด๊ธฐ - ์™„๋ฃŒ ๋Œ€๊ธฐ ์ƒํƒœ์—์„œ๋„ '์ˆ˜์ •' ๋ฒ„ํŠผ์œผ๋กœ ๋™์ผ ๋ชจ๋‹ฌ ์‚ฌ์šฉ - 6์—ด ์™€์ด๋“œ ๋ชจ๋‹ฌ๋กœ ๋ชจ๋“  ์ •๋ณด ํ•œ๋ˆˆ์— ํ‘œ์‹œ ๐Ÿ“ ๋ชจ๋‹ฌ ๊ตฌ์„ฑ: - ์™ผ์ชฝ: ๊ธฐ๋ณธ ์ •๋ณด (ํ”„๋กœ์ ํŠธ, ๋ถ€์ ํ•ฉ๋ช…, ์ƒ์„ธ๋‚ด์šฉ, ์›์ธ๋ถ„๋ฅ˜, ์—…๋กœ๋“œ ์‚ฌ์ง„) - ์˜ค๋ฅธ์ชฝ: ๊ด€๋ฆฌ ์ •๋ณด (ํ•ด๊ฒฐ๋ฐฉ์•ˆ, ๋‹ด๋‹น๋ถ€์„œ/์ž, ์กฐ์น˜์˜ˆ์ƒ์ผ) + ์™„๋ฃŒ ์‹ ์ฒญ ์ •๋ณด ๐ŸŽฏ ๋ฒ„ํŠผ ์‹œ์Šคํ…œ ๊ฐœ์„ : - ์ผ๋ฐ˜ ์ง„ํ–‰ ์ค‘: ์ €์žฅ | ํ™•์ธ | ์™„๋ฃŒ์ฒ˜๋ฆฌ - ์™„๋ฃŒ ๋Œ€๊ธฐ: ๋ฐ˜๋ ค | ์ˆ˜์ • | ์ตœ์ข…ํ™•์ธ - ๋ชจ๋‹ฌ์—์„œ ํ†ตํ•ฉ ์ˆ˜์ • ๊ฐ€๋Šฅ โœ๏ธ ์ˆ˜์ • ๊ธฐ๋Šฅ: - ๋ถ€์ ํ•ฉ๋ช…, ์ƒ์„ธ๋‚ด์šฉ ์ง์ ‘ ์ˆ˜์ • - ํ•ด๊ฒฐ๋ฐฉ์•ˆ, ๋‹ด๋‹น๋ถ€์„œ/์ž, ์กฐ์น˜์˜ˆ์ƒ์ผ ์ˆ˜์ • - ๋ชจ๋‹ฌ์—์„œ ์ €์žฅ ์‹œ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ ๐Ÿ“‹ ํžˆ์Šคํ† ๋ฆฌ ์ถ”์  ๋ฐฉ์•ˆ ๋ฌธ์„œํ™”: - ๋‹จ์ผ ํžˆ์Šคํ† ๋ฆฌ ํ…Œ์ด๋ธ” vs ํŽ˜์ด์ง€๋ณ„ ํ…Œ์ด๋ธ” ๋น„๊ต - ๋ณ€๊ฒฝ ์ด๋ ฅ ๊ธฐ๋ก ์„œ๋น„์Šค ํด๋ž˜์Šค ์„ค๊ณ„ - ํ”„๋ก ํŠธ์—”๋“œ ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ ๋ชจ๋‹ฌ ๊ตฌํ˜„ ๋ฐฉ์•ˆ - ๊ฐ์‚ฌ ์ถ”์ , ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ, ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ ํฌํ•จ ๐Ÿ” ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„: - Phase 1: ๊ธฐ๋ณธ ํžˆ์Šคํ† ๋ฆฌ ํ…Œ์ด๋ธ” + ๊ด€๋ฆฌํ•จ ์ด๋ ฅ - Phase 2: ์ˆ˜์‹ ํ•จ ์ด๋ ฅ + ํžˆ์Šคํ† ๋ฆฌ UI - Phase 3: ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ + ๊ฐ์‚ฌ ๋ณด๊ณ ์„œ ๐Ÿ’ก ์ถ”๊ฐ€ ์•„์ด๋””์–ด: - ๋ณ€๊ฒฝ ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ - ์ž๋™ ๋ฐฑ์—… ์‹œ์Šคํ…œ - ๋ณ€๊ฒฝ ์˜ํ–ฅ๋„ ๋ถ„์„ Expected Result: โœ… ๋ชจ๋“  ์ง„ํ–‰ ์ค‘ ์ƒํƒœ์—์„œ ํ†ตํ•ฉ ์ˆ˜์ • ๋ชจ๋‹ฌ ์‚ฌ์šฉ โœ… ์™„๋ฃŒ ๋Œ€๊ธฐ ์ƒํƒœ ์ •๋ณด ํฌํ•จ ํ‘œ์‹œ โœ… ์ฒด๊ณ„์ ์ธ ํžˆ์Šคํ† ๋ฆฌ ์ถ”์  ๋ฐฉ์•ˆ ์ˆ˜๋ฆฝ โœ… ํˆฌ๋ช…ํ•˜๊ณ  ์ถ”์  ๊ฐ€๋Šฅํ•œ ์ด์Šˆ ๊ด€๋ฆฌ ๊ธฐ๋ฐ˜ ๋งˆ๋ จ --- HISTORY_TRACKING_IDEAS.md | 307 ++++++++++++++++++++++++++++++++ frontend/issues-management.html | 203 ++++++++++++++++++++- 2 files changed, 506 insertions(+), 4 deletions(-) create mode 100644 HISTORY_TRACKING_IDEAS.md 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 = ` +
+
+
+
+

+ + ๋ณ€๊ฒฝ ์ด๋ ฅ - No.${issueId} +

+ +
+ +
+ ${history.map(entry => ` +
+
+
+ ${entry.field_name} + ${entry.page_source} +
+ ${new Date(entry.changed_at).toLocaleString('ko-KR')} +
+
+
+ ์ด์ „: +

${entry.old_value || '์—†์Œ'}

+
+
+ ๋ณ€๊ฒฝ: +

${entry.new_value || '์—†์Œ'}

+
+
+
+ ๋ณ€๊ฒฝ์ž: ${entry.changed_by_name} +
+
+ `).join('')} +
+
+
+
+ `; + + document.body.insertAdjacentHTML('beforeend', modalContent); +} +``` + +## ๐Ÿ“Š ํžˆ์Šคํ† ๋ฆฌ ํ™œ์šฉ ๋ฐฉ์•ˆ + +### 1๏ธโƒฃ ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ +- ์ตœ๊ทผ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์š”์•ฝ +- ์‚ฌ์šฉ์ž๋ณ„ ํ™œ๋™ ํ†ต๊ณ„ +- ํŽ˜์ด์ง€๋ณ„ ์ˆ˜์ • ๋นˆ๋„ + +### 2๏ธโƒฃ ๊ฐ์‚ฌ ๋ณด๊ณ ์„œ +- ํŠน์ • ๊ธฐ๊ฐ„ ๋™์•ˆ์˜ ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ +- ์ค‘์š” ํ•„๋“œ ๋ณ€๊ฒฝ ์•Œ๋ฆผ +- ๊ทœ์ • ์ค€์ˆ˜ ํ™•์ธ + +### 3๏ธโƒฃ ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ +- ์ž˜๋ชป๋œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋กค๋ฐฑ +- ํŠน์ • ์‹œ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ณต์› +- ๋ณ€๊ฒฝ ์‚ฌํ•ญ ๋น„๊ต ๋ฐ ๋ถ„์„ + +## ๐Ÿ”’ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ + +### 1๏ธโƒฃ ์ ‘๊ทผ ๊ถŒํ•œ +- ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ ๊ถŒํ•œ ๋ถ„๋ฆฌ +- ๋ฏผ๊ฐํ•œ ์ •๋ณด ๋งˆ์Šคํ‚น +- ๊ด€๋ฆฌ์ž๋งŒ ์ „์ฒด ํžˆ์Šคํ† ๋ฆฌ ์ ‘๊ทผ + +### 2๏ธโƒฃ ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ +- ํžˆ์Šคํ† ๋ฆฌ ๋ฐ์ดํ„ฐ ์•”ํ˜ธํ™” +- ๊ฐœ์ธ์ •๋ณด ์ž๋™ ์‚ญ์ œ ์ •์ฑ… +- ๋ฐฑ์—… ๋ฐ ์•„์นด์ด๋น™ + +## ๐Ÿš€ ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„ + +### Phase 1 (ํ•„์ˆ˜) +1. ๊ธฐ๋ณธ ํžˆ์Šคํ† ๋ฆฌ ํ…Œ์ด๋ธ” ์ƒ์„ฑ +2. ๊ด€๋ฆฌํ•จ ์ˆ˜์ • ์ด๋ ฅ ๊ธฐ๋ก +3. ๊ฐ„๋‹จํ•œ ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ API + +### Phase 2 (ํ™•์žฅ) +1. ์ˆ˜์‹ ํ•จ ์ฒ˜๋ฆฌ ์ด๋ ฅ ๊ธฐ๋ก +2. ํžˆ์Šคํ† ๋ฆฌ ์กฐํšŒ UI ๊ตฌํ˜„ +3. ๋ณ€๊ฒฝ ์‚ฌ์œ  ์ž…๋ ฅ ๊ธฐ๋Šฅ + +### Phase 3 (๊ณ ๊ธ‰) +1. ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ ๊ธฐ๋Šฅ +2. ๊ฐ์‚ฌ ๋ณด๊ณ ์„œ ์ƒ์„ฑ +3. ์‹ค์‹œ๊ฐ„ ๋ณ€๊ฒฝ ์•Œ๋ฆผ + +## ๐Ÿ’ก ์ถ”๊ฐ€ ์•„์ด๋””์–ด + +### 1๏ธโƒฃ ๋ณ€๊ฒฝ ์Šน์ธ ์›Œํฌํ”Œ๋กœ์šฐ +- ์ค‘์š”ํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ์Šน์ธ ํ›„ ์ ์šฉ +- ๋ณ€๊ฒฝ ์š”์ฒญ โ†’ ๊ฒ€ํ†  โ†’ ์Šน์ธ/๋ฐ˜๋ ค + +### 2๏ธโƒฃ ์ž๋™ ๋ฐฑ์—… +- ์ค‘์š” ๋ณ€๊ฒฝ ์ „ ์ž๋™ ์Šค๋ƒ…์ƒท +- ์ผ์ • ์ฃผ๊ธฐ๋ณ„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋ฐฑ์—… + +### 3๏ธโƒฃ ๋ณ€๊ฒฝ ์˜ํ–ฅ๋„ ๋ถ„์„ +- ์—ฐ๊ด€๋œ ๋‹ค๋ฅธ ์ด์Šˆ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ +- ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ํ†ต๊ณ„ ๋ณ€ํ™” + +์ด๋Ÿฌํ•œ ํžˆ์Šคํ† ๋ฆฌ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด **ํˆฌ๋ช…ํ•˜๊ณ  ์ถ”์  ๊ฐ€๋Šฅํ•œ ์ด์Šˆ ๊ด€๋ฆฌ**๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค! ๐ŸŽฏ diff --git a/frontend/issues-management.html b/frontend/issues-management.html index 64e9c14..d5ee217 100644 --- a/frontend/issues-management.html +++ b/frontend/issues-management.html @@ -786,20 +786,23 @@
${isPendingCompletion ? ` - + ` : ` + @@ -1745,6 +1748,198 @@ } } + // ์ด์Šˆ ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ (๋ชจ๋“  ์ง„ํ–‰ ์ค‘ ์ƒํƒœ์—์„œ ์‚ฌ์šฉ) + function openIssueEditModal(issueId) { + const issue = issues.find(i => i.id === issueId); + if (!issue) return; + + const project = projects.find(p => p.id === issue.project_id); + const isPendingCompletion = issue.completion_requested_at; + + // ๋ชจ๋‹ฌ ๋‚ด์šฉ ์ƒ์„ฑ + const modalContent = ` +
+
+
+ +
+

+ + ์ด์Šˆ ์ˆ˜์ • - No.${issue.project_sequence_no || '-'} +

+ +
+ + +
+ +
+
+

๊ธฐ๋ณธ ์ •๋ณด

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

์—…๋กœ๋“œ ์‚ฌ์ง„

+
+ ${issue.photo_path ? `์—…๋กœ๋“œ ์‚ฌ์ง„ 1` : '
์‚ฌ์ง„ ์—†์Œ
'} + ${issue.photo_path2 ? `์—…๋กœ๋“œ ์‚ฌ์ง„ 2` : '
์‚ฌ์ง„ ์—†์Œ
'} +
+
+
+ + +
+
+

๊ด€๋ฆฌ ์ •๋ณด

+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + ${isPendingCompletion ? ` +
+

์™„๋ฃŒ ์‹ ์ฒญ ์ •๋ณด

+
+
+ + ${issue.completion_photo_path ? ` +
+ ์™„๋ฃŒ ์‚ฌ์ง„ +
+ ` : '

์™„๋ฃŒ ์‚ฌ์ง„ ์—†์Œ

'} +
+
+ +

${issue.completion_comment || '์ฝ”๋ฉ˜ํŠธ ์—†์Œ'}

+
+
+ +

${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}

+
+
+
+ ` : ''} +
+
+ + +
+ + + ${isPendingCompletion ? ` + + ` : ''} +
+
+
+
+ `; + + // ๋ชจ๋‹ฌ์„ body์— ์ถ”๊ฐ€ + document.body.insertAdjacentHTML('beforeend', modalContent); + } + + // ์ด์Šˆ ์ˆ˜์ • ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + function closeIssueEditModal() { + const modal = document.getElementById('issueEditModal'); + if (modal) { + modal.remove(); + } + } + + // ๋ชจ๋‹ฌ์—์„œ ์ด์Šˆ ์ €์žฅ + async function saveIssueFromModal(issueId) { + const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim(); + const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim(); + const solution = document.getElementById(`edit-solution-${issueId}`).value.trim(); + const department = document.getElementById(`edit-department-${issueId}`).value; + const person = document.getElementById(`edit-person-${issueId}`).value.trim(); + const date = document.getElementById(`edit-date-${issueId}`).value; + + if (!title) { + alert('๋ถ€์ ํ•ฉ๋ช…์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'); + return; + } + + const combinedDescription = title + (detail ? '\\n' + detail : ''); + + try { + const response = await fetch(\`/api/management/\${issueId}\`, { + method: 'PUT', + headers: { + 'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + final_description: combinedDescription, + solution: solution || null, + responsible_department: department || null, + responsible_person: person || null, + expected_completion_date: date || null + }) + }); + + if (response.ok) { + alert('์ด์Šˆ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + closeIssueEditModal(); + loadManagementData(); // ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ + } else { + const error = await response.json(); + alert(\`์ €์žฅ ์‹คํŒจ: \${error.detail || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}\`); + } + } catch (error) { + console.error('์ €์žฅ ์˜ค๋ฅ˜:', error); + alert('์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + } + // ์™„๋ฃŒ ๋Œ€๊ธฐ ์ƒํƒœ ๊ด€๋ จ ํ•จ์ˆ˜๋“ค function editIssue(issueId) { // ์ˆ˜์ • ๋ชจ๋“œ๋กœ ์ „ํ™˜ (์™„๋ฃŒ ๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ํ•ด์ œ)