import asyncio import os import sys import uuid from datetime import datetime, timezone, timedelta sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv() from sqlalchemy import select from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from src.database.models import ( Base, User, Tenant, Machine, EquipmentPart, PartCounter, InspectionTemplate, InspectionTemplateItem, ) from src.auth.password import hash_password DATABASE_URL = os.getenv( "DATABASE_URL", "postgresql+asyncpg://factoryops:factoryops@localhost:5432/factoryops_v2", ) TENANTS = [ {"id": "spifox", "name": "SpiFox", "industry_type": "semiconductor"}, {"id": "enkid", "name": "Enkid", "industry_type": "manufacturing"}, {"id": "alpet", "name": "Alpet", "industry_type": "chemical"}, ] SUPERADMIN = { "email": "admin@vexplor.com", "password": "qlalfqjsgh11", "name": "Super Admin", "role": "superadmin", "tenant_id": None, } TENANT_ADMINS = [ { "email": "admin@spifox.com", "password": "qlalfqjsgh11", "name": "SpiFox Admin", "role": "tenant_admin", "tenant_id": "spifox", }, { "email": "admin@enkid.com", "password": "qlalfqjsgh11", "name": "Enkid Admin", "role": "tenant_admin", "tenant_id": "enkid", }, { "email": "admin@alpet.com", "password": "qlalfqjsgh11", "name": "Alpet Admin", "role": "tenant_admin", "tenant_id": "alpet", }, ] # ─── 고객사별 설비 데이터 ─── NOW = datetime.now(timezone.utc) MACHINES = { "spifox": [ { "name": "CVD Chamber #1", "equipment_code": "SF-CVD-001", "model": "AMAT Centura 5200", }, {"name": "Etcher DRY-A", "equipment_code": "SF-ETCH-001", "model": "LAM 9600"}, { "name": "Sputter MC-200", "equipment_code": "SF-SPT-001", "model": "Ulvac SH-450", }, ], "enkid": [ { "name": "CNC 선반 #1", "equipment_code": "EK-CNC-001", "model": "두산 LYNX 220", }, { "name": "프레스 500T", "equipment_code": "EK-PRS-001", "model": "현대위아 HF-500", }, { "name": "용접 로봇 #1", "equipment_code": "EK-WLD-001", "model": "화낙 ARC Mate 120", }, ], "alpet": [ { "name": "반응기 R-101", "equipment_code": "AP-RCT-101", "model": "Custom Reactor 5000L", }, { "name": "증류탑 D-201", "equipment_code": "AP-DST-201", "model": "Sulzer Packed Tower", }, { "name": "열교환기 E-301", "equipment_code": "AP-HEX-301", "model": "Alfa Laval M10-BW", }, ], } # 부품: (name, part_number, category, lifecycle_type, lifecycle_limit, alarm_threshold, counter_source, current_value) PARTS = { "spifox": { "CVD Chamber #1": [ ("Showerhead", "SH-CVD-01", "소모품", "hours", 3000, 80, "auto_time", 2450), ( "Heater Block", "HB-CVD-01", "핵심부품", "hours", 8000, 85, "auto_time", 3200, ), ("O-Ring Seal", "OR-CVD-01", "소모품", "count", 500, 90, "manual", 480), ], "Etcher DRY-A": [ ( "RF Generator", "RF-ETCH-01", "핵심부품", "hours", 10000, 80, "auto_time", 4500, ), ( "Focus Ring", "FR-ETCH-01", "소모품", "hours", 2000, 85, "auto_time", 1750, ), ("ESC Plate", "ESC-ETCH-01", "핵심부품", "count", 5000, 80, "manual", 2100), ], "Sputter MC-200": [ ( "Target (Ti)", "TG-SPT-01", "소모품", "hours", 1500, 80, "auto_time", 1200, ), ( "Magnet Assembly", "MA-SPT-01", "핵심부품", "hours", 12000, 85, "auto_time", 5000, ), ], }, "enkid": { "CNC 선반 #1": [ ( "스핀들 베어링", "BRG-CNC-01", "핵심부품", "hours", 15000, 85, "auto_time", 12800, ), ( "절삭공구 홀더", "TH-CNC-01", "소모품", "count", 10000, 80, "manual", 7500, ), ( "볼스크류", "BS-CNC-01", "핵심부품", "hours", 20000, 90, "auto_time", 8000, ), ], "프레스 500T": [ ( "유압 실린더 씰", "HS-PRS-01", "소모품", "count", 50000, 85, "manual", 42000, ), ( "금형 상판", "MD-PRS-01", "핵심부품", "count", 100000, 80, "manual", 65000, ), ( "유압 펌프", "HP-PRS-01", "핵심부품", "hours", 10000, 85, "auto_time", 4200, ), ], "용접 로봇 #1": [ ("용접 토치", "WT-WLD-01", "소모품", "hours", 500, 80, "auto_time", 320), ( "와이어 피더", "WF-WLD-01", "소모품", "hours", 3000, 85, "auto_time", 1500, ), ], }, "alpet": { "반응기 R-101": [ ( "교반기 임펠러", "IMP-RCT-01", "핵심부품", "hours", 20000, 85, "auto_time", 15500, ), ("기계적 씰", "MS-RCT-01", "소모품", "hours", 8000, 80, "auto_time", 6800), ("온도 센서", "TS-RCT-01", "계측", "hours", 5000, 90, "auto_time", 4600), ], "증류탑 D-201": [ ( "충전물 (패킹)", "PK-DST-01", "소모품", "hours", 30000, 85, "auto_time", 12000, ), ( "컨덴서 튜브", "CT-DST-01", "핵심부품", "hours", 25000, 80, "auto_time", 10000, ), ], "열교환기 E-301": [ ( "개스킷 세트", "GK-HEX-01", "소모품", "hours", 8000, 80, "auto_time", 7200, ), ( "플레이트 팩", "PP-HEX-01", "핵심부품", "hours", 40000, 85, "auto_time", 15000, ), ("압력 센서", "PS-HEX-01", "계측", "hours", 5000, 90, "auto_time", 2000), ], }, } # ─── 고객사별 검사 템플릿 ─── TEMPLATES = { "spifox": [ { "name": "CVD 챔버 일일 점검", "subject_type": "equipment", "machine_name": "CVD Chamber #1", "schedule_type": "daily", "inspection_mode": "checklist", "items": [ { "name": "챔버 진공도 확인", "category": "진공", "data_type": "numeric", "unit": "mTorr", "spec_min": 0, "spec_max": 10, "warning_max": 8, }, { "name": "가스 유량 정상 여부", "category": "가스", "data_type": "boolean", }, { "name": "히터 온도", "category": "온도", "data_type": "numeric", "unit": "°C", "spec_min": 350, "spec_max": 450, "warning_min": 360, "warning_max": 440, }, { "name": "RF 전력 안정성", "category": "전기", "data_type": "select", "select_options": ["정상", "불안정", "이상"], }, { "name": "파티클 카운트", "category": "청정도", "data_type": "numeric", "unit": "ea", "spec_min": 0, "spec_max": 50, "warning_max": 30, }, ], }, { "name": "웨이퍼 두께 품질검사", "subject_type": "product", "product_code": "WF-300-SI", "schedule_type": "ad_hoc", "inspection_mode": "measurement", "items": [ { "name": "중심부 두께", "category": "두께", "data_type": "numeric", "unit": "μm", "spec_min": 725, "spec_max": 775, "warning_min": 730, "warning_max": 770, }, { "name": "Edge 두께 편차", "category": "두께", "data_type": "numeric", "unit": "μm", "spec_min": 0, "spec_max": 5, "warning_max": 3, }, {"name": "표면 결함 유무", "category": "외관", "data_type": "boolean"}, { "name": "비고", "category": "기타", "data_type": "text", "is_required": False, }, ], }, ], "enkid": [ { "name": "CNC 선반 주간 점검", "subject_type": "equipment", "machine_name": "CNC 선반 #1", "schedule_type": "weekly", "inspection_mode": "checklist", "items": [ { "name": "스핀들 진동값", "category": "진동", "data_type": "numeric", "unit": "mm/s", "spec_min": 0, "spec_max": 4.5, "warning_max": 3.5, }, { "name": "절삭유 농도", "category": "유체", "data_type": "numeric", "unit": "%", "spec_min": 3, "spec_max": 8, "warning_min": 4, "warning_max": 7, }, {"name": "척 그립력 정상", "category": "기계", "data_type": "boolean"}, { "name": "윤활유 레벨", "category": "유체", "data_type": "select", "select_options": ["적정", "부족", "과다"], }, {"name": "이상 소음 여부", "category": "소음", "data_type": "boolean"}, { "name": "툴 마모 상태", "category": "공구", "data_type": "select", "select_options": ["양호", "주의", "교체필요"], }, ], }, { "name": "가공품 치수 검사", "subject_type": "product", "product_code": "SHAFT-AL-20", "schedule_type": "ad_hoc", "inspection_mode": "measurement", "items": [ { "name": "외경", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 19.98, "spec_max": 20.02, "warning_min": 19.99, "warning_max": 20.01, }, { "name": "길이", "category": "치수", "data_type": "numeric", "unit": "mm", "spec_min": 99.9, "spec_max": 100.1, "warning_min": 99.95, "warning_max": 100.05, }, { "name": "표면 거칠기 (Ra)", "category": "표면", "data_type": "numeric", "unit": "μm", "spec_min": 0, "spec_max": 1.6, "warning_max": 1.2, }, {"name": "외관 검사 합격", "category": "외관", "data_type": "boolean"}, ], }, ], "alpet": [ { "name": "반응기 일일 점검", "subject_type": "equipment", "machine_name": "반응기 R-101", "schedule_type": "daily", "inspection_mode": "checklist", "items": [ { "name": "반응 온도", "category": "온도", "data_type": "numeric", "unit": "°C", "spec_min": 180, "spec_max": 220, "warning_min": 185, "warning_max": 215, }, { "name": "반응 압력", "category": "압력", "data_type": "numeric", "unit": "bar", "spec_min": 2.0, "spec_max": 5.0, "warning_min": 2.5, "warning_max": 4.5, }, { "name": "교반 속도", "category": "기계", "data_type": "numeric", "unit": "RPM", "spec_min": 100, "spec_max": 300, "warning_min": 120, "warning_max": 280, }, { "name": "냉각수 유량 정상", "category": "유체", "data_type": "boolean", }, {"name": "누출 여부", "category": "안전", "data_type": "boolean"}, { "name": "씰 상태", "category": "기계", "data_type": "select", "select_options": ["양호", "주의", "교체필요"], }, ], }, { "name": "제품 농도 분석", "subject_type": "product", "product_code": "SOL-HCL-35", "schedule_type": "ad_hoc", "inspection_mode": "measurement", "items": [ { "name": "농도 (wt%)", "category": "농도", "data_type": "numeric", "unit": "wt%", "spec_min": 34.0, "spec_max": 36.0, "warning_min": 34.5, "warning_max": 35.5, }, { "name": "pH", "category": "농도", "data_type": "numeric", "unit": "pH", "spec_min": 0.1, "spec_max": 1.0, "warning_max": 0.8, }, { "name": "비중", "category": "물성", "data_type": "numeric", "unit": "g/mL", "spec_min": 1.16, "spec_max": 1.19, "warning_min": 1.165, "warning_max": 1.185, }, {"name": "색상 이상 유무", "category": "외관", "data_type": "boolean"}, { "name": "비고", "category": "기타", "data_type": "text", "is_required": False, }, ], }, ], } async def seed(): engine = create_async_engine(DATABASE_URL, echo=False) session_factory = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) async with session_factory() as db: # ── 1. Tenants ── for t in TENANTS: existing = await db.execute(select(Tenant).where(Tenant.id == t["id"])) if existing.scalar_one_or_none(): print(f" Tenant '{t['id']}' already exists, skipping") continue db.add(Tenant(**t)) print(f" + Tenant '{t['id']}' ({t['name']})") await db.commit() # ── 2. Users ── all_users = [SUPERADMIN] + TENANT_ADMINS for u in all_users: existing = await db.execute(select(User).where(User.email == u["email"])) if existing.scalar_one_or_none(): print(f" User '{u['email']}' already exists, skipping") continue db.add( User( id=uuid.uuid4(), email=u["email"], password_hash=hash_password(u["password"]), name=u["name"], role=u["role"], tenant_id=u["tenant_id"], ) ) print(f" + User '{u['email']}' (role={u['role']})") await db.commit() # ── 3. Machines ── machine_map = {} # (tenant_id, machine_name) → machine_id for tenant_id, machines in MACHINES.items(): for m in machines: existing = await db.execute( select(Machine).where( Machine.tenant_id == tenant_id, Machine.name == m["name"], ) ) row = existing.scalar_one_or_none() if row: print( f" Machine '{m['name']}' ({tenant_id}) already exists, skipping" ) machine_map[(tenant_id, m["name"])] = row.id continue machine_id = uuid.uuid4() db.add( Machine( id=machine_id, tenant_id=tenant_id, name=m["name"], equipment_code=m["equipment_code"], model=m["model"], ) ) machine_map[(tenant_id, m["name"])] = machine_id print(f" + Machine '{m['name']}' ({tenant_id})") await db.commit() # ── 4. Equipment Parts + Counters ── for tenant_id, machines_parts in PARTS.items(): for machine_name, parts in machines_parts.items(): machine_id = machine_map.get((tenant_id, machine_name)) if not machine_id: print(f" ! Machine '{machine_name}' not found for parts, skipping") continue for name, part_number, category, lt, ll, at, cs, cv in parts: existing = await db.execute( select(EquipmentPart).where( EquipmentPart.tenant_id == tenant_id, EquipmentPart.machine_id == machine_id, EquipmentPart.name == name, ) ) if existing.scalar_one_or_none(): print( f" Part '{name}' ({tenant_id}/{machine_name}) already exists, skipping" ) continue part_id = uuid.uuid4() db.add( EquipmentPart( id=part_id, tenant_id=tenant_id, machine_id=machine_id, name=name, part_number=part_number, category=category, lifecycle_type=lt, lifecycle_limit=ll, alarm_threshold=at, counter_source=cs, installed_at=NOW - timedelta(days=180), is_active=True, ) ) lifecycle_pct = round((cv / ll) * 100, 1) db.add( PartCounter( id=uuid.uuid4(), tenant_id=tenant_id, equipment_part_id=part_id, current_value=cv, lifecycle_pct=lifecycle_pct, last_reset_at=NOW - timedelta(days=180), last_updated_at=NOW - timedelta(hours=2), version=1, ) ) print( f" + Part '{name}' ({tenant_id}/{machine_name}) — {cv}/{ll} ({lifecycle_pct}%)" ) await db.commit() # ── 5. Inspection Templates ── for tenant_id, templates in TEMPLATES.items(): for tmpl in templates: existing = await db.execute( select(InspectionTemplate).where( InspectionTemplate.tenant_id == tenant_id, InspectionTemplate.name == tmpl["name"], ) ) if existing.scalar_one_or_none(): print( f" Template '{tmpl['name']}' ({tenant_id}) already exists, skipping" ) continue template_id = uuid.uuid4() machine_id_for_tmpl = None if tmpl.get("machine_name"): machine_id_for_tmpl = machine_map.get( (tenant_id, tmpl["machine_name"]) ) db.add( InspectionTemplate( id=template_id, tenant_id=tenant_id, name=tmpl["name"], subject_type=tmpl["subject_type"], machine_id=machine_id_for_tmpl, product_code=tmpl.get("product_code"), schedule_type=tmpl["schedule_type"], inspection_mode=tmpl["inspection_mode"], version=1, is_active=True, ) ) for idx, item in enumerate(tmpl["items"]): db.add( InspectionTemplateItem( id=uuid.uuid4(), template_id=template_id, sort_order=idx + 1, name=item["name"], category=item.get("category"), data_type=item["data_type"], unit=item.get("unit"), spec_min=item.get("spec_min"), spec_max=item.get("spec_max"), warning_min=item.get("warning_min"), warning_max=item.get("warning_max"), select_options=item.get("select_options"), is_required=item.get("is_required", True), ) ) print( f" + Template '{tmpl['name']}' ({tenant_id}) — {len(tmpl['items'])} items" ) await db.commit() await engine.dispose() print("\nSeed complete.") if __name__ == "__main__": print("Seeding FactoryOps v2 database...\n") asyncio.run(seed())