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 sqlalchemy.orm import Session
from sqlalchemy import text, func
from typing import List, Optional
from datetime import datetime
@@ -140,6 +141,20 @@ async def update_issue(
# 업데이트
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장) - 새 사진 저장 후 기존 사진 삭제 (안전)
old_photos_to_delete = []
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>
</div>
<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">
<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>

View File

@@ -253,6 +253,14 @@ function openEditMgmtSheet(issueId) {
var issue = issues.find(function (i) { return i.id === issueId; });
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('editResponsibleDept').value = issue.responsible_department || '';
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
};
// 프로젝트 변경 확인
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', {
method: 'PUT',
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; });
if (!issue) return;
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 || '';
}
@@ -372,7 +396,7 @@ async function saveAdditionalInfo() {
headers: { 'Authorization': 'Bearer ' + TokenManager.getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({
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
})
});

View File

@@ -66,6 +66,7 @@ async function loadProjects() {
if (response.ok) {
projects = await response.json();
window._cachedProjects = projects;
updateProjectFilter();
}
} catch (error) {
@@ -1573,7 +1574,10 @@ function openIssueEditModal(issueId, isCompletionMode = false) {
<div class="space-y-3">
<div>
<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>
<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 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 = {
final_description: combinedDescription,
final_category: category,