diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 8c6c584..ba44a63 100644 Binary files a/backend/__pycache__/main.cpython-311.pyc and b/backend/__pycache__/main.cpython-311.pyc differ diff --git a/backend/database/__pycache__/models.cpython-311.pyc b/backend/database/__pycache__/models.cpython-311.pyc index e71ab5f..2a4b533 100644 Binary files a/backend/database/__pycache__/models.cpython-311.pyc and b/backend/database/__pycache__/models.cpython-311.pyc differ diff --git a/backend/database/__pycache__/schemas.cpython-311.pyc b/backend/database/__pycache__/schemas.cpython-311.pyc index c397091..7010336 100644 Binary files a/backend/database/__pycache__/schemas.cpython-311.pyc and b/backend/database/__pycache__/schemas.cpython-311.pyc differ diff --git a/backend/database/models.py b/backend/database/models.py index 34d277d..5bdcbe4 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -132,6 +132,12 @@ class Issue(Base): final_description = Column(Text) # 최종 내용 (수정본 또는 원본) final_category = Column(Enum(IssueCategory)) # 최종 카테고리 (수정본 또는 원본) + # 추가 정보 필드들 (관리함에서 기록용) + responsible_person_detail = Column(String(200)) # 해당자 상세 정보 + cause_detail = Column(Text) # 원인 상세 정보 + additional_info_updated_at = Column(DateTime) # 추가 정보 입력 시간 + additional_info_updated_by_id = Column(Integer, ForeignKey("users.id")) # 추가 정보 입력자 + # Relationships reporter = relationship("User", back_populates="issues", foreign_keys=[reporter_id]) reviewer = relationship("User", foreign_keys=[reviewed_by_id], overlaps="reviewed_issues") diff --git a/backend/database/schemas.py b/backend/database/schemas.py index 1fb6442..5ed5f6d 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -141,6 +141,12 @@ class Issue(IssueBase): final_description: Optional[str] = None # 최종 내용 final_category: Optional[IssueCategory] = None # 최종 카테고리 + # 추가 정보 필드들 (관리함에서 기록용) + responsible_person_detail: Optional[str] = None # 해당자 상세 정보 + cause_detail: Optional[str] = None # 원인 상세 정보 + additional_info_updated_at: Optional[datetime] = None # 추가 정보 입력 시간 + additional_info_updated_by_id: Optional[int] = None # 추가 정보 입력자 + class Config: from_attributes = True @@ -176,6 +182,12 @@ class ManagementUpdateRequest(BaseModel): management_comment: Optional[str] = None # ISSUE에 대한 의견 completion_photo: Optional[str] = None # 완료 사진 (Base64) +class AdditionalInfoUpdateRequest(BaseModel): + """추가 정보 업데이트 요청 (관리함 진행중에서 사용)""" + cause_department: Optional[DepartmentType] = None # 원인부서 + responsible_person_detail: Optional[str] = None # 해당자 상세 정보 + cause_detail: Optional[str] = None # 원인 상세 정보 + class InboxIssue(BaseModel): """수신함용 부적합 정보 (간소화된 버전)""" id: int diff --git a/backend/main.py b/backend/main.py index ea89be8..2aea158 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,7 +5,7 @@ import uvicorn from database.database import engine, get_db from database.models import Base -from routers import auth, issues, daily_work, reports, projects, page_permissions, inbox +from routers import auth, issues, daily_work, reports, projects, page_permissions, inbox, management from services.auth_service import create_admin_user # 데이터베이스 테이블 생성 @@ -38,6 +38,7 @@ app.include_router(daily_work.router) app.include_router(reports.router) app.include_router(projects.router) app.include_router(page_permissions.router) +app.include_router(management.router) # 관리함 라우터 추가 # 시작 시 관리자 계정 생성 @app.on_event("startup") diff --git a/backend/migrations/018_add_additional_info_fields.sql b/backend/migrations/018_add_additional_info_fields.sql new file mode 100644 index 0000000..445d020 --- /dev/null +++ b/backend/migrations/018_add_additional_info_fields.sql @@ -0,0 +1,85 @@ +-- 수신함 추가 정보 필드 추가 +-- 원인부서, 해당자, 원인 상세 정보를 기록하기 위한 필드들 + +DO $migration$ +DECLARE + col_count INTEGER := 0; + idx_count INTEGER := 0; +BEGIN + RAISE NOTICE '=== 수신함 추가 정보 필드 추가 마이그레이션 시작 ==='; + + -- 1. 해당자 상세 정보 (responsible_person과 별도) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'responsible_person_detail') THEN + ALTER TABLE issues ADD COLUMN responsible_person_detail VARCHAR(200); + RAISE NOTICE '✅ issues.responsible_person_detail 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.responsible_person_detail 컬럼이 이미 존재합니다.'; + END IF; + + -- 2. 원인 상세 정보 (기록용) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'cause_detail') THEN + ALTER TABLE issues ADD COLUMN cause_detail TEXT; + RAISE NOTICE '✅ issues.cause_detail 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.cause_detail 컬럼이 이미 존재합니다.'; + END IF; + + -- 3. 추가 정보 입력 시간 (언제 입력되었는지 기록) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'additional_info_updated_at') THEN + ALTER TABLE issues ADD COLUMN additional_info_updated_at TIMESTAMP WITH TIME ZONE; + RAISE NOTICE '✅ issues.additional_info_updated_at 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.additional_info_updated_at 컬럼이 이미 존재합니다.'; + END IF; + + -- 4. 추가 정보 입력자 ID (누가 입력했는지 기록) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'additional_info_updated_by_id') THEN + ALTER TABLE issues ADD COLUMN additional_info_updated_by_id INTEGER REFERENCES users(id); + RAISE NOTICE '✅ issues.additional_info_updated_by_id 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.additional_info_updated_by_id 컬럼이 이미 존재합니다.'; + END IF; + + -- 인덱스 추가 (검색 성능 향상) + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_additional_info_updated_at') THEN + CREATE INDEX idx_issues_additional_info_updated_at ON issues (additional_info_updated_at); + RAISE NOTICE '✅ idx_issues_additional_info_updated_at 인덱스가 생성되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ idx_issues_additional_info_updated_at 인덱스가 이미 존재합니다.'; + END IF; + + -- 검증 + SELECT COUNT(*) INTO col_count FROM information_schema.columns + WHERE table_name = 'issues' AND column_name IN ( + 'responsible_person_detail', 'cause_detail', 'additional_info_updated_at', 'additional_info_updated_by_id' + ); + + SELECT COUNT(*) INTO idx_count FROM pg_indexes + WHERE tablename = 'issues' AND indexname = 'idx_issues_additional_info_updated_at'; + + RAISE NOTICE '=== 마이그레이션 검증 결과 ==='; + RAISE NOTICE '추가된 컬럼: %/4개', col_count; + RAISE NOTICE '생성된 인덱스: %/1개', idx_count; + + IF col_count = 4 AND idx_count = 1 THEN + RAISE NOTICE '✅ 모든 추가 정보 필드가 성공적으로 추가되었습니다.'; + ELSE + RAISE WARNING '⚠️ 일부 필드나 인덱스가 누락되었습니다.'; + END IF; + +EXCEPTION + WHEN OTHERS THEN + RAISE EXCEPTION '❌ 마이그레이션 실행 중 오류 발생: %', SQLERRM; +END $migration$; + +-- 마이그레이션 로그 기록 +INSERT INTO migration_log (migration_file, executed_at, status, notes) +VALUES ( + '018_add_additional_info_fields.sql', + NOW(), + 'SUCCESS', + '수신함 추가 정보 필드 추가: 해당자 상세(responsible_person_detail), 원인 상세(cause_detail), 입력 시간/입력자 추적 필드' +) ON CONFLICT (migration_file) DO UPDATE SET + executed_at = NOW(), + status = EXCLUDED.status, + notes = EXCLUDED.notes; diff --git a/backend/routers/__pycache__/management.cpython-311.pyc b/backend/routers/__pycache__/management.cpython-311.pyc new file mode 100644 index 0000000..18b93b2 Binary files /dev/null and b/backend/routers/__pycache__/management.cpython-311.pyc differ diff --git a/backend/routers/management.py b/backend/routers/management.py new file mode 100644 index 0000000..4ed9f44 --- /dev/null +++ b/backend/routers/management.py @@ -0,0 +1,103 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from datetime import datetime +from typing import List + +from database.database import get_db +from database.models import Issue, User, ReviewStatus +from database.schemas import ( + ManagementUpdateRequest, AdditionalInfoUpdateRequest, Issue as IssueSchema +) +from routers.auth import get_current_user +from routers.page_permissions import check_page_access + +router = APIRouter(prefix="/api/management", tags=["management"]) + +@router.get("/", response_model=List[IssueSchema]) +async def get_management_issues( + 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="관리함 접근 권한이 없습니다.") + + # 진행 중 또는 완료된 이슈들 조회 + issues = db.query(Issue).filter( + Issue.review_status.in_([ReviewStatus.in_progress, ReviewStatus.completed]) + ).order_by(Issue.reviewed_at.desc()).all() + + return issues + +@router.put("/{issue_id}/additional-info") +async def update_additional_info( + issue_id: int, + additional_info: AdditionalInfoUpdateRequest, + 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 = additional_info.dict(exclude_unset=True) + + for field, value in update_data.items(): + setattr(issue, field, value) + + # 추가 정보 입력 시간 및 입력자 기록 + issue.additional_info_updated_at = datetime.now() + issue.additional_info_updated_by_id = current_user.id + + db.commit() + db.refresh(issue) + + return { + "message": "추가 정보가 성공적으로 업데이트되었습니다.", + "issue_id": issue.id, + "updated_at": issue.additional_info_updated_at, + "updated_by": current_user.username + } + +@router.get("/{issue_id}/additional-info") +async def get_additional_info( + issue_id: int, + 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="부적합을 찾을 수 없습니다.") + + return { + "issue_id": issue.id, + "cause_department": issue.cause_department.value if issue.cause_department else None, + "responsible_person_detail": issue.responsible_person_detail, + "cause_detail": issue.cause_detail, + "additional_info_updated_at": issue.additional_info_updated_at, + "additional_info_updated_by_id": issue.additional_info_updated_by_id + } diff --git a/frontend/issues-management.html b/frontend/issues-management.html index aa22894..af8ae33 100644 --- a/frontend/issues-management.html +++ b/frontend/issues-management.html @@ -247,17 +247,27 @@ - -
- - + +
+ + + @@ -518,13 +528,18 @@ // 탭 버튼 스타일 업데이트 const inProgressTab = document.getElementById('inProgressTab'); const completedTab = document.getElementById('completedTab'); + const additionalInfoBtn = document.getElementById('additionalInfoBtn'); if (tab === 'in_progress') { inProgressTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-blue-500 text-white'; completedTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900'; + // 진행 중 탭에서만 추가 정보 버튼 표시 + additionalInfoBtn.style.display = 'block'; } else { inProgressTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900'; completedTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-green-500 text-white'; + // 완료됨 탭에서는 추가 정보 버튼 숨김 + additionalInfoBtn.style.display = 'none'; } filterIssues(); // 이미 updateStatistics()가 포함됨 @@ -1392,6 +1407,181 @@ console.error('❌ API 스크립트 로드 실패'); }; document.head.appendChild(script); + + // 추가 정보 모달 관련 함수들 + let selectedIssueId = null; + + function openAdditionalInfoModal() { + // 진행 중 탭에서 선택된 이슈가 있는지 확인 + const inProgressIssues = allIssues.filter(issue => issue.review_status === 'in_progress'); + + if (inProgressIssues.length === 0) { + alert('진행 중인 부적합이 없습니다.'); + return; + } + + // 첫 번째 진행 중 이슈를 기본 선택 (추후 개선 가능) + selectedIssueId = inProgressIssues[0].id; + + // 기존 데이터 로드 + loadAdditionalInfo(selectedIssueId); + + document.getElementById('additionalInfoModal').classList.remove('hidden'); + } + + function closeAdditionalInfoModal() { + document.getElementById('additionalInfoModal').classList.add('hidden'); + selectedIssueId = null; + + // 폼 초기화 + document.getElementById('additionalInfoForm').reset(); + } + + async function loadAdditionalInfo(issueId) { + try { + const response = await fetch(`/api/management/${issueId}/additional-info`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + + // 폼에 기존 데이터 채우기 + document.getElementById('causeDepartment').value = data.cause_department || ''; + document.getElementById('responsiblePersonDetail').value = data.responsible_person_detail || ''; + document.getElementById('causeDetail').value = data.cause_detail || ''; + } + } catch (error) { + console.error('추가 정보 로드 실패:', error); + } + } + + // 추가 정보 폼 제출 처리 + document.getElementById('additionalInfoForm').addEventListener('submit', async function(e) { + e.preventDefault(); + + if (!selectedIssueId) { + alert('선택된 부적합이 없습니다.'); + return; + } + + const formData = { + cause_department: document.getElementById('causeDepartment').value || null, + responsible_person_detail: document.getElementById('responsiblePersonDetail').value || null, + cause_detail: document.getElementById('causeDetail').value || null + }; + + try { + const response = await fetch(`/api/management/${selectedIssueId}/additional-info`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('access_token')}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + const result = await response.json(); + alert('추가 정보가 성공적으로 저장되었습니다.'); + closeAdditionalInfoModal(); + + // 목록 새로고침 + loadIssues(); + } else { + const error = await response.json(); + alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`); + } + } catch (error) { + console.error('추가 정보 저장 실패:', error); + alert('저장 중 오류가 발생했습니다.'); + } + }); + + +