#!/usr/bin/env python3 """ 누락된 테이블 생성 스크립트 - support_details - special_material_details - purchase_requests - purchase_request_items """ import sys import os import psycopg2 from psycopg2.extras import RealDictCursor import bcrypt # 프로젝트 루트를 Python path에 추가 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) def get_db_connection(): """데이터베이스 연결""" try: # Docker 환경에서는 서비스명으로 연결 conn = psycopg2.connect( host="tk-mp-postgres", port="5432", database="tk_mp_bom", user="tkmp_user", password="tkmp_password_2025" ) return conn except Exception as e: print(f"❌ DB 연결 실패: {e}") return None def create_admin_user(cursor): """기본 admin 계정 생성""" try: # admin 계정이 이미 있는지 확인 cursor.execute("SELECT COUNT(*) FROM users WHERE username = 'admin';") if cursor.fetchone()[0] > 0: print("✅ admin 계정이 이미 존재합니다.") return # bcrypt로 비밀번호 해시 생성 password = "admin123" hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') # admin 계정 생성 cursor.execute(""" INSERT INTO users ( username, password, name, email, role, access_level, is_active, status, department, position ) VALUES ( 'admin', %s, 'System Administrator', 'admin@example.com', 'admin', 'admin', true, 'active', 'IT', 'Administrator' ); """, (hashed_password,)) print("✅ admin 계정 생성 완료 (username: admin, password: admin123)") except Exception as e: print(f"⚠️ admin 계정 생성 실패: {e}") def add_missing_columns(cursor): """누락된 컬럼들 추가""" try: print("🔧 누락된 컬럼 확인 및 추가 중...") # users 테이블에 status 컬럼 확인 및 추가 cursor.execute(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'status'; """) if not cursor.fetchone(): print("➕ users 테이블에 status 컬럼 추가 중...") cursor.execute(""" ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active'; """) print("✅ users.status 컬럼 추가 완료") else: print("✅ users.status 컬럼이 이미 존재합니다") # files 테이블에 누락된 컬럼들 확인 및 추가 files_columns = { 'job_no': 'VARCHAR(50)', 'bom_name': 'VARCHAR(255)', 'description': 'TEXT', 'parsed_count': 'INTEGER DEFAULT 0', 'classification_completed': 'BOOLEAN DEFAULT FALSE' } for column_name, column_type in files_columns.items(): cursor.execute(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'files' AND column_name = %s; """, (column_name,)) if not cursor.fetchone(): print(f"➕ files 테이블에 {column_name} 컬럼 추가 중...") cursor.execute(f""" ALTER TABLE files ADD COLUMN {column_name} {column_type}; """) print(f"✅ files.{column_name} 컬럼 추가 완료") else: print(f"✅ files.{column_name} 컬럼이 이미 존재합니다") # materials 테이블에 누락된 컬럼들 확인 및 추가 materials_columns = { 'main_nom': 'VARCHAR(50)', 'red_nom': 'VARCHAR(50)', 'full_material_grade': 'TEXT', 'row_number': 'INTEGER', 'length': 'NUMERIC(10,3)', 'purchase_confirmed': 'BOOLEAN DEFAULT FALSE', 'confirmed_quantity': 'NUMERIC(10,3)', 'purchase_status': 'VARCHAR(20)', 'purchase_confirmed_by': 'VARCHAR(100)', 'purchase_confirmed_at': 'TIMESTAMP', 'revision_status': 'VARCHAR(20)', 'material_hash': 'VARCHAR(64)', 'normalized_description': 'TEXT', 'drawing_reference': 'VARCHAR(100)', 'notes': 'TEXT', 'created_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP', 'brand': 'VARCHAR(100)', 'user_requirement': 'TEXT', 'is_active': 'BOOLEAN DEFAULT TRUE', 'total_length': 'NUMERIC(10,3)' } for column_name, column_type in materials_columns.items(): cursor.execute(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'materials' AND column_name = %s; """, (column_name,)) if not cursor.fetchone(): print(f"➕ materials 테이블에 {column_name} 컬럼 추가 중...") cursor.execute(f""" ALTER TABLE materials ADD COLUMN {column_name} {column_type}; """) print(f"✅ materials.{column_name} 컬럼 추가 완료") else: print(f"✅ materials.{column_name} 컬럼이 이미 존재합니다") # purchase_requests 테이블에 누락된 컬럼들 확인 및 추가 purchase_requests_columns = { 'file_id': 'INTEGER REFERENCES files(id)' } for column_name, column_type in purchase_requests_columns.items(): cursor.execute(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'purchase_requests' AND column_name = %s; """, (column_name,)) if not cursor.fetchone(): print(f"➕ purchase_requests 테이블에 {column_name} 컬럼 추가 중...") cursor.execute(f""" ALTER TABLE purchase_requests ADD COLUMN {column_name} {column_type}; """) print(f"✅ purchase_requests.{column_name} 컬럼 추가 완료") else: print(f"✅ purchase_requests.{column_name} 컬럼이 이미 존재합니다") # material_purchase_tracking 테이블에 누락된 컬럼들 확인 및 추가 mpt_columns = { 'description': 'TEXT', 'purchase_status': 'VARCHAR(20) DEFAULT \'pending\'' } for column_name, column_type in mpt_columns.items(): cursor.execute(""" SELECT column_name FROM information_schema.columns WHERE table_name = 'material_purchase_tracking' AND column_name = %s; """, (column_name,)) if not cursor.fetchone(): print(f"➕ material_purchase_tracking 테이블에 {column_name} 컬럼 추가 중...") cursor.execute(f""" ALTER TABLE material_purchase_tracking ADD COLUMN {column_name} {column_type}; """) print(f"✅ material_purchase_tracking.{column_name} 컬럼 추가 완료") else: print(f"✅ material_purchase_tracking.{column_name} 컬럼이 이미 존재합니다") except Exception as e: print(f"⚠️ 컬럼 추가 실패: {e}") def create_missing_tables(): """누락된 테이블들 생성 (처음 설치 시에만)""" conn = get_db_connection() if not conn: return False try: cursor = conn.cursor() # 이미 설치되어 있는지 확인 (핵심 테이블들이 모두 존재하는지 체크) print("🔍 기존 설치 상태 확인 중...") cursor.execute(""" SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('support_details', 'special_material_details', 'purchase_requests', 'purchase_request_items'); """) existing_tables = cursor.fetchone()[0] if existing_tables == 4: print("✅ 모든 테이블이 이미 존재합니다.") # 컬럼 체크는 항상 수행 print("🔧 누락된 컬럼 확인 중...") add_missing_columns(cursor) # admin 계정 확인 cursor.execute("SELECT COUNT(*) FROM users WHERE username = 'admin';") admin_exists = cursor.fetchone()[0] if admin_exists == 0: print("👤 admin 계정 생성 중...") create_admin_user(cursor) conn.commit() print("✅ admin 계정 생성 완료") else: print("✅ admin 계정이 이미 존재합니다.") conn.commit() return True print(f"🔍 누락된 테이블 확인 및 생성 중... ({existing_tables}/4개 존재)") # 1. support_details 테이블 print("📋 1. support_details 테이블 확인...") cursor.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'support_details' ); """) if not cursor.fetchone()[0]: print("➕ support_details 테이블 생성 중...") cursor.execute(""" CREATE TABLE support_details ( id SERIAL PRIMARY KEY, material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE, file_id INTEGER REFERENCES files(id) ON DELETE CASCADE, support_type VARCHAR(50), support_subtype VARCHAR(100), load_rating VARCHAR(50), load_capacity VARCHAR(50), material_standard VARCHAR(100), material_grade VARCHAR(50), pipe_size VARCHAR(20), length_mm NUMERIC(10,2), width_mm NUMERIC(10,2), height_mm NUMERIC(10,2), classification_confidence NUMERIC(3,2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_support_details_material_id ON support_details(material_id); CREATE INDEX idx_support_details_file_id ON support_details(file_id); """) print("✅ support_details 테이블 생성 완료") else: print("✅ support_details 테이블 이미 존재") # 2. special_material_details 테이블 print("📋 2. special_material_details 테이블 확인...") cursor.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'special_material_details' ); """) if not cursor.fetchone()[0]: print("➕ special_material_details 테이블 생성 중...") cursor.execute(""" CREATE TABLE special_material_details ( id SERIAL PRIMARY KEY, material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE, file_id INTEGER REFERENCES files(id) ON DELETE CASCADE, special_type VARCHAR(50), special_subtype VARCHAR(100), material_standard VARCHAR(100), material_grade VARCHAR(50), specifications TEXT, dimensions VARCHAR(100), weight_kg NUMERIC(10,3), classification_confidence NUMERIC(3,2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_special_material_details_material_id ON special_material_details(material_id); CREATE INDEX idx_special_material_details_file_id ON special_material_details(file_id); """) print("✅ special_material_details 테이블 생성 완료") else: print("✅ special_material_details 테이블 이미 존재") # 3. purchase_requests 테이블 print("📋 3. purchase_requests 테이블 확인...") cursor.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'purchase_requests' ); """) if not cursor.fetchone()[0]: print("➕ purchase_requests 테이블 생성 중...") cursor.execute(""" CREATE TABLE purchase_requests ( request_id SERIAL PRIMARY KEY, request_no VARCHAR(50) UNIQUE NOT NULL, job_no VARCHAR(50) NOT NULL, project_name VARCHAR(200), requested_by INTEGER REFERENCES users(user_id), requested_by_username VARCHAR(100), request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) DEFAULT 'pending', total_items INTEGER DEFAULT 0, notes TEXT, approved_by INTEGER REFERENCES users(user_id), approved_by_username VARCHAR(100), approved_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_purchase_requests_job_no ON purchase_requests(job_no); CREATE INDEX idx_purchase_requests_status ON purchase_requests(status); CREATE INDEX idx_purchase_requests_requested_by ON purchase_requests(requested_by); """) print("✅ purchase_requests 테이블 생성 완료") else: print("✅ purchase_requests 테이블 이미 존재") # 4. purchase_request_items 테이블 print("📋 4. purchase_request_items 테이블 확인...") cursor.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'purchase_request_items' ); """) if not cursor.fetchone()[0]: print("➕ purchase_request_items 테이블 생성 중...") cursor.execute(""" CREATE TABLE purchase_request_items ( item_id SERIAL PRIMARY KEY, request_id INTEGER REFERENCES purchase_requests(request_id) ON DELETE CASCADE, material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE, description TEXT NOT NULL, category VARCHAR(50), subcategory VARCHAR(100), material_grade VARCHAR(50), size_spec VARCHAR(50), quantity NUMERIC(10,3) NOT NULL, unit VARCHAR(10) NOT NULL, drawing_name VARCHAR(100), notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_purchase_request_items_request_id ON purchase_request_items(request_id); CREATE INDEX idx_purchase_request_items_material_id ON purchase_request_items(material_id); CREATE INDEX idx_purchase_request_items_category ON purchase_request_items(category); """) print("✅ purchase_request_items 테이블 생성 완료") else: print("✅ purchase_request_items 테이블 이미 존재") # 변경사항 커밋 conn.commit() print("\n🎉 누락된 테이블 생성 완료!") # 최종 테이블 목록 확인 print("\n📋 현재 테이블 목록:") cursor.execute(""" SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name; """) tables = cursor.fetchall() for table in tables: print(f" - {table[0]}") print(f"\n총 {len(tables)}개 테이블 존재") # users 테이블에 status 컬럼 추가 (필요한 경우) add_missing_columns(cursor) # admin 계정 생성 create_admin_user(cursor) conn.commit() return True except Exception as e: print(f"❌ 테이블 생성 실패: {e}") conn.rollback() return False finally: if conn: conn.close() if __name__ == "__main__": print("🚀 누락된 테이블 생성 시작...") success = create_missing_tables() if success: print("✅ 모든 작업 완료!") sys.exit(0) else: print("❌ 작업 실패!") sys.exit(1)