feat(tkqc): 관리함 이슈 프로젝트 변경 + cause_person 필드명 버그 수정

- 모바일/데스크톱 관리함에서 이슈 소속 프로젝트 변경 가능
- 프로젝트 변경 시 sequence_no 자동 재계산 (DB 함수 사용)
- in_progress 상태에서만 변경 허용 (프론트+백엔드 이중 제한)
- cause_person → responsible_person_detail 필드명 불일치 수정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-27 07:56:20 +09:00
parent ea6f7c3013
commit ac2a2e7eed
4 changed files with 71 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import text, func
from typing import List, Optional from typing import List, Optional
from datetime import datetime from datetime import datetime
@@ -140,6 +141,20 @@ async def update_issue(
# 업데이트 # 업데이트
update_data = issue_update.dict(exclude_unset=True) update_data = issue_update.dict(exclude_unset=True)
# 프로젝트 변경 시 sequence_no 재계산
if "project_id" in update_data and update_data["project_id"] != issue.project_id:
if issue.review_status and issue.review_status != ReviewStatus.in_progress:
raise HTTPException(status_code=400, detail="진행 중 상태에서만 프로젝트를 변경할 수 있습니다")
try:
new_seq = db.execute(text("SELECT generate_project_sequence_no(:pid)"),
{"pid": update_data["project_id"]}).scalar()
update_data["project_sequence_no"] = new_seq
except Exception:
max_seq = db.query(func.coalesce(func.max(Issue.project_sequence_no), 0)).filter(
Issue.project_id == update_data["project_id"]
).scalar()
update_data["project_sequence_no"] = max_seq + 1
# 사진 업데이트 처리 (최대 5장) - 새 사진 저장 후 기존 사진 삭제 (안전) # 사진 업데이트 처리 (최대 5장) - 새 사진 저장 후 기존 사진 삭제 (안전)
old_photos_to_delete = [] old_photos_to_delete = []
for i in range(1, 6): for i in range(1, 6):

View File

@@ -65,6 +65,10 @@
<button class="m-sheet-close" onclick="closeSheet('editMgmt')"><i class="fas fa-times"></i></button> <button class="m-sheet-close" onclick="closeSheet('editMgmt')"><i class="fas fa-times"></i></button>
</div> </div>
<div class="m-sheet-body"> <div class="m-sheet-body">
<div class="m-form-group">
<label class="m-label"><i class="fas fa-folder" style="color:#8b5cf6;margin-right:4px"></i>프로젝트</label>
<select id="editProject" class="m-select"><option value="">선택하세요</option></select>
</div>
<div class="m-form-group"> <div class="m-form-group">
<label class="m-label"><i class="fas fa-lightbulb" style="color:#eab308;margin-right:4px"></i>해결방안 (확정)</label> <label class="m-label"><i class="fas fa-lightbulb" style="color:#eab308;margin-right:4px"></i>해결방안 (확정)</label>
<textarea id="editManagementComment" class="m-textarea" rows="3" placeholder="확정된 해결 방안을 입력하세요..."></textarea> <textarea id="editManagementComment" class="m-textarea" rows="3" placeholder="확정된 해결 방안을 입력하세요..."></textarea>

View File

@@ -253,6 +253,14 @@ function openEditMgmtSheet(issueId) {
var issue = issues.find(function (i) { return i.id === issueId; }); var issue = issues.find(function (i) { return i.id === issueId; });
if (!issue) return; if (!issue) return;
// 프로젝트 셀렉트 채우기
var projSel = document.getElementById('editProject');
projSel.innerHTML = '<option value="">선택하세요</option>';
projects.forEach(function (p) {
projSel.innerHTML += '<option value="' + p.id + '"' + (p.id == issue.project_id ? ' selected' : '') + '>' + escapeHtml(p.project_name || p.job_no) + '</option>';
});
projSel.disabled = (issue.review_status === 'completed');
document.getElementById('editManagementComment').value = cleanManagementComment(issue.management_comment) || ''; document.getElementById('editManagementComment').value = cleanManagementComment(issue.management_comment) || '';
document.getElementById('editResponsibleDept').value = issue.responsible_department || ''; document.getElementById('editResponsibleDept').value = issue.responsible_department || '';
document.getElementById('editResponsiblePerson').value = issue.responsible_person || ''; document.getElementById('editResponsiblePerson').value = issue.responsible_person || '';
@@ -270,6 +278,22 @@ async function saveManagementEdit() {
expected_completion_date: document.getElementById('editExpectedDate').value ? document.getElementById('editExpectedDate').value + 'T00:00:00' : null expected_completion_date: document.getElementById('editExpectedDate').value ? document.getElementById('editExpectedDate').value + 'T00:00:00' : null
}; };
// 프로젝트 변경 확인
var newProjectId = parseInt(document.getElementById('editProject').value);
var issue = issues.find(function (i) { return i.id === currentIssueId; });
if (newProjectId && issue && newProjectId !== issue.project_id) {
// 프로젝트 변경은 /issues/{id} PUT으로 별도 호출
var projResp = await fetch(API_BASE_URL + '/issues/' + currentIssueId, {
method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ project_id: newProjectId })
});
if (!projResp.ok) {
var projErr = await projResp.json();
throw new Error(projErr.detail || '프로젝트 변경 실패');
}
}
var resp = await fetch(API_BASE_URL + '/issues/' + currentIssueId + '/management', { var resp = await fetch(API_BASE_URL + '/issues/' + currentIssueId + '/management', {
method: 'PUT', method: 'PUT',
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
@@ -358,7 +382,7 @@ function loadAdditionalInfo() {
var issue = issues.find(function (i) { return i.id === id; }); var issue = issues.find(function (i) { return i.id === id; });
if (!issue) return; if (!issue) return;
document.getElementById('additionalCauseDept').value = issue.cause_department || ''; document.getElementById('additionalCauseDept').value = issue.cause_department || '';
document.getElementById('additionalCausePerson').value = issue.cause_person || ''; document.getElementById('additionalCausePerson').value = issue.responsible_person_detail || '';
document.getElementById('additionalCauseDetail').value = issue.cause_detail || ''; document.getElementById('additionalCauseDetail').value = issue.cause_detail || '';
} }
@@ -372,7 +396,7 @@ async function saveAdditionalInfo() {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
cause_department: document.getElementById('additionalCauseDept').value || null, cause_department: document.getElementById('additionalCauseDept').value || null,
cause_person: document.getElementById('additionalCausePerson').value.trim() || null, responsible_person_detail: document.getElementById('additionalCausePerson').value.trim() || null,
cause_detail: document.getElementById('additionalCauseDetail').value.trim() || null cause_detail: document.getElementById('additionalCauseDetail').value.trim() || null
}) })
}); });

