Files
factoryOps-v2/scripts/seed.py
Johngreen 62e54ae2ef
All checks were successful
Deploy to Production / deploy (push) Successful in 48s
feat: replace spifox seed data with real factory data from business plan
- Replace semiconductor equipment with actual press forming equipment (14 machines)
- Add real equipment: cup presses, finish presses, dryer, washer, heat treatment,
  vision inspectors, conveyor, AMR across 6 area groups
- Add real die parts: cut pin, cut die, forming die with count-based lifecycles
- Add press daily inspection + heat treatment inspection + case dimension inspection templates
- Fix seed.py to pass area/criticality when creating machines
- Add backfill migration to populate area/criticality for existing DB records
- Change spifox industry_type from semiconductor to press_forming
2026-02-10 22:45:53 +09:00

1277 lines
43 KiB
Python

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": "press_forming"},
{"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": "반제품 프레스 #1",
"equipment_code": "SF-CUP-001",
"model": "Cup Making Press 60T",
"manufacturer": "SpiFox 자체설계",
"location": "1동 반제품 라인",
"area": "반제품 프레스",
"criticality": "critical",
"rated_capacity": "60T, UPH 520pcs",
"power_rating": "15kW",
"description": "알루미늄 슬러그 → CUP 형상 1차 성형. 딥드로잉 공법.",
},
{
"name": "반제품 프레스 #2",
"equipment_code": "SF-CUP-002",
"model": "Cup Making Press 60T",
"manufacturer": "SpiFox 자체설계",
"location": "1동 반제품 라인",
"area": "반제품 프레스",
"criticality": "critical",
"rated_capacity": "60T, UPH 520pcs",
"power_rating": "15kW",
"description": "알루미늄 슬러그 → CUP 형상 1차 성형.",
},
{
"name": "완제품 프레스 #1",
"equipment_code": "SF-FIN-001",
"model": "Finish Press 80T",
"manufacturer": "SpiFox 자체설계",
"location": "2동 완제품 성형 라인 A",
"area": "완제품 성형 A",
"criticality": "critical",
"rated_capacity": "80T, UPH 480pcs",
"power_rating": "18.5kW",
"description": "2차 딥드로잉 성형. 콘덴서 케이스 최종 형상.",
},
{
"name": "완제품 프레스 #2",
"equipment_code": "SF-FIN-002",
"model": "Finish Press 80T",
"manufacturer": "SpiFox 자체설계",
"location": "2동 완제품 성형 라인 A",
"area": "완제품 성형 A",
"criticality": "critical",
"rated_capacity": "80T, UPH 480pcs",
"power_rating": "18.5kW",
"description": "2차 딥드로잉 성형.",
},
{
"name": "완제품 프레스 #3",
"equipment_code": "SF-FIN-003",
"model": "Finish Press 80T",
"manufacturer": "SpiFox 자체설계",
"location": "2동 완제품 성형 라인 B",
"area": "완제품 성형 B",
"criticality": "critical",
"rated_capacity": "80T, UPH 480pcs",
"power_rating": "18.5kW",
},
{
"name": "건조기 #1",
"equipment_code": "SF-DRY-001",
"model": "Hot Air Dryer 300",
"manufacturer": "대한열기",
"location": "1동 건조 구역",
"area": "건조/세척",
"criticality": "major",
"rated_capacity": "반제품 건조 300kg/batch",
"power_rating": "25kW",
"description": "반제품 CUP 건조. 열풍 순환 방식.",
},
{
"name": "탈유기 #1",
"equipment_code": "SF-DEG-001",
"model": "Degreaser SC-500",
"manufacturer": "세정산업",
"location": "2동 후공정 라인",
"area": "건조/세척",
"criticality": "major",
"rated_capacity": "500L 세척조",
"power_rating": "12kW",
"description": "성형유 제거. 초음파 탈유 방식.",
},
{
"name": "세척기 #1",
"equipment_code": "SF-WSH-001",
"model": "Ultrasonic Washer UW-800",
"manufacturer": "세정산업",
"location": "2동 후공정 라인",
"area": "건조/세척",
"criticality": "major",
"rated_capacity": "800L 세척조",
"power_rating": "15kW",
"description": "탈유 후 초음파 세척.",
},
{
"name": "열처리로 #1",
"equipment_code": "SF-HT-001",
"model": "Annealing Furnace AF-1200",
"manufacturer": "한국전기로",
"location": "2동 열처리 구역",
"area": "열처리",
"criticality": "critical",
"rated_capacity": "1200°C, 500kg/batch",
"power_rating": "80kW",
"description": "어닐링 열처리. 알루미늄 케이스 응력 제거.",
},
{
"name": "머신비전 검사기 #1",
"equipment_code": "SF-VIS-001",
"model": "AI Vision Inspector V3",
"manufacturer": "SpiFox 자체개발",
"location": "2동 검사 라인",
"area": "검사/포장",
"criticality": "critical",
"rated_capacity": "검사속도 1,200pcs/min",
"power_rating": "3kW",
"description": "AI 머신러닝 기반 전수 외관검사. 치수/형상/결함 동시 검출.",
},
{
"name": "머신비전 검사기 #2",
"equipment_code": "SF-VIS-002",
"model": "AI Vision Inspector V3",
"manufacturer": "SpiFox 자체개발",
"location": "2동 검사 라인",
"area": "검사/포장",
"criticality": "critical",
"rated_capacity": "검사속도 1,200pcs/min",
"power_rating": "3kW",
},
{
"name": "자동포장기 #1",
"equipment_code": "SF-PKG-001",
"model": "Auto Packer AP-200",
"manufacturer": "포장기계",
"location": "2동 포장 구역",
"area": "검사/포장",
"criticality": "minor",
"rated_capacity": "200box/h",
"power_rating": "5kW",
},
{
"name": "구름다리 컨베이어",
"equipment_code": "SF-CVY-001",
"model": "Overbridge Conveyor OB-50",
"manufacturer": "물류자동화",
"location": "1동-2동 연결",
"area": "물류/반송",
"criticality": "major",
"rated_capacity": "반제품 이송 50m",
"power_rating": "7.5kW",
"description": "1동(반제품)→2동(완제품) 반제품 및 빈 통 이송.",
},
{
"name": "AMR #1",
"equipment_code": "SF-AMR-001",
"model": "자율주행로봇 AGV-300",
"manufacturer": "로봇솔루션",
"location": "2동 물류 구역",
"area": "물류/반송",
"criticality": "minor",
"rated_capacity": "적재 300kg",
"power_rating": "배터리 48V",
"description": "완제품 자동 반송. 성형→후공정→포장 경로.",
},
],
"enkid": [
{
"name": "CNC 선반 #1",
"equipment_code": "EK-CNC-001",
"model": "두산 LYNX 220",
"manufacturer": "두산공작기계",
"location": "1공장 A라인",
"area": "A라인",
"criticality": "major",
"rated_capacity": "최대 가공경 220mm",
"power_rating": "15kW (주축 모터)",
"description": "2축 CNC 선반. 알루미늄/스틸 가공용.",
},
{
"name": "프레스 500T",
"equipment_code": "EK-PRS-001",
"model": "현대위아 HF-500",
"manufacturer": "현대위아",
"location": "1공장 B라인",
"area": "B라인",
"criticality": "critical",
"rated_capacity": "500톤",
"power_rating": "37kW",
},
{
"name": "용접 로봇 #1",
"equipment_code": "EK-WLD-001",
"model": "화낙 ARC Mate 120",
"manufacturer": "FANUC",
"location": "2공장 용접 셀 1",
"area": "용접 셀",
"criticality": "major",
"rated_capacity": "6축 120kg 가반하중",
"power_rating": "6kVA",
},
],
"alpet": [
{
"name": "반응기 R-101",
"equipment_code": "AP-RCT-101",
"model": "Custom Reactor 5000L",
"manufacturer": "한화솔루션 엔지니어링",
"location": "1플랜트 반응 구역",
"area": "반응 구역",
"criticality": "critical",
"rated_capacity": "5,000L (SUS316L)",
"power_rating": "22kW (교반기)",
"description": "교반식 배치 반응기. HCl 합성 공정.",
},
{
"name": "증류탑 D-201",
"equipment_code": "AP-DST-201",
"model": "Sulzer Packed Tower",
"manufacturer": "Sulzer",
"location": "1플랜트 분리 구역",
"area": "분리 구역",
"criticality": "critical",
"rated_capacity": "처리량 2,000 kg/h",
"power_rating": "리보일러 150kW",
},
{
"name": "열교환기 E-301",
"equipment_code": "AP-HEX-301",
"model": "Alfa Laval M10-BW",
"manufacturer": "Alfa Laval",
"location": "1플랜트 유틸리티",
"area": "유틸리티",
"criticality": "minor",
"rated_capacity": "열전달 500kW",
"power_rating": "펌프 7.5kW",
},
],
}
# 부품: (name, part_number, category, lifecycle_type, lifecycle_limit, alarm_threshold, counter_source, current_value)
PARTS = {
"spifox": {
"반제품 프레스 #1": [
(
"컷트핀 (Cut Pin)",
"CP-CUP-01",
"금형",
"count",
150000,
80,
"manual",
128000,
),
(
"컷트메스 (Cut Die)",
"CD-CUP-01",
"금형",
"count",
200000,
85,
"manual",
165000,
),
("스트리퍼", "ST-CUP-01", "금형", "count", 300000, 80, "manual", 210000),
],
"반제품 프레스 #2": [
(
"컷트핀 (Cut Pin)",
"CP-CUP-02",
"금형",
"count",
150000,
80,
"manual",
95000,
),
(
"컷트메스 (Cut Die)",
"CD-CUP-02",
"금형",
"count",
200000,
85,
"manual",
120000,
),
("스트리퍼", "ST-CUP-02", "금형", "count", 300000, 80, "manual", 180000),
],
"완제품 프레스 #1": [
(
"정형메스 (Forming Die)",
"FD-FIN-01",
"금형",
"count",
100000,
80,
"manual",
87000,
),
(
"컷트핀 (Cut Pin)",
"CP-FIN-01",
"금형",
"count",
120000,
80,
"manual",
98000,
),
("펀치", "PH-FIN-01", "금형", "count", 250000, 85, "manual", 175000),
("성형유 필터", "OF-FIN-01", "소모품", "hours", 720, 80, "auto_time", 580),
],
"완제품 프레스 #2": [
(
"정형메스 (Forming Die)",
"FD-FIN-02",
"금형",
"count",
100000,
80,
"manual",
62000,
),
(
"컷트핀 (Cut Pin)",
"CP-FIN-02",
"금형",
"count",
120000,
80,
"manual",
73000,
),
("펀치", "PH-FIN-02", "금형", "count", 250000, 85, "manual", 140000),
],
"완제품 프레스 #3": [
(
"정형메스 (Forming Die)",
"FD-FIN-03",
"금형",
"count",
100000,
80,
"manual",
45000,
),
(
"컷트핀 (Cut Pin)",
"CP-FIN-03",
"금형",
"count",
120000,
80,
"manual",
52000,
),
("펀치", "PH-FIN-03", "금형", "count", 250000, 85, "manual", 95000),
],
"건조기 #1": [
(
"히터 엘리먼트",
"HE-DRY-01",
"핵심부품",
"hours",
8000,
85,
"auto_time",
6500,
),
(
"순환팬 베어링",
"FB-DRY-01",
"소모품",
"hours",
15000,
80,
"auto_time",
11000,
),
],
"열처리로 #1": [
("발열체", "HT-HT-01", "핵심부품", "hours", 10000, 80, "auto_time", 7200),
(
"온도 센서 (TC)",
"TC-HT-01",
"계측",
"hours",
5000,
90,
"auto_time",
4200,
),
("단열재", "IN-HT-01", "소모품", "hours", 20000, 85, "auto_time", 14000),
],
"머신비전 검사기 #1": [
(
"카메라 모듈",
"CM-VIS-01",
"핵심부품",
"hours",
20000,
85,
"auto_time",
8500,
),
("조명 LED", "LD-VIS-01", "소모품", "hours", 10000, 80, "auto_time", 7800),
("교정 인증서", "CAL-VIS-01", "인증", "date", 365, 80, "manual", 0),
],
},
"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,
),
(
"안전 검사 인증",
"CERT-WLD-01",
"인증",
"date",
180,
85,
"manual",
0,
),
],
},
"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),
(
"내압 검사 인증",
"CERT-HEX-01",
"인증",
"date",
730,
85,
"manual",
0,
),
],
},
}
# ─── 고객사별 검사 템플릿 ───
TEMPLATES = {
"spifox": [
{
"name": "프레스 일일 점검",
"subject_type": "equipment",
"machine_name": "완제품 프레스 #1",
"schedule_type": "daily",
"inspection_mode": "checklist",
"items": [
{
"name": "타발 압력",
"category": "프레스",
"data_type": "numeric",
"unit": "T",
"spec_min": 70,
"spec_max": 85,
"warning_min": 72,
"warning_max": 83,
},
{
"name": "슬라이드 스트로크",
"category": "프레스",
"data_type": "numeric",
"unit": "mm",
"spec_min": 148,
"spec_max": 152,
"warning_min": 149,
"warning_max": 151,
},
{
"name": "성형유 공급 정상",
"category": "윤활",
"data_type": "boolean",
},
{
"name": "금형 상태",
"category": "금형",
"data_type": "select",
"select_options": ["양호", "주의", "교체필요"],
},
{
"name": "이상 소음/진동 여부",
"category": "기계",
"data_type": "boolean",
},
{
"name": "성형유 온도",
"category": "윤활",
"data_type": "numeric",
"unit": "°C",
"spec_min": 30,
"spec_max": 50,
"warning_max": 45,
},
{
"name": "안전장치 작동 확인",
"category": "안전",
"data_type": "boolean",
},
{
"name": "타발수 (현재)",
"category": "생산",
"data_type": "numeric",
"unit": "",
},
],
},
{
"name": "반제품 프레스 일일 점검",
"subject_type": "equipment",
"machine_name": "반제품 프레스 #1",
"schedule_type": "daily",
"inspection_mode": "checklist",
"items": [
{
"name": "타발 압력",
"category": "프레스",
"data_type": "numeric",
"unit": "T",
"spec_min": 50,
"spec_max": 65,
"warning_min": 52,
"warning_max": 63,
},
{
"name": "슬러그 공급 정상",
"category": "소재",
"data_type": "boolean",
},
{
"name": "금형 상태",
"category": "금형",
"data_type": "select",
"select_options": ["양호", "주의", "교체필요"],
},
{
"name": "성형유 공급 정상",
"category": "윤활",
"data_type": "boolean",
},
{
"name": "이상 소음/진동 여부",
"category": "기계",
"data_type": "boolean",
},
{
"name": "안전장치 작동 확인",
"category": "안전",
"data_type": "boolean",
},
],
},
{
"name": "열처리로 일일 점검",
"subject_type": "equipment",
"machine_name": "열처리로 #1",
"schedule_type": "daily",
"inspection_mode": "checklist",
"items": [
{
"name": "노내 온도",
"category": "온도",
"data_type": "numeric",
"unit": "°C",
"spec_min": 350,
"spec_max": 420,
"warning_min": 360,
"warning_max": 410,
},
{
"name": "온도 균일도 (편차)",
"category": "온도",
"data_type": "numeric",
"unit": "°C",
"spec_min": 0,
"spec_max": 10,
"warning_max": 7,
},
{
"name": "배기 팬 작동 정상",
"category": "기계",
"data_type": "boolean",
},
{"name": "도어 씰 상태", "category": "기계", "data_type": "boolean"},
{
"name": "가스 분위기",
"category": "분위기",
"data_type": "select",
"select_options": ["정상", "불안정", "이상"],
},
],
},
{
"name": "콘덴서 케이스 치수검사",
"subject_type": "product",
"product_code": "CASE-AL-1012",
"schedule_type": "ad_hoc",
"inspection_mode": "measurement",
"items": [
{
"name": "컷트핀 내경",
"category": "치수",
"data_type": "numeric",
"unit": "mm",
"spec_min": 9.98,
"spec_max": 10.02,
"warning_min": 9.99,
"warning_max": 10.01,
},
{
"name": "컷트핀 외경",
"category": "치수",
"data_type": "numeric",
"unit": "mm",
"spec_min": 12.48,
"spec_max": 12.52,
"warning_min": 12.49,
"warning_max": 12.51,
},
{
"name": "정형메스 내경",
"category": "치수",
"data_type": "numeric",
"unit": "mm",
"spec_min": 9.96,
"spec_max": 10.04,
"warning_min": 9.98,
"warning_max": 10.02,
},
{
"name": "CR값",
"category": "형상",
"data_type": "numeric",
"unit": "mm",
"spec_min": 0.3,
"spec_max": 0.5,
"warning_min": 0.32,
"warning_max": 0.48,
},
{
"name": "케이스 높이",
"category": "치수",
"data_type": "numeric",
"unit": "mm",
"spec_min": 11.9,
"spec_max": 12.1,
"warning_min": 11.95,
"warning_max": 12.05,
},
{
"name": "표면 조도 (Ra)",
"category": "표면",
"data_type": "numeric",
"unit": "μm",
"spec_min": 0,
"spec_max": 0.8,
"warning_max": 0.6,
},
{"name": "외관 결함 유무", "category": "외관", "data_type": "boolean"},
{
"name": "결함 유형",
"category": "외관",
"data_type": "select",
"select_options": [
"없음",
"스크래치",
"찍힘",
"필름짤림",
"오염",
"기타",
],
"is_required": False,
},
{
"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()
install_date = NOW - timedelta(days=365 * 2)
db.add(
Machine(
id=machine_id,
tenant_id=tenant_id,
name=m["name"],
equipment_code=m["equipment_code"],
model=m["model"],
manufacturer=m.get("manufacturer"),
installation_date=install_date,
location=m.get("location"),
area=m.get("area"),
criticality=m.get("criticality", "major"),
rated_capacity=m.get("rated_capacity"),
power_rating=m.get("power_rating"),
description=m.get("description"),
)
)
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())