Files
TK-BOM-Project/backend/scripts/create_missing_tables.py
Hyungi Ahn 17843e285f feat: 리비전 관리 시스템 및 구매확정 기능 구현
- 리비전 관리 라우터 및 서비스 추가 (revision_management.py, revision_comparison_service.py, revision_session_service.py)
- 구매확정 기능 구현: materials 테이블에 purchase_confirmed 필드 추가 및 업데이트 로직
- 리비전 비교 로직 구현: 구매확정된 자재 기반으로 신규/변경 자재 자동 분류
- 데이터베이스 스키마 확장: revision_sessions, revision_material_changes, inventory_transfers 테이블 추가
- 구매신청 생성 시 자재 상세 정보 저장 및 purchase_confirmed 자동 업데이트
- 프론트엔드: 리비전 관리 컴포넌트 및 hooks 추가
- 파일 목록 조회 API 추가 (/files/list)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 07:36:44 +09:00

655 lines
27 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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} 컬럼이 이미 존재합니다")
# purchase_requests 테이블에 누락된 컬럼들 확인 및 추가
purchase_requests_columns = {
'file_id': 'INTEGER REFERENCES files(id)',
'category': 'VARCHAR(50)',
'material_count': 'INTEGER DEFAULT 0',
'excel_file_path': 'VARCHAR(500)'
}
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} 컬럼이 이미 존재합니다")
# purchase_request_items 테이블에 누락된 컬럼들 확인 및 추가
purchase_request_items_columns = {
'user_requirement': 'TEXT',
'description': 'TEXT',
'category': 'VARCHAR(50)',
'subcategory': 'VARCHAR(100)',
'material_grade': 'VARCHAR(50)',
'size_spec': 'VARCHAR(50)',
'drawing_name': 'VARCHAR(100)',
'notes': 'TEXT',
'is_ordered': 'BOOLEAN DEFAULT FALSE',
'is_received': 'BOOLEAN DEFAULT FALSE'
}
for column_name, column_type in purchase_request_items_columns.items():
cursor.execute("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'purchase_request_items' AND column_name = %s;
""", (column_name,))
if not cursor.fetchone():
print(f" purchase_request_items 테이블에 {column_name} 컬럼 추가 중...")
cursor.execute(f"""
ALTER TABLE purchase_request_items ADD COLUMN {column_name} {column_type};
""")
print(f"✅ purchase_request_items.{column_name} 컬럼 추가 완료")
else:
print(f"✅ purchase_request_items.{column_name} 컬럼이 이미 존재합니다")
print("✅ 모든 누락된 컬럼 추가 완료!")
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,
file_id INTEGER REFERENCES files(id),
job_no VARCHAR(50) NOT NULL,
category VARCHAR(50),
material_count INTEGER DEFAULT 0,
excel_file_path VARCHAR(500),
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 테이블 이미 존재")
# 5. revision_sessions 테이블
print("📋 5. revision_sessions 테이블 확인...")
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'revision_sessions'
);
""")
if not cursor.fetchone()[0]:
print(" revision_sessions 테이블 생성 중...")
cursor.execute("""
CREATE TABLE revision_sessions (
id SERIAL PRIMARY KEY,
job_no VARCHAR(50) NOT NULL,
current_file_id INTEGER REFERENCES files(id),
previous_file_id INTEGER REFERENCES files(id),
current_revision VARCHAR(20) NOT NULL,
previous_revision VARCHAR(20) NOT NULL,
status VARCHAR(20) DEFAULT 'processing',
total_materials INTEGER DEFAULT 0,
processed_materials INTEGER DEFAULT 0,
added_count INTEGER DEFAULT 0,
removed_count INTEGER DEFAULT 0,
changed_count INTEGER DEFAULT 0,
unchanged_count INTEGER DEFAULT 0,
purchase_cancel_count INTEGER DEFAULT 0,
inventory_transfer_count INTEGER DEFAULT 0,
additional_purchase_count INTEGER DEFAULT 0,
created_by VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
);
CREATE INDEX idx_revision_sessions_job_no ON revision_sessions(job_no);
CREATE INDEX idx_revision_sessions_status ON revision_sessions(status);
""")
print("✅ revision_sessions 테이블 생성 완료")
else:
print("✅ revision_sessions 테이블 이미 존재")
# 6. revision_material_changes 테이블
print("📋 6. revision_material_changes 테이블 확인...")
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'revision_material_changes'
);
""")
if not cursor.fetchone()[0]:
print(" revision_material_changes 테이블 생성 중...")
cursor.execute("""
CREATE TABLE revision_material_changes (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES revision_sessions(id) ON DELETE CASCADE,
material_id INTEGER REFERENCES materials(id),
previous_material_id INTEGER,
material_description TEXT NOT NULL,
category VARCHAR(50) NOT NULL,
change_type VARCHAR(20) NOT NULL,
previous_quantity NUMERIC(10,3),
current_quantity NUMERIC(10,3),
quantity_difference NUMERIC(10,3),
purchase_status VARCHAR(20) NOT NULL,
purchase_confirmed_at TIMESTAMP,
revision_action VARCHAR(30),
action_status VARCHAR(20) DEFAULT 'pending',
processed_by VARCHAR(100),
processed_at TIMESTAMP,
processing_notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_revision_changes_session ON revision_material_changes(session_id);
CREATE INDEX idx_revision_changes_action ON revision_material_changes(revision_action);
CREATE INDEX idx_revision_changes_status ON revision_material_changes(action_status);
""")
print("✅ revision_material_changes 테이블 생성 완료")
else:
print("✅ revision_material_changes 테이블 이미 존재")
# 7. inventory_transfers 테이블
print("📋 7. inventory_transfers 테이블 확인...")
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'inventory_transfers'
);
""")
if not cursor.fetchone()[0]:
print(" inventory_transfers 테이블 생성 중...")
cursor.execute("""
CREATE TABLE inventory_transfers (
id SERIAL PRIMARY KEY,
revision_change_id INTEGER REFERENCES revision_material_changes(id),
material_description TEXT NOT NULL,
category VARCHAR(50) NOT NULL,
quantity NUMERIC(10,3) NOT NULL,
unit VARCHAR(10) NOT NULL,
inventory_location VARCHAR(100),
storage_notes TEXT,
transferred_by VARCHAR(100) NOT NULL,
transferred_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'transferred'
);
CREATE INDEX idx_inventory_transfers_material ON inventory_transfers(material_description);
CREATE INDEX idx_inventory_transfers_date ON inventory_transfers(transferred_at);
""")
print("✅ inventory_transfers 테이블 생성 완료")
else:
print("✅ inventory_transfers 테이블 이미 존재")
# 8. revision_action_logs 테이블
print("📋 8. revision_action_logs 테이블 확인...")
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'revision_action_logs'
);
""")
if not cursor.fetchone()[0]:
print(" revision_action_logs 테이블 생성 중...")
cursor.execute("""
CREATE TABLE revision_action_logs (
id SERIAL PRIMARY KEY,
session_id INTEGER REFERENCES revision_sessions(id),
revision_change_id INTEGER REFERENCES revision_material_changes(id),
action_type VARCHAR(30) NOT NULL,
action_description TEXT,
executed_by VARCHAR(100) NOT NULL,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
result VARCHAR(20) NOT NULL,
result_message TEXT,
result_data JSONB
);
CREATE INDEX idx_revision_logs_session ON revision_action_logs(session_id);
CREATE INDEX idx_revision_logs_type ON revision_action_logs(action_type);
CREATE INDEX idx_revision_logs_date ON revision_action_logs(executed_at);
""")
print("✅ revision_action_logs 테이블 생성 완료")
else:
print("✅ revision_action_logs 테이블 이미 존재")
# 변경사항 커밋
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)