feat: 관리함 진행중 페이지 상세 내용 편집 기능 구현

📝 상세 내용 인라인 편집 시스템:

🎨 프론트엔드 UI 개선:
- 상세 내용 섹션에 '수정' 버튼 추가
- 읽기 모드 ↔ 편집 모드 토글 기능
- 편집 시 텍스트 영역으로 전환
- 취소/저장 버튼으로 편집 제어
- 실시간 UI 업데이트

🔧 백엔드 API 확장:
- PUT /api/management/{issue_id} 엔드포인트 추가
- ManagementUpdateRequest에 final_description 필드 추가
- 진행중 상태 이슈만 수정 가능하도록 제한
- 권한 검증 및 오류 처리

💡 핵심 기능:
- 부적합명은 유지하고 상세 내용만 수정
- 수신함에서 입력한 상세 부분을 관리함에서 보완 가능
- 원본 데이터와 수정 데이터 자동 결합
- 실시간 저장 및 화면 반영

🔐 보안 및 제한사항:
- 관리함 페이지 권한 필요
- 진행중 상태 이슈만 편집 가능
- 완료된 이슈는 편집 불가
- 사용자 인증 및 권한 검증

🎯 사용 시나리오:
1. 관리함 진행중 탭에서 '수정' 버튼 클릭
2. 텍스트 영역에서 상세 내용 편집
3. 저장 시 부적합명과 자동 결합
4. 실시간으로 화면에 반영

Expected Result:
 수신함 검토 후 관리함에서 상세 내용 보완 가능
 직관적인 인라인 편집 인터페이스
 데이터 일관성 유지 (부적합명 + 상세 내용)
 안전한 권한 기반 편집 제어
This commit is contained in:
Hyungi Ahn
2025-10-26 12:36:35 +09:00
parent 151d1cc875
commit 7caf36c856
5 changed files with 157 additions and 4 deletions

View File

@@ -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):
"""추가 정보 업데이트 요청 (관리함 진행중에서 사용)"""

View File

@@ -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,

View File

@@ -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>
<!-- 추가 정보 입력 모달 -->