feat: 관리함 진행중 페이지 상세 내용 편집 기능 구현
📝 상세 내용 인라인 편집 시스템: 🎨 프론트엔드 UI 개선: - 상세 내용 섹션에 '수정' 버튼 추가 - 읽기 모드 ↔ 편집 모드 토글 기능 - 편집 시 텍스트 영역으로 전환 - 취소/저장 버튼으로 편집 제어 - 실시간 UI 업데이트 🔧 백엔드 API 확장: - PUT /api/management/{issue_id} 엔드포인트 추가 - ManagementUpdateRequest에 final_description 필드 추가 - 진행중 상태 이슈만 수정 가능하도록 제한 - 권한 검증 및 오류 처리 💡 핵심 기능: - 부적합명은 유지하고 상세 내용만 수정 - 수신함에서 입력한 상세 부분을 관리함에서 보완 가능 - 원본 데이터와 수정 데이터 자동 결합 - 실시간 저장 및 화면 반영 🔐 보안 및 제한사항: - 관리함 페이지 권한 필요 - 진행중 상태 이슈만 편집 가능 - 완료된 이슈는 편집 불가 - 사용자 인증 및 권한 검증 🎯 사용 시나리오: 1. 관리함 진행중 탭에서 '수정' 버튼 클릭 2. 텍스트 영역에서 상세 내용 편집 3. 저장 시 부적합명과 자동 결합 4. 실시간으로 화면에 반영 Expected Result: ✅ 수신함 검토 후 관리함에서 상세 내용 보완 가능 ✅ 직관적인 인라인 편집 인터페이스 ✅ 데이터 일관성 유지 (부적합명 + 상세 내용) ✅ 안전한 권한 기반 편집 제어
This commit is contained in:
Binary file not shown.
@@ -181,6 +181,7 @@ class ManagementUpdateRequest(BaseModel):
|
||||
cause_department: Optional[DepartmentType] = None # 원인부서
|
||||
management_comment: Optional[str] = None # ISSUE에 대한 의견
|
||||
completion_photo: Optional[str] = None # 완료 사진 (Base64)
|
||||
final_description: Optional[str] = None # 최종 내용 (부적합명 + 상세 내용)
|
||||
|
||||
class AdditionalInfoUpdateRequest(BaseModel):
|
||||
"""추가 정보 업데이트 요청 (관리함 진행중에서 사용)"""
|
||||
|
||||
Binary file not shown.
@@ -32,6 +32,47 @@ async def get_management_issues(
|
||||
|
||||
return issues
|
||||
|
||||
@router.put("/{issue_id}")
|
||||
async def update_issue(
|
||||
issue_id: int,
|
||||
update_request: ManagementUpdateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
관리함에서 이슈 정보 업데이트
|
||||
"""
|
||||
# 관리함 페이지 권한 확인
|
||||
if not check_page_access(current_user.id, 'issues_management', db):
|
||||
raise HTTPException(status_code=403, detail="관리함 접근 권한이 없습니다.")
|
||||
|
||||
# 이슈 조회
|
||||
issue = db.query(Issue).filter(Issue.id == issue_id).first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="부적합을 찾을 수 없습니다.")
|
||||
|
||||
# 진행 중 상태인지 확인
|
||||
if issue.review_status != ReviewStatus.in_progress:
|
||||
raise HTTPException(status_code=400, detail="진행 중 상태의 부적합만 수정할 수 있습니다.")
|
||||
|
||||
# 업데이트할 데이터 처리
|
||||
update_data = update_request.dict(exclude_unset=True)
|
||||
|
||||
for field, value in update_data.items():
|
||||
if field == 'completion_photo':
|
||||
# 완료 사진은 별도 처리 (필요시)
|
||||
continue
|
||||
setattr(issue, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(issue)
|
||||
|
||||
return {
|
||||
"message": "이슈가 성공적으로 업데이트되었습니다.",
|
||||
"issue_id": issue.id,
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
|
||||
@router.put("/{issue_id}/additional-info")
|
||||
async def update_additional_info(
|
||||
issue_id: int,
|
||||
|
||||
@@ -739,15 +739,34 @@
|
||||
<!-- 왼쪽: 기본 정보 -->
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fas fa-align-left text-gray-500 mr-2"></i>
|
||||
<label class="text-sm font-medium text-gray-700">상세 내용</label>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-align-left text-gray-500 mr-2"></i>
|
||||
<label class="text-sm font-medium text-gray-700">상세 내용</label>
|
||||
</div>
|
||||
<button onclick="toggleDetailEdit(${issue.id})" class="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 rounded border border-blue-200 hover:bg-blue-50 transition-colors">
|
||||
<i class="fas fa-edit mr-1"></i>수정
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200 min-h-[80px]">
|
||||
<div id="detail-display-${issue.id}" class="p-3 bg-gray-50 rounded-lg border border-gray-200 min-h-[80px]">
|
||||
<div class="text-gray-600 text-sm leading-relaxed italic">
|
||||
${getIssueDetail(issue)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="detail-edit-${issue.id}" class="hidden">
|
||||
<textarea
|
||||
id="detail-textarea-${issue.id}"
|
||||
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-h-[80px] text-sm"
|
||||
placeholder="상세 내용을 입력하세요...">${getIssueDetail(issue)}</textarea>
|
||||
<div class="flex justify-end space-x-2 mt-2">
|
||||
<button onclick="cancelDetailEdit(${issue.id})" class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800 border border-gray-300 rounded hover:bg-gray-50 transition-colors">
|
||||
취소
|
||||
</button>
|
||||
<button onclick="saveDetailEdit(${issue.id})" class="px-3 py-1 text-xs text-white bg-blue-500 hover:bg-blue-600 rounded transition-colors">
|
||||
<i class="fas fa-save mr-1"></i>저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -1524,6 +1543,98 @@
|
||||
alert('저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
|
||||
// 상세 내용 편집 관련 함수들
|
||||
function toggleDetailEdit(issueId) {
|
||||
const displayDiv = document.getElementById(`detail-display-${issueId}`);
|
||||
const editDiv = document.getElementById(`detail-edit-${issueId}`);
|
||||
|
||||
if (displayDiv && editDiv) {
|
||||
displayDiv.classList.add('hidden');
|
||||
editDiv.classList.remove('hidden');
|
||||
|
||||
// 텍스트 영역에 포커스
|
||||
const textarea = document.getElementById(`detail-textarea-${issueId}`);
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDetailEdit(issueId) {
|
||||
const displayDiv = document.getElementById(`detail-display-${issueId}`);
|
||||
const editDiv = document.getElementById(`detail-edit-${issueId}`);
|
||||
|
||||
if (displayDiv && editDiv) {
|
||||
displayDiv.classList.remove('hidden');
|
||||
editDiv.classList.add('hidden');
|
||||
|
||||
// 원래 값으로 복원
|
||||
const issue = allIssues.find(i => i.id === issueId);
|
||||
if (issue) {
|
||||
const textarea = document.getElementById(`detail-textarea-${issueId}`);
|
||||
if (textarea) {
|
||||
textarea.value = getIssueDetail(issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDetailEdit(issueId) {
|
||||
const textarea = document.getElementById(`detail-textarea-${issueId}`);
|
||||
if (!textarea) return;
|
||||
|
||||
const newDetailContent = textarea.value.trim();
|
||||
|
||||
try {
|
||||
// 현재 이슈 정보 가져오기
|
||||
const issue = allIssues.find(i => i.id === issueId);
|
||||
if (!issue) {
|
||||
alert('이슈 정보를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 부적합명과 새로운 상세 내용을 결합
|
||||
const issueTitle = getIssueTitle(issue);
|
||||
const combinedDescription = issueTitle + (newDetailContent ? '\n' + newDetailContent : '');
|
||||
|
||||
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
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// 성공 시 이슈 데이터 업데이트
|
||||
issue.final_description = combinedDescription;
|
||||
|
||||
// 표시 영역 업데이트
|
||||
const displayDiv = document.getElementById(`detail-display-${issueId}`);
|
||||
if (displayDiv) {
|
||||
const contentDiv = displayDiv.querySelector('div');
|
||||
if (contentDiv) {
|
||||
contentDiv.textContent = newDetailContent || '상세 내용 없음';
|
||||
}
|
||||
}
|
||||
|
||||
// 편집 모드 종료
|
||||
cancelDetailEdit(issueId);
|
||||
|
||||
alert('상세 내용이 성공적으로 저장되었습니다.');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('상세 내용 저장 실패:', error);
|
||||
alert('저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 추가 정보 입력 모달 -->
|
||||
|
||||
Reference in New Issue
Block a user