diff --git a/backend/database/__pycache__/models.cpython-311.pyc b/backend/database/__pycache__/models.cpython-311.pyc index 72796a9..e71ab5f 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 745a75c..459f413 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 6760c4a..34d277d 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -119,6 +119,19 @@ class Issue(Base): duplicate_of_issue_id = Column(Integer, ForeignKey("issues.id")) # 중복 대상 이슈 ID duplicate_reporters = Column(JSONB, default=lambda: []) # 중복 신고자 목록 + # 관리함에서 사용할 추가 필드들 + completion_photo_path = Column(String) # 완료 사진 경로 + solution = Column(Text) # 해결방안 (관리함에서 입력) + responsible_department = Column(Enum(DepartmentType)) # 담당부서 + responsible_person = Column(String(100)) # 담당자 + expected_completion_date = Column(DateTime) # 조치 예상일 + actual_completion_date = Column(DateTime) # 완료 확인일 + cause_department = Column(Enum(DepartmentType)) # 원인부서 + management_comment = Column(Text) # ISSUE에 대한 의견 + project_sequence_no = Column(Integer) # 프로젝트별 순번 (No) + final_description = Column(Text) # 최종 내용 (수정본 또는 원본) + final_category = Column(Enum(IssueCategory)) # 최종 카테고리 (수정본 또는 원본) + # 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 d443933..aa6806f 100644 --- a/backend/database/schemas.py +++ b/backend/database/schemas.py @@ -128,6 +128,19 @@ class Issue(IssueBase): duplicate_of_issue_id: Optional[int] = None duplicate_reporters: Optional[List[Dict[str, Any]]] = None + # 관리함에서 사용할 추가 필드들 + completion_photo_path: Optional[str] = None # 완료 사진 경로 + solution: Optional[str] = None # 해결방안 + responsible_department: Optional[DepartmentType] = None # 담당부서 + responsible_person: Optional[str] = None # 담당자 + expected_completion_date: Optional[datetime] = None # 조치 예상일 + actual_completion_date: Optional[datetime] = None # 완료 확인일 + cause_department: Optional[DepartmentType] = None # 원인부서 + management_comment: Optional[str] = None # ISSUE에 대한 의견 + project_sequence_no: Optional[int] = None # 프로젝트별 순번 + final_description: Optional[str] = None # 최종 내용 + final_category: Optional[IssueCategory] = None # 최종 카테고리 + class Config: from_attributes = True @@ -149,6 +162,17 @@ class IssueStatusUpdateRequest(BaseModel): """부적합 상태 변경 요청""" review_status: ReviewStatus notes: Optional[str] = None + completion_photo: Optional[str] = None # 완료 사진 (Base64) + +class ManagementUpdateRequest(BaseModel): + """관리함에서 사용할 필드 업데이트 요청""" + solution: Optional[str] = None # 해결방안 + responsible_department: Optional[DepartmentType] = None # 담당부서 + responsible_person: Optional[str] = None # 담당자 + expected_completion_date: Optional[datetime] = None # 조치 예상일 + cause_department: Optional[DepartmentType] = None # 원인부서 + management_comment: Optional[str] = None # ISSUE에 대한 의견 + completion_photo: Optional[str] = None # 완료 사진 (Base64) class InboxIssue(BaseModel): """수신함용 부적합 정보 (간소화된 버전)""" diff --git a/backend/migrations/016_add_management_fields.sql b/backend/migrations/016_add_management_fields.sql new file mode 100644 index 0000000..5c37418 --- /dev/null +++ b/backend/migrations/016_add_management_fields.sql @@ -0,0 +1,223 @@ +-- 016_add_management_fields.sql +-- 관리함에서 사용할 추가 필드들과 완료 사진 업로드 기능 추가 + +BEGIN; + +DO $$ +DECLARE + migration_name VARCHAR(255) := '016_add_management_fields.sql'; + migration_notes TEXT := '관리함 필드 추가: 원인/해결방안, 담당부서/담당자, 조치예상일, 완료확인일, 원인부서, 의견, 완료사진, 프로젝트별 No 등'; + current_status VARCHAR(50); +BEGIN + -- migration_log 테이블이 없으면 생성 (멱등성) + CREATE TABLE IF NOT EXISTS migration_log ( + id SERIAL PRIMARY KEY, + migration_file VARCHAR(255) NOT NULL UNIQUE, + executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + status VARCHAR(50), + notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + + SELECT status INTO current_status FROM migration_log WHERE migration_file = migration_name; + + IF current_status IS NULL THEN + RAISE NOTICE '--- 마이그레이션 % 시작 ---', migration_name; + + -- issues 테이블에 관리함 관련 컬럼들 추가 + + -- 완료 사진 경로 + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'completion_photo_path') THEN + ALTER TABLE issues ADD COLUMN completion_photo_path VARCHAR(255); + RAISE NOTICE '✅ issues.completion_photo_path 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.completion_photo_path 컬럼이 이미 존재합니다.'; + END IF; + + -- 해결방안 (관리함에서 입력) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'solution') THEN + ALTER TABLE issues ADD COLUMN solution TEXT; + RAISE NOTICE '✅ issues.solution 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.solution 컬럼이 이미 존재합니다.'; + END IF; + + -- 담당부서 (관리함에서 선택) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'responsible_department') THEN + ALTER TABLE issues ADD COLUMN responsible_department department_type; + RAISE NOTICE '✅ issues.responsible_department 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.responsible_department 컬럼이 이미 존재합니다.'; + END IF; + + -- 담당자 (관리함에서 입력) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'responsible_person') THEN + ALTER TABLE issues ADD COLUMN responsible_person VARCHAR(100); + RAISE NOTICE '✅ issues.responsible_person 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.responsible_person 컬럼이 이미 존재합니다.'; + END IF; + + -- 조치 예상일 (관리함에서 입력) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'expected_completion_date') THEN + ALTER TABLE issues ADD COLUMN expected_completion_date DATE; + RAISE NOTICE '✅ issues.expected_completion_date 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.expected_completion_date 컬럼이 이미 존재합니다.'; + END IF; + + -- 완료 확인일 (완료 상태로 변경 시 자동 입력) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'actual_completion_date') THEN + ALTER TABLE issues ADD COLUMN actual_completion_date DATE; + RAISE NOTICE '✅ issues.actual_completion_date 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.actual_completion_date 컬럼이 이미 존재합니다.'; + END IF; + + -- 원인부서 (관리함에서 입력) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'cause_department') THEN + ALTER TABLE issues ADD COLUMN cause_department department_type; + RAISE NOTICE '✅ issues.cause_department 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.cause_department 컬럼이 이미 존재합니다.'; + END IF; + + -- ISSUE에 대한 의견 (관리함에서 입력) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'management_comment') THEN + ALTER TABLE issues ADD COLUMN management_comment TEXT; + RAISE NOTICE '✅ issues.management_comment 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.management_comment 컬럼이 이미 존재합니다.'; + END IF; + + -- 프로젝트별 순번 (No) - 프로젝트 내에서 1, 2, 3... 순으로 증가 + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'project_sequence_no') THEN + ALTER TABLE issues ADD COLUMN project_sequence_no INTEGER; + RAISE NOTICE '✅ issues.project_sequence_no 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.project_sequence_no 컬럼이 이미 존재합니다.'; + END IF; + + -- 최종 내용 (수정된 내용이 있으면 수정본, 없으면 원본) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'final_description') THEN + ALTER TABLE issues ADD COLUMN final_description TEXT; + RAISE NOTICE '✅ issues.final_description 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.final_description 컬럼이 이미 존재합니다.'; + END IF; + + -- 최종 카테고리 (수정된 카테고리가 있으면 수정본, 없으면 원본) + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'issues' AND column_name = 'final_category') THEN + ALTER TABLE issues ADD COLUMN final_category issuecategory; + RAISE NOTICE '✅ issues.final_category 컬럼이 추가되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ issues.final_category 컬럼이 이미 존재합니다.'; + END IF; + + -- 인덱스 추가 + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_project_sequence') THEN + CREATE INDEX idx_issues_project_sequence ON issues (project_id, project_sequence_no); + RAISE NOTICE '✅ idx_issues_project_sequence 인덱스가 생성되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ idx_issues_project_sequence 인덱스가 이미 존재합니다.'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_responsible_department') THEN + CREATE INDEX idx_issues_responsible_department ON issues (responsible_department) WHERE responsible_department IS NOT NULL; + RAISE NOTICE '✅ idx_issues_responsible_department 인덱스가 생성되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ idx_issues_responsible_department 인덱스가 이미 존재합니다.'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE tablename = 'issues' AND indexname = 'idx_issues_expected_completion') THEN + CREATE INDEX idx_issues_expected_completion ON issues (expected_completion_date) WHERE expected_completion_date IS NOT NULL; + RAISE NOTICE '✅ idx_issues_expected_completion 인덱스가 생성되었습니다.'; + ELSE + RAISE NOTICE 'ℹ️ idx_issues_expected_completion 인덱스가 이미 존재합니다.'; + END IF; + + -- 프로젝트별 순번 자동 생성 함수 + CREATE OR REPLACE FUNCTION generate_project_sequence_no(p_project_id BIGINT) RETURNS INTEGER AS $func$ + DECLARE + next_no INTEGER; + BEGIN + -- 해당 프로젝트의 최대 순번 + 1 + SELECT COALESCE(MAX(project_sequence_no), 0) + 1 + INTO next_no + FROM issues + WHERE project_id = p_project_id; + + RETURN next_no; + END; + $func$ LANGUAGE plpgsql; + RAISE NOTICE '✅ generate_project_sequence_no 함수가 생성되었습니다.'; + + -- 기존 이슈들에 대해 프로젝트별 순번 설정 + DO $update_sequence$ + DECLARE + issue_record RECORD; + seq_no INTEGER; + BEGIN + FOR issue_record IN + SELECT id, project_id + FROM issues + WHERE project_sequence_no IS NULL + ORDER BY project_id, report_date + LOOP + SELECT generate_project_sequence_no(issue_record.project_id) INTO seq_no; + UPDATE issues + SET project_sequence_no = seq_no + WHERE id = issue_record.id; + END LOOP; + END $update_sequence$; + RAISE NOTICE '✅ 기존 이슈들의 프로젝트별 순번이 설정되었습니다.'; + + -- 기존 이슈들의 final_description과 final_category 초기화 + UPDATE issues + SET + final_description = description, + final_category = category + WHERE final_description IS NULL OR final_category IS NULL; + RAISE NOTICE '✅ 기존 이슈들의 final_description과 final_category가 초기화되었습니다.'; + + -- 마이그레이션 검증 + DECLARE + col_count INTEGER; + idx_count INTEGER; + func_count INTEGER; + BEGIN + SELECT COUNT(*) INTO col_count FROM information_schema.columns + WHERE table_name = 'issues' AND column_name IN ( + 'completion_photo_path', 'solution', 'responsible_department', 'responsible_person', + 'expected_completion_date', 'actual_completion_date', 'cause_department', + 'management_comment', 'project_sequence_no', 'final_description', 'final_category' + ); + + SELECT COUNT(*) INTO idx_count FROM pg_indexes + WHERE tablename = 'issues' AND indexname IN ( + 'idx_issues_project_sequence', 'idx_issues_responsible_department', 'idx_issues_expected_completion' + ); + + SELECT COUNT(*) INTO func_count FROM pg_proc WHERE proname = 'generate_project_sequence_no'; + + RAISE NOTICE '=== 마이그레이션 검증 결과 ==='; + RAISE NOTICE '추가된 컬럼: %/11개', col_count; + RAISE NOTICE '생성된 인덱스: %/3개', idx_count; + RAISE NOTICE '생성된 함수: %/1개', func_count; + + IF col_count = 11 AND idx_count = 3 AND func_count = 1 THEN + RAISE NOTICE '✅ 마이그레이션이 성공적으로 완료되었습니다!'; + INSERT INTO migration_log (migration_file, status, notes) VALUES (migration_name, 'SUCCESS', migration_notes); + ELSE + RAISE EXCEPTION '❌ 마이그레이션 검증 실패!'; + END IF; + END; + + ELSIF current_status = 'SUCCESS' THEN + RAISE NOTICE 'ℹ️ 마이그레이션 %는 이미 성공적으로 실행되었습니다. 스킵합니다.', migration_name; + ELSE + RAISE NOTICE '⚠️ 마이그레이션 %는 이전에 실패했습니다. 수동 확인이 필요합니다.', migration_name; + END IF; +END $$; + +COMMIT; diff --git a/backend/routers/__pycache__/inbox.cpython-311.pyc b/backend/routers/__pycache__/inbox.cpython-311.pyc index 2d39406..d1dd7b5 100644 Binary files a/backend/routers/__pycache__/inbox.cpython-311.pyc and b/backend/routers/__pycache__/inbox.cpython-311.pyc differ diff --git a/backend/routers/inbox.py b/backend/routers/inbox.py index b693ca1..eade902 100644 --- a/backend/routers/inbox.py +++ b/backend/routers/inbox.py @@ -7,10 +7,11 @@ from database.database import get_db from database.models import Issue, User, Project, ReviewStatus, DisposalReasonType from database.schemas import ( InboxIssue, IssueDisposalRequest, IssueReviewRequest, - IssueStatusUpdateRequest, ModificationLogEntry + IssueStatusUpdateRequest, ModificationLogEntry, ManagementUpdateRequest ) from routers.auth import get_current_user, get_current_admin from routers.page_permissions import check_page_access +from services.file_service import save_base64_image router = APIRouter(prefix="/api/inbox", tags=["inbox"]) @@ -260,6 +261,18 @@ async def update_issue_status( issue.reviewed_by_id = current_user.id issue.reviewed_at = datetime.now() + # 완료 사진 업로드 처리 + if status_request.completion_photo and status_request.review_status == ReviewStatus.completed: + try: + completion_photo_path = save_base64_image(status_request.completion_photo, "completion") + issue.completion_photo_path = completion_photo_path + except Exception as e: + raise HTTPException(status_code=400, detail=f"완료 사진 저장 실패: {str(e)}") + + # 완료 상태로 변경 시 완료 확인일 설정 + if status_request.review_status == ReviewStatus.completed: + issue.actual_completion_date = datetime.now().date() + # 노트가 있으면 detail_notes에 추가 if status_request.notes: current_notes = issue.detail_notes or "" diff --git a/frontend/issues-inbox.html b/frontend/issues-inbox.html index 18f2e09..eaa3aa6 100644 --- a/frontend/issues-inbox.html +++ b/frontend/issues-inbox.html @@ -411,11 +411,11 @@
@@ -427,6 +427,21 @@ placeholder="처리 내용이나 특이사항을 입력하세요..."> + + +