View File

@@ -66,6 +66,7 @@ async function loadProjects() {
if (response.ok) { if (response.ok) {
projects = await response.json(); projects = await response.json();
window._cachedProjects = projects;
updateProjectFilter(); updateProjectFilter();
} }
} catch (error) { } catch (error) {
@@ -1573,7 +1574,10 @@ function openIssueEditModal(issueId, isCompletionMode = false) {
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label> <label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
<input type="text" value="${project ? project.project_name : '-'}" class="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-lg text-sm" readonly> <select id="edit-issue-project-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"${issue.review_status === 'completed' ? ' disabled' : ''}>
<option value="">선택하세요</option>
${(window._cachedProjects || []).map(p => `<option value="${p.id}"${p.id == issue.project_id ? ' selected' : ''}>${p.project_name || p.job_no}</option>`).join('')}
</select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">부적합명</label> <label class="block text-sm font-medium text-gray-700 mb-1">부적합명</label>
@@ -1845,6 +1849,27 @@ async function saveIssueFromModal(issueId) {
const combinedDescription = title + (detail ? '\n' + detail : ''); const combinedDescription = title + (detail ? '\n' + detail : '');
// 프로젝트 변경 처리
const projectSelect = document.getElementById(`edit-issue-project-${issueId}`);
if (projectSelect) {
const newProjectId = parseInt(projectSelect.value);
const issue = issues.find(i => i.id === issueId);
if (newProjectId && issue && newProjectId !== issue.project_id) {
try {
const projResp = await fetch(`/api/issues/${issueId}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${TokenManager.getToken()}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ project_id: newProjectId })
});
if (!projResp.ok) {
const err = await projResp.json();
showToast(err.detail || '프로젝트 변경 실패', 'error');
return;
}
} catch (e) { showToast('프로젝트 변경 실패: ' + e.message, 'error'); return; }
}
}
const requestBody = { const requestBody = {
final_description: combinedDescription, final_description: combinedDescription,
final_category: category, final_category: category,