1220 lines
39 KiB
Python
1220 lines
39 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)
|
|
|
|
|
|
# ─── Spifox 설비 생성 (이천공장 304대 — Digital Twin DB 기준) ───
|
|
|
|
# CASE프레스 번호 목록 (P-1xx ~ P-9xx, 총 215대)
|
|
_CASE_NUMBERS = (
|
|
list(range(101, 132)) # P-101 ~ P-131 (31)
|
|
+ list(range(201, 233)) # P-201 ~ P-232 (32)
|
|
+ list(range(301, 333)) # P-301 ~ P-332 (32)
|
|
+ list(range(401, 433)) # P-401 ~ P-432 (32)
|
|
+ list(range(501, 519)) # P-501 ~ P-518 (18)
|
|
+ list(range(601, 633)) # P-601 ~ P-632 (32)
|
|
+ list(range(701, 713)) # P-701 ~ P-712 (12)
|
|
+ list(range(801, 821)) # P-801 ~ P-820 (20)
|
|
+ [903, 904, 905, 906, 908, 910] # (6)
|
|
)
|
|
|
|
SPIFOX_MACHINES: list[dict] = []
|
|
|
|
# ── CASE프레스 215대 ──
|
|
for num in _CASE_NUMBERS:
|
|
m: dict = {
|
|
"name": f"CASE프레스 P-{num}",
|
|
"equipment_code": f"SF-CASE-P{num}",
|
|
"model": "Finish Press 80T",
|
|
"manufacturer": "SpiFox",
|
|
"location": "이천공장",
|
|
"area": "CASE프레스",
|
|
"criticality": "critical",
|
|
"rated_capacity": "80T",
|
|
"power_rating": "18.5kW",
|
|
}
|
|
if num == 101:
|
|
m["description"] = "2차 딥드로잉 성형. 콘덴서 케이스 최종 형상."
|
|
SPIFOX_MACHINES.append(m)
|
|
|
|
# ── CUP프레스 32대 ──
|
|
for i in range(1, 33):
|
|
m = {
|
|
"name": f"CUP프레스 {i}호기",
|
|
"equipment_code": f"SF-CUP-{i:03d}",
|
|
"model": "Cup Making Press 60T",
|
|
"manufacturer": "SpiFox",
|
|
"location": "이천공장",
|
|
"area": "CUP프레스",
|
|
"criticality": "critical",
|
|
"rated_capacity": "60T, UPH 520pcs",
|
|
"power_rating": "15kW",
|
|
}
|
|
if i == 1:
|
|
m["description"] = "알루미늄 슬러그 → CUP 형상 1차 딥드로잉 성형."
|
|
SPIFOX_MACHINES.append(m)
|
|
|
|
# ── 컵핑 33대 (컵핑1~32호기 + 컵핑기 003호기) ──
|
|
for i in range(1, 33):
|
|
SPIFOX_MACHINES.append(
|
|
{
|
|
"name": f"컵핑{i}호기",
|
|
"equipment_code": f"SF-CPN-{i:03d}",
|
|
"model": "Cup Forming Press",
|
|
"manufacturer": "SpiFox",
|
|
"location": "이천공장",
|
|
"area": "컵핑",
|
|
"criticality": "critical",
|
|
"rated_capacity": "컵핑 성형",
|
|
"power_rating": "15kW",
|
|
}
|
|
)
|
|
SPIFOX_MACHINES.append(
|
|
{
|
|
"name": "컵핑기 003호기",
|
|
"equipment_code": "SF-CPN-033",
|
|
"model": "Cup Forming Press",
|
|
"manufacturer": "SpiFox",
|
|
"location": "이천공장",
|
|
"area": "컵핑",
|
|
"criticality": "critical",
|
|
"rated_capacity": "컵핑 성형",
|
|
"power_rating": "15kW",
|
|
}
|
|
)
|
|
|
|
# ── CUP건조설비 8대 ──
|
|
for i in range(1, 9):
|
|
m = {
|
|
"name": f"CUP건조설비 {i}호기",
|
|
"equipment_code": f"SF-DRY-{i:03d}",
|
|
"model": "Hot Air Dryer 300",
|
|
"manufacturer": "대한열기",
|
|
"location": "이천공장",
|
|
"area": "CUP건조설비",
|
|
"criticality": "major",
|
|
"rated_capacity": "반제품 건조 300kg/batch",
|
|
"power_rating": "25kW",
|
|
}
|
|
if i == 1:
|
|
m["description"] = "반제품 CUP 건조. 열풍 순환 방식."
|
|
SPIFOX_MACHINES.append(m)
|
|
|
|
# ── 건조 8대 ──
|
|
for i in range(1, 9):
|
|
SPIFOX_MACHINES.append(
|
|
{
|
|
"name": f"건조{i}호기",
|
|
"equipment_code": f"SF-DRB-{i:03d}",
|
|
"model": "Industrial Dryer",
|
|
"manufacturer": "대한열기",
|
|
"location": "이천공장",
|
|
"area": "건조",
|
|
"criticality": "major",
|
|
"rated_capacity": "건조 300kg/batch",
|
|
"power_rating": "25kW",
|
|
}
|
|
)
|
|
|
|
# ── EMS 6대 ──
|
|
for i in range(1, 7):
|
|
m = {
|
|
"name": f"EMS {i}호기",
|
|
"equipment_code": f"SF-EMS-{i:03d}",
|
|
"model": "Electrified Monorail System",
|
|
"manufacturer": "물류자동화",
|
|
"location": "이천공장",
|
|
"area": "EMS",
|
|
"criticality": "major",
|
|
"rated_capacity": "천장주행 모노레일",
|
|
"power_rating": "5.5kW",
|
|
}
|
|
if i == 1:
|
|
m["description"] = "반제품 공정 간 천장주행 이송 시스템(EMS)."
|
|
SPIFOX_MACHINES.append(m)
|
|
|
|
# ── 크레인 2대 ──
|
|
SPIFOX_MACHINES.append(
|
|
{
|
|
"name": "CUP공급크레인",
|
|
"equipment_code": "SF-CRN-001",
|
|
"model": "Auto Warehouse Crane",
|
|
"manufacturer": "물류자동화",
|
|
"location": "이천공장",
|
|
"area": "크레인",
|
|
"criticality": "major",
|
|
"rated_capacity": "자동창고 스태커 크레인",
|
|
"power_rating": "11kW",
|
|
"description": "CUP 반제품 자동창고 입고 크레인.",
|
|
}
|
|
)
|
|
SPIFOX_MACHINES.append(
|
|
{
|
|
"name": "CUP출고크레인",
|
|
"equipment_code": "SF-CRN-002",
|
|
"model": "Auto Warehouse Crane",
|
|
"manufacturer": "물류자동화",
|
|
"location": "이천공장",
|
|
"area": "크레인",
|
|
"criticality": "major",
|
|
"rated_capacity": "자동창고 스태커 크레인",
|
|
"power_rating": "11kW",
|
|
"description": "CUP 반제품 자동창고 출고 크레인.",
|
|
}
|
|
)
|
|
|
|
assert len(SPIFOX_MACHINES) == 304, (
|
|
f"Expected 304 spifox machines, got {len(SPIFOX_MACHINES)}"
|
|
)
|
|
|
|
MACHINES = {
|
|
"spifox": SPIFOX_MACHINES,
|
|
"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": {
|
|
"CUP프레스 1호기": [
|
|
(
|
|
"컷트핀 (Cut Pin)",
|
|
"CP-CUP-01",
|
|
"금형",
|
|
"count",
|
|
150000,
|
|
80,
|
|
"manual",
|
|
128000,
|
|
),
|
|
(
|
|
"컷트메스 (Cut Die)",
|
|
"CD-CUP-01",
|
|
"금형",
|
|
"count",
|
|
200000,
|
|
85,
|
|
"manual",
|
|
165000,
|
|
),
|
|
(
|
|
"정형메스 (Forming Die)",
|
|
"FD-CUP-01",
|
|
"금형",
|
|
"count",
|
|
300000,
|
|
80,
|
|
"manual",
|
|
210000,
|
|
),
|
|
],
|
|
"CASE프레스 P-101": [
|
|
(
|
|
"컷트핀 (Cut Pin)",
|
|
"CP-FIN-01",
|
|
"금형",
|
|
"count",
|
|
120000,
|
|
80,
|
|
"manual",
|
|
98000,
|
|
),
|
|
(
|
|
"컷트메스 (Cut Die)",
|
|
"CD-FIN-01",
|
|
"금형",
|
|
"count",
|
|
150000,
|
|
85,
|
|
"manual",
|
|
112000,
|
|
),
|
|
(
|
|
"정형메스 (Forming Die)",
|
|
"FD-FIN-01",
|
|
"금형",
|
|
"count",
|
|
100000,
|
|
80,
|
|
"manual",
|
|
87000,
|
|
),
|
|
],
|
|
"CASE프레스 P-102": [
|
|
(
|
|
"컷트핀 (Cut Pin)",
|
|
"CP-FIN-02",
|
|
"금형",
|
|
"count",
|
|
120000,
|
|
80,
|
|
"manual",
|
|
73000,
|
|
),
|
|
(
|
|
"컷트메스 (Cut Die)",
|
|
"CD-FIN-02",
|
|
"금형",
|
|
"count",
|
|
150000,
|
|
85,
|
|
"manual",
|
|
95000,
|
|
),
|
|
(
|
|
"정형메스 (Forming Die)",
|
|
"FD-FIN-02",
|
|
"금형",
|
|
"count",
|
|
100000,
|
|
80,
|
|
"manual",
|
|
62000,
|
|
),
|
|
],
|
|
"CASE프레스 P-103": [
|
|
(
|
|
"컷트핀 (Cut Pin)",
|
|
"CP-FIN-03",
|
|
"금형",
|
|
"count",
|
|
120000,
|
|
80,
|
|
"manual",
|
|
52000,
|
|
),
|
|
(
|
|
"컷트메스 (Cut Die)",
|
|
"CD-FIN-03",
|
|
"금형",
|
|
"count",
|
|
150000,
|
|
85,
|
|
"manual",
|
|
68000,
|
|
),
|
|
(
|
|
"정형메스 (Forming Die)",
|
|
"FD-FIN-03",
|
|
"금형",
|
|
"count",
|
|
100000,
|
|
80,
|
|
"manual",
|
|
45000,
|
|
),
|
|
],
|
|
"CUP건조설비 1호기": [
|
|
(
|
|
"히터 엘리먼트",
|
|
"HE-DRY-01",
|
|
"핵심부품",
|
|
"hours",
|
|
8000,
|
|
85,
|
|
"auto_time",
|
|
6500,
|
|
),
|
|
(
|
|
"순환팬 베어링",
|
|
"FB-DRY-01",
|
|
"소모품",
|
|
"hours",
|
|
15000,
|
|
80,
|
|
"auto_time",
|
|
11000,
|
|
),
|
|
],
|
|
},
|
|
"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": "CASE프레스 일일 점검",
|
|
"subject_type": "equipment",
|
|
"machine_name": "CASE프레스 P-101",
|
|
"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": "CUP프레스 일일 점검",
|
|
"subject_type": "equipment",
|
|
"machine_name": "CUP프레스 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": "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())
|