🔧 볼트 재질 정보 개선 및 A320/A194M 패턴 지원
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- bolt_classifier.py: A320/A194M 조합 패턴 처리 로직 추가
- material_grade_extractor.py: A320/A194M 패턴 추출 개선
- integrated_classifier.py: SPECIAL, U_BOLT 카테고리 우선 분류
- 데이터베이스: 492개 볼트의 material_grade를 완전한 형태로 업데이트
  - A320/A194M GR B8/8: 78개
  - A193/A194 GR B7/2H: 414개
- 프론트엔드: BOLT 카테고리 전용 UI (길이 표시)
- Excel 내보내기: BOLT용 컬럼 순서 및 재질 정보 개선
- SPECIAL, U_BOLT 카테고리 지원 추가
This commit is contained in:
Hyungi Ahn
2025-10-01 08:18:25 +09:00
parent 50570e4624
commit 2e0d91cf59
12 changed files with 2370 additions and 256 deletions

View File

@@ -633,6 +633,17 @@ async def upload_file(
classification_result = classify_valve("", description, main_nom or "")
elif material_type == "BOLT":
classification_result = classify_bolt("", description, main_nom or "")
print(f"🔧 BOLT 분류 결과: {classification_result}")
print(f"🔧 원본 설명: {description}")
print(f"🔧 main_nom: {main_nom}")
# 길이 정보 확인
dimensions_info = classification_result.get("dimensions", {})
print(f"🔧 길이 정보: {dimensions_info}")
# 재질 정보 확인
material_info = classification_result.get("material", {})
print(f"🔧 재질 정보: {material_info}")
elif material_type == "GASKET":
classification_result = classify_gasket("", description, main_nom or "")
elif material_type == "INSTRUMENT":
@@ -1075,12 +1086,35 @@ async def upload_file(
dimensions_info = classification_result.get("dimensions", {})
material_info = classification_result.get("material", {})
print(f"🔧 fastener_type_info: {fastener_type_info}")
# 볼트 타입 (STUD_BOLT, HEX_BOLT 등)
bolt_type = ""
if isinstance(fastener_type_info, dict):
bolt_type = fastener_type_info.get("type", "UNKNOWN")
print(f"🔧 추출된 bolt_type: {bolt_type}")
else:
bolt_type = str(fastener_type_info) if fastener_type_info else "UNKNOWN"
print(f"🔧 문자열 bolt_type: {bolt_type}")
# 특수 용도 볼트 확인 (PSV, LT, CK 등)
special_result = classification_result.get("special_applications", {})
print(f"🔧 special_result: {special_result}")
# 특수 용도가 감지되면 타입 우선 적용
if special_result and special_result.get("detected_applications"):
detected_apps = special_result.get("detected_applications", [])
if "LT" in detected_apps:
bolt_type = "LT_BOLT"
print(f"🔧 특수 용도 감지로 bolt_type 변경: {bolt_type}")
elif "PSV" in detected_apps:
bolt_type = "PSV_BOLT"
print(f"🔧 특수 용도 감지로 bolt_type 변경: {bolt_type}")
elif "CK" in detected_apps:
bolt_type = "CK_BOLT"
print(f"🔧 특수 용도 감지로 bolt_type 변경: {bolt_type}")
print(f"🔧 최종 bolt_type: {bolt_type}")
# 나사 타입 (METRIC, INCH 등)
thread_type = ""
@@ -1553,6 +1587,9 @@ async def get_materials(
fd.fitting_type, fd.fitting_subtype, fd.connection_method, fd.pressure_rating,
fd.material_standard, fd.material_grade as fitting_material_grade, fd.main_size,
fd.reduced_size, fd.length_mm as fitting_length_mm, fd.schedule as fitting_schedule,
gd.gasket_type, gd.gasket_subtype, gd.material_type as gasket_material_type,
gd.filler_material, gd.pressure_rating as gasket_pressure_rating, gd.size_inches as gasket_size_inches,
gd.thickness as gasket_thickness, gd.temperature_range as gasket_temperature_range, gd.fire_safe,
mpt.confirmed_quantity, mpt.purchase_status, mpt.confirmed_by, mpt.confirmed_at,
-- 구매수량 계산에서 분류된 정보를 우선 사용
CASE
@@ -1579,6 +1616,7 @@ async def get_materials(
LEFT JOIN pipe_end_preparations pep ON m.id = pep.material_id
LEFT JOIN fitting_details fd ON m.id = fd.material_id
LEFT JOIN valve_details vd ON m.id = vd.material_id
LEFT JOIN gasket_details gd ON m.id = gd.material_id
LEFT JOIN material_purchase_tracking mpt ON (
m.material_hash = mpt.material_hash
AND f.job_no = mpt.job_no
@@ -1914,17 +1952,18 @@ async def get_materials(
flange_groups[flange_key]["materials"].append(material_dict)
material_dict['clean_description'] = clean_description
elif m.classified_category == 'GASKET':
gasket_query = text("SELECT * FROM gasket_details WHERE material_id = :material_id")
gasket_result = db.execute(gasket_query, {"material_id": m.id})
gasket_detail = gasket_result.fetchone()
if gasket_detail:
# 이미 JOIN된 gasket_details 데이터 사용
if m.gasket_type: # gasket_details가 있는 경우
material_dict['gasket_details'] = {
"gasket_type": gasket_detail.gasket_type,
"material_type": gasket_detail.material_type,
"pressure_rating": gasket_detail.pressure_rating,
"size_inches": gasket_detail.size_inches,
"thickness": gasket_detail.thickness,
"temperature_range": gasket_detail.temperature_range
"gasket_type": m.gasket_type,
"gasket_subtype": m.gasket_subtype,
"material_type": m.gasket_material_type,
"filler_material": m.filler_material,
"pressure_rating": m.gasket_pressure_rating,
"size_inches": m.gasket_size_inches,
"thickness": m.gasket_thickness,
"temperature_range": m.gasket_temperature_range,
"fire_safe": m.fire_safe
}
# 가스켓 그룹핑 - 크기, 압력, 재질로 그룹핑

View File

@@ -12,6 +12,31 @@ def classify_bolt_material(description: str) -> Dict:
desc_upper = description.upper()
# A320/A194M 동시 처리 (예: "ASTM A320/A194M GR B8/8") - 저온용 볼트 조합
if "A320" in desc_upper and "A194" in desc_upper:
# B8/8 등급 추출
bolt_grade = "UNKNOWN"
nut_grade = "UNKNOWN"
if "B8" in desc_upper:
bolt_grade = "B8"
nut_grade = "8" # A320/A194M의 경우 보통 B8/8 조합
elif "L7" in desc_upper:
bolt_grade = "L7"
elif "B8M" in desc_upper:
bolt_grade = "B8M"
combined_grade = f"{bolt_grade}/{nut_grade}" if bolt_grade != "UNKNOWN" and nut_grade != "UNKNOWN" else f"{bolt_grade}" if bolt_grade != "UNKNOWN" else "A320/A194M"
return {
"standard": "ASTM A320/A194M",
"grade": combined_grade,
"material_type": "LOW_TEMP_STAINLESS", # 저온용 스테인리스
"manufacturing": "FORGED",
"confidence": 0.95,
"evidence": ["ASTM_A320_A194M_COMBINED"]
}
# A193/A194 동시 처리 (예: "ASTM A193/A194 GR B7/2H")
if "A193" in desc_upper and "A194" in desc_upper:
# B7/2H 등급 추출
@@ -136,6 +161,39 @@ def classify_bolt_material(description: str) -> Dict:
"evidence": ["ISO_4762_SOCKET_SCREW"]
}
# 일반적인 볼트 재질 패턴 추가 확인
if "B7" in desc_upper and "2H" in desc_upper:
return {
"standard": "ASTM A193/A194",
"grade": "B7/2H",
"material_type": "ALLOY_STEEL",
"manufacturing": "FORGED",
"confidence": 0.85,
"evidence": ["B7_2H_PATTERN"]
}
# 단독 B7 패턴
if "B7" in desc_upper:
return {
"standard": "ASTM A193",
"grade": "B7",
"material_type": "ALLOY_STEEL",
"manufacturing": "FORGED",
"confidence": 0.80,
"evidence": ["B7_PATTERN"]
}
# 단독 2H 패턴
if "2H" in desc_upper:
return {
"standard": "ASTM A194",
"grade": "2H",
"material_type": "ALLOY_STEEL",
"manufacturing": "FORGED",
"confidence": 0.80,
"evidence": ["2H_PATTERN"]
}
# 기본 재질 분류기 호출 (materials_schema 문제가 있어도 우회)
try:
return classify_material(description)
@@ -195,7 +253,7 @@ BOLT_TYPES = {
"LT_BOLT": {
"dat_file_patterns": ["LT_BOLT", "LT_BLT"],
"description_keywords": ["LT", "LOW TEMP", "저온용"],
"description_keywords": ["LT BOLT", "LT BLT", "LOW TEMP", "저온용"],
"characteristics": "저온용 특수 볼트",
"applications": "저온 환경 체결용",
"head_type": "HEXAGON",
@@ -507,7 +565,8 @@ def classify_special_application_bolts(description: str) -> Dict:
# LT 볼트 확인 (저온용 볼트)
lt_patterns = [
r'\bLT\b', # 단어 경계로 LT만
r'\bLT\s', # LT 다음에 공백이 있는 경우만 (LT BOLT, LT BLT)
r'^LT\b', # 문장 시작의 LT만
r'LOW\s+TEMP',
r'저온용',
r'CRYOGENIC',
@@ -915,20 +974,31 @@ def extract_bolt_dimensions(main_nom: str, description: str) -> Dict:
"dimension_description": nominal_size_fraction # 분수로 표시
}
# 길이 정보 추출
# 길이 정보 추출 (개선된 패턴)
length_patterns = [
r'(\d+(?:\.\d+)?)\s*LG', # 70.0000 LG 형태 (최우선)
r'(\d+(?:\.\d+)?)\s*LG', # 70.0000 LG, 145.0000 LG 형태 (최우선)
r'(\d+(?:\.\d+)?)\s*MM\s*LG', # 70MM LG 형태
r'L\s*(\d+(?:\.\d+)?)\s*MM',
r'LENGTH\s*(\d+(?:\.\d+)?)\s*MM',
r'(\d+(?:\.\d+)?)\s*MM\s*LONG',
r'X\s*(\d+(?:\.\d+)?)\s*MM' # M8 X 20MM 형태
r'X\s*(\d+(?:\.\d+)?)\s*MM', # M8 X 20MM 형태
r',\s*(\d+(?:\.\d+)?)\s*LG', # ", 145.0000 LG" 형태 (PSV, LT 볼트용)
r',\s*(\d+(?:\.\d+)?)\s+(?:CK|PSV|LT)', # ", 140 CK" 형태 (PSV 볼트용)
r'(\d+(?:\.\d+)?)\s*MM(?:\s|,|$)', # 75MM 형태 (단독)
r'(\d+(?:\.\d+)?)\s*mm(?:\s|,|$)' # 75mm 형태 (단독)
]
for pattern in length_patterns:
match = re.search(pattern, desc_upper)
if match:
dimensions["length"] = f"{match.group(1)}mm"
length_value = match.group(1)
# 소수점 제거 (145.0000 → 145)
if '.' in length_value and length_value.endswith('.0000'):
length_value = length_value.split('.')[0]
elif '.' in length_value and all(c == '0' for c in length_value.split('.')[1]):
length_value = length_value.split('.')[0]
dimensions["length"] = f"{length_value}mm"
break
# 지름 정보 (이미 main_nom에 있지만 확인)

View File

@@ -89,6 +89,29 @@ def classify_material_integrated(description: str, main_nom: str = "",
desc_upper = description.upper()
# 최우선: SPECIAL 키워드 확인 (도면 업로드가 필요한 특수 자재)
special_keywords = ['SPECIAL', '스페셜', 'SPEC', 'SPL']
for keyword in special_keywords:
if keyword in desc_upper:
return {
"category": "SPECIAL",
"confidence": 1.0,
"evidence": [f"SPECIAL_KEYWORD: {keyword}"],
"classification_level": "LEVEL0_SPECIAL",
"reason": f"스페셜 키워드 발견: {keyword}"
}
# U-BOLT 및 관련 부품 우선 확인 (BOLT 카테고리보다 먼저)
if ('U-BOLT' in desc_upper or 'U BOLT' in desc_upper or '유볼트' in desc_upper or
'URETHANE BLOCK' in desc_upper or 'BLOCK SHOE' in desc_upper or '우레탄' in desc_upper):
return {
"category": "U_BOLT",
"confidence": 1.0,
"evidence": ["U_BOLT_SYSTEM_KEYWORD"],
"classification_level": "LEVEL0_U_BOLT",
"reason": "U-BOLT 시스템 키워드 발견"
}
# 쉼표로 구분된 각 부분을 별도로 체크 (예: "NIPPLE, SMLS, SCH 80")
desc_parts = [part.strip() for part in desc_upper.split(',')]

View File

@@ -30,6 +30,15 @@ def extract_full_material_grade(description: str) -> str:
# ASTM A193/A194 GR B7/2H (볼트용 조합 패턴) - 최우선
r'ASTM\s+A193/A194\s+GR\s+[A-Z0-9/]+',
r'ASTM\s+A193/A194\s+[A-Z0-9/]+',
# ASTM A320/A194M GR B8/8 (저온용 볼트 조합 패턴)
r'ASTM\s+A320[M]?/A194[M]?\s+GR\s+[A-Z0-9/]+',
r'ASTM\s+A320[M]?/A194[M]?\s+[A-Z0-9/]+',
# 단독 A193/A194 패턴 (ASTM 없이)
r'\bA193/A194\s+GR\s+[A-Z0-9/]+\b',
r'\bA193/A194\s+[A-Z0-9/]+\b',
# 단독 A320/A194M 패턴 (ASTM 없이)
r'\bA320[M]?/A194[M]?\s+GR\s+[A-Z0-9/]+\b',
r'\bA320[M]?/A194[M]?\s+[A-Z0-9/]+\b',
# ASTM A312 TP304, ASTM A312 TP316L 등
r'ASTM\s+A\d{3,4}[A-Z]*\s+TP\d+[A-Z]*',
# ASTM A182 F304, ASTM A182 F316L 등

View File

@@ -0,0 +1,130 @@
-- ================================
-- SPECIAL 카테고리 지원 추가 마이그레이션
-- 생성일: 2025.09.30
-- 목적: SPECIAL 카테고리 분류 및 관련 기능 지원
-- ================================
-- 1. materials 테이블 SPECIAL 카테고리 지원 확인
-- ================================
-- classified_category 컬럼이 SPECIAL 값을 지원하는지 확인
-- (이미 VARCHAR(50)이므로 추가 작업 불필요, 하지만 명시적으로 체크)
-- SPECIAL 카테고리 관련 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_special_category
ON materials(classified_category)
WHERE classified_category = 'SPECIAL';
-- 2. SPECIAL 카테고리 분류 규칙 테이블 생성
-- ================================
-- SPECIAL 키워드 패턴 테이블
CREATE TABLE IF NOT EXISTS special_classification_patterns (
id SERIAL PRIMARY KEY,
pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT'
pattern_value VARCHAR(200) NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1, -- 우선순위 (낮을수록 높은 우선순위)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 기본 SPECIAL 키워드 패턴 삽입
INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES
('KEYWORD', 'SPECIAL', '영문 SPECIAL 키워드', 1),
('KEYWORD', '스페셜', '한글 스페셜 키워드', 1),
('KEYWORD', 'SPEC', '영문 SPEC 축약어', 2),
('KEYWORD', 'SPL', '영문 SPL 축약어', 2)
ON CONFLICT DO NOTHING;
-- 3. SPECIAL 자재 추가 정보 테이블
-- ================================
-- SPECIAL 자재 상세 정보 테이블 (도면 업로드 관련)
CREATE TABLE IF NOT EXISTS 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,
-- 도면 정보
drawing_number VARCHAR(100), -- 도면 번호
drawing_revision VARCHAR(20), -- 도면 리비전
drawing_uploaded BOOLEAN DEFAULT FALSE, -- 도면 업로드 여부
drawing_file_path TEXT, -- 도면 파일 경로
-- 특수 요구사항
special_requirements TEXT, -- 특수 제작 요구사항
manufacturing_notes TEXT, -- 제작 참고사항
approval_required BOOLEAN DEFAULT TRUE, -- 승인 필요 여부
approved_by VARCHAR(100), -- 승인자
approved_at TIMESTAMP, -- 승인 일시
-- 분류 정보
classification_confidence FLOAT DEFAULT 1.0,
classification_reason TEXT, -- 분류 근거
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 인덱스 생성 (성능 최적화)
-- ================================
-- special_classification_patterns 인덱스
CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type);
CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active);
CREATE INDEX IF NOT EXISTS idx_special_patterns_priority ON special_classification_patterns(priority);
-- special_material_details 인덱스
CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id);
CREATE INDEX IF NOT EXISTS idx_special_details_file ON special_material_details(file_id);
CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded);
CREATE INDEX IF NOT EXISTS idx_special_details_approval ON special_material_details(approval_required);
-- 5. 기존 자재 재분류 (선택적)
-- ================================
-- 기존 자료 중 SPECIAL 키워드가 포함된 자재를 SPECIAL 카테고리로 재분류
UPDATE materials
SET
classified_category = 'SPECIAL',
classification_confidence = 1.0,
updated_by = 'SYSTEM_MIGRATION',
classified_at = CURRENT_TIMESTAMP
WHERE
(
UPPER(original_description) LIKE '%SPECIAL%' OR
UPPER(original_description) LIKE '%스페셜%' OR
UPPER(original_description) LIKE '%SPEC%' OR
UPPER(original_description) LIKE '%SPL%'
)
AND (classified_category IS NULL OR classified_category != 'SPECIAL');
-- 6. 통계 및 검증
-- ================================
-- SPECIAL 카테고리 자재 개수 확인
DO $$
DECLARE
special_count INTEGER;
BEGIN
SELECT COUNT(*) INTO special_count FROM materials WHERE classified_category = 'SPECIAL';
RAISE NOTICE 'SPECIAL 카테고리로 분류된 자재 개수: %', special_count;
END $$;
-- 7. 권한 설정 (필요시)
-- ================================
-- SPECIAL 자재 관리 권한 (향후 확장용)
-- 현재는 기본 materials 테이블 권한을 따름
COMMIT;
-- ================================
-- 마이그레이션 완료 로그
-- ================================
INSERT INTO migration_log (script_name, executed_at, description) VALUES
('24_add_special_category_support.sql', CURRENT_TIMESTAMP, 'SPECIAL 카테고리 지원 추가 및 기존 자재 재분류')
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
SPECIAL 카테고리 마이그레이션 실행 스크립트
생성일: 2025.09.30
목적: SPECIAL 카테고리 지원 추가 및 기존 자재 재분류
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from app.database import DATABASE_URL
def execute_special_migration():
"""SPECIAL 카테고리 마이그레이션 실행"""
engine = create_engine(DATABASE_URL)
with engine.connect() as conn:
print("🚀 SPECIAL 카테고리 마이그레이션 시작...")
print("=" * 60)
try:
# 1. 마이그레이션 스크립트 실행
print("📋 1단계: 마이그레이션 스크립트 실행...")
script_path = os.path.join(os.path.dirname(__file__), '24_add_special_category_support.sql')
with open(script_path, 'r', encoding='utf-8') as f:
sql_content = f.read()
# SQL 명령어들을 분리하여 실행
sql_commands = sql_content.split(';')
for i, command in enumerate(sql_commands):
command = command.strip()
if command and not command.startswith('--') and command != 'COMMIT':
try:
conn.execute(text(command))
if i % 10 == 0: # 진행상황 표시
print(f" - 명령어 {i+1}/{len(sql_commands)} 실행 중...")
except Exception as e:
print(f" ⚠️ 명령어 실행 중 오류 (무시됨): {e}")
continue
print("✅ 마이그레이션 스크립트 실행 완료")
# 2. 기존 자재 재분류 확인
print("\n📊 2단계: 기존 자재 재분류 결과 확인...")
# SPECIAL 키워드가 포함된 자재 개수 확인
result = conn.execute(text("""
SELECT COUNT(*) as count
FROM materials
WHERE classified_category = 'SPECIAL'
""")).fetchone()
special_count = result.count if result else 0
print(f" - SPECIAL 카테고리로 분류된 자재: {special_count}")
# 키워드별 분류 결과 확인
keyword_results = conn.execute(text("""
SELECT
CASE
WHEN UPPER(original_description) LIKE '%SPECIAL%' THEN 'SPECIAL'
WHEN UPPER(original_description) LIKE '%스페셜%' THEN '스페셜'
WHEN UPPER(original_description) LIKE '%SPEC%' THEN 'SPEC'
WHEN UPPER(original_description) LIKE '%SPL%' THEN 'SPL'
END as keyword_type,
COUNT(*) as count
FROM materials
WHERE classified_category = 'SPECIAL'
GROUP BY keyword_type
ORDER BY count DESC
""")).fetchall()
if keyword_results:
print(" - 키워드별 분류 결과:")
for row in keyword_results:
print(f" * {row.keyword_type}: {row.count}")
# 3. 테이블 생성 확인
print("\n🏗️ 3단계: 새 테이블 생성 확인...")
# special_classification_patterns 테이블 확인
patterns_count = conn.execute(text("""
SELECT COUNT(*) as count
FROM special_classification_patterns
""")).fetchone()
patterns_count = patterns_count.count if patterns_count else 0
print(f" - special_classification_patterns 테이블: {patterns_count}개 패턴 등록됨")
# special_material_details 테이블 확인
details_exists = conn.execute(text("""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'special_material_details'
) as exists
""")).fetchone()
if details_exists.exists:
print(" - special_material_details 테이블: 생성 완료")
else:
print(" - ❌ special_material_details 테이블: 생성 실패")
# 4. 인덱스 생성 확인
print("\n🔍 4단계: 인덱스 생성 확인...")
special_indexes = conn.execute(text("""
SELECT indexname
FROM pg_indexes
WHERE indexname LIKE '%special%'
ORDER BY indexname
""")).fetchall()
if special_indexes:
print(" - SPECIAL 관련 인덱스:")
for idx in special_indexes:
print(f" * {idx.indexname}")
else:
print(" - ⚠️ SPECIAL 관련 인덱스가 생성되지 않았습니다.")
# 커밋
conn.commit()
print("\n" + "=" * 60)
print("🎉 SPECIAL 카테고리 마이그레이션 완료!")
print(f"📊 총 {special_count}개 자재가 SPECIAL 카테고리로 분류되었습니다.")
print("🔧 새로운 기능:")
print(" - SPECIAL 키워드 자동 감지")
print(" - 도면 업로드 관리")
print(" - 특수 제작 요구사항 추적")
print(" - 승인 프로세스 지원")
except Exception as e:
print(f"\n❌ 마이그레이션 실행 중 오류 발생: {e}")
conn.rollback()
raise
def main():
"""메인 실행 함수"""
print("SPECIAL 카테고리 마이그레이션 실행")
print("TK-MP-Project - 특수 자재 관리 시스템")
print("=" * 60)
try:
execute_special_migration()
except Exception as e:
print(f"\n💥 마이그레이션 실패: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
볼트 재질 정보 업데이트 스크립트
A320/A194M 패턴 등을 올바르게 인식하도록 기존 볼트들의 material_grade 재분류
"""
import os
import sys
import psycopg2
from psycopg2.extras import RealDictCursor
# 프로젝트 루트 디렉토리를 Python 경로에 추가
sys.path.append('/app')
from app.services.bolt_classifier import classify_bolt_material
def update_bolt_material_grades():
"""기존 볼트들의 material_grade 업데이트"""
# 데이터베이스 연결
try:
conn = psycopg2.connect(
host=os.getenv('DB_HOST', 'postgres'),
port=os.getenv('DB_PORT', '5432'),
database=os.getenv('DB_NAME', 'tk_mp_bom'),
user=os.getenv('DB_USER', 'tkmp_user'),
password=os.getenv('DB_PASSWORD', 'tkmp2024!')
)
cursor = conn.cursor(cursor_factory=RealDictCursor)
print("🔧 볼트 재질 정보 업데이트 시작...")
# 볼트 카테고리 자재들 조회
cursor.execute("""
SELECT id, original_description, material_grade, full_material_grade
FROM materials
WHERE classified_category = 'BOLT'
ORDER BY id
""")
bolts = cursor.fetchall()
print(f"📊 총 {len(bolts)}개 볼트 발견")
updated_count = 0
for bolt in bolts:
bolt_id = bolt['id']
original_desc = bolt['original_description'] or ''
current_material_grade = bolt['material_grade'] or ''
current_full_grade = bolt['full_material_grade'] or ''
# 볼트 재질 재분류
material_result = classify_bolt_material(original_desc)
if material_result and material_result.get('standard') != 'UNKNOWN':
new_standard = material_result.get('standard', '')
new_grade = material_result.get('grade', '')
# 새로운 material_grade 구성
if new_grade and new_grade != 'UNKNOWN':
if new_standard in new_grade:
# 이미 standard가 포함된 경우 (예: "ASTM A320/A194M")
new_material_grade = new_grade
else:
# standard + grade 조합 (예: "ASTM A193" + "B7")
new_material_grade = f"{new_standard} {new_grade}" if new_grade not in new_standard else new_standard
else:
new_material_grade = new_standard
# 기존 값과 다른 경우에만 업데이트
if new_material_grade != current_material_grade:
print(f"🔄 ID {bolt_id}: '{current_material_grade}''{new_material_grade}'")
print(f" 원본: {original_desc}")
cursor.execute("""
UPDATE materials
SET material_grade = %s
WHERE id = %s
""", (new_material_grade, bolt_id))
updated_count += 1
# 변경사항 커밋
conn.commit()
print(f"✅ 볼트 재질 정보 업데이트 완료: {updated_count}개 업데이트됨")
# 업데이트 결과 확인
cursor.execute("""
SELECT material_grade, COUNT(*) as count
FROM materials
WHERE classified_category = 'BOLT'
GROUP BY material_grade
ORDER BY count DESC
""")
results = cursor.fetchall()
print("\n📈 업데이트 후 볼트 재질 분포:")
for result in results:
print(f" {result['material_grade']}: {result['count']}")
except Exception as e:
print(f"❌ 오류 발생: {str(e)}")
if conn:
conn.rollback()
finally:
if cursor:
cursor.close()
if conn:
conn.close()
if __name__ == "__main__":
update_bolt_material_grades()

View File

@@ -129,6 +129,83 @@ CREATE INDEX IF NOT EXISTS idx_support_details_material_id ON support_details(ma
CREATE INDEX IF NOT EXISTS idx_support_details_file_id ON support_details(file_id);
CREATE INDEX IF NOT EXISTS idx_support_details_support_type ON support_details(support_type);
-- 8. SPECIAL 카테고리 지원 추가
-- ================================
-- SPECIAL 카테고리 관련 인덱스 추가 (성능 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_special_category
ON materials(classified_category)
WHERE classified_category = 'SPECIAL';
-- SPECIAL 키워드 패턴 테이블
CREATE TABLE IF NOT EXISTS special_classification_patterns (
id SERIAL PRIMARY KEY,
pattern_type VARCHAR(20) NOT NULL, -- 'KEYWORD', 'REGEX', 'EXACT'
pattern_value VARCHAR(200) NOT NULL,
description TEXT,
priority INTEGER DEFAULT 1, -- 우선순위 (낮을수록 높은 우선순위)
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 기본 SPECIAL 키워드 패턴 삽입
INSERT INTO special_classification_patterns (pattern_type, pattern_value, description, priority) VALUES
('KEYWORD', 'SPECIAL', '영문 SPECIAL 키워드', 1),
('KEYWORD', '스페셜', '한글 스페셜 키워드', 1),
('KEYWORD', 'SPEC', '영문 SPEC 축약어', 2),
('KEYWORD', 'SPL', '영문 SPL 축약어', 2)
ON CONFLICT DO NOTHING;
-- SPECIAL 자재 상세 정보 테이블 (도면 업로드 관련)
CREATE TABLE IF NOT EXISTS 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,
-- 도면 정보
drawing_number VARCHAR(100), -- 도면 번호
drawing_revision VARCHAR(20), -- 도면 리비전
drawing_uploaded BOOLEAN DEFAULT FALSE, -- 도면 업로드 여부
drawing_file_path TEXT, -- 도면 파일 경로
-- 특수 요구사항
special_requirements TEXT, -- 특수 제작 요구사항
manufacturing_notes TEXT, -- 제작 참고사항
approval_required BOOLEAN DEFAULT TRUE, -- 승인 필요 여부
approved_by VARCHAR(100), -- 승인자
approved_at TIMESTAMP, -- 승인 일시
-- 분류 정보
classification_confidence FLOAT DEFAULT 1.0,
classification_reason TEXT, -- 분류 근거
-- 관리 정보
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- SPECIAL 관련 인덱스
CREATE INDEX IF NOT EXISTS idx_special_patterns_type ON special_classification_patterns(pattern_type);
CREATE INDEX IF NOT EXISTS idx_special_patterns_active ON special_classification_patterns(is_active);
CREATE INDEX IF NOT EXISTS idx_special_details_material ON special_material_details(material_id);
CREATE INDEX IF NOT EXISTS idx_special_details_drawing_uploaded ON special_material_details(drawing_uploaded);
-- 기존 자재 중 SPECIAL 키워드가 포함된 자재를 SPECIAL 카테고리로 재분류
UPDATE materials
SET
classified_category = 'SPECIAL',
classification_confidence = 1.0,
classified_at = CURRENT_TIMESTAMP
WHERE
(
UPPER(original_description) LIKE '%SPECIAL%' OR
UPPER(original_description) LIKE '%스페셜%' OR
UPPER(original_description) LIKE '%SPEC%' OR
UPPER(original_description) LIKE '%SPL%'
)
AND (classified_category IS NULL OR classified_category != 'SPECIAL');
-- 7. 기존 데이터 정리 (선택사항)
-- ================================