feat: add equipment area grouping, criticality, and batch inspection
All checks were successful
Deploy to Production / deploy (push) Successful in 1m7s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m7s
- Add area and criticality fields to Machine model with DB migration - Add batch inspection endpoint (POST /inspections/batch) for area-wide inspection creation - Rewrite MachineList with area grouping, criticality badges, and batch inspect button - Update machine create/edit forms with area and criticality fields - Update seed data with area/criticality values for all tenants
This commit is contained in:
@@ -14,6 +14,7 @@ from src.database.models import (
|
||||
InspectionRecord,
|
||||
InspectionTemplate,
|
||||
InspectionTemplateItem,
|
||||
Machine,
|
||||
)
|
||||
from src.auth.models import TokenData
|
||||
from src.auth.dependencies import require_auth, verify_tenant_access
|
||||
@@ -28,6 +29,11 @@ class InspectionCreate(BaseModel):
|
||||
template_id: str
|
||||
|
||||
|
||||
class BatchInspectionCreate(BaseModel):
|
||||
machine_ids: Optional[List[str]] = None
|
||||
area: Optional[str] = None
|
||||
|
||||
|
||||
class RecordInput(BaseModel):
|
||||
template_item_id: str
|
||||
value_numeric: Optional[float] = None
|
||||
@@ -530,6 +536,103 @@ async def complete_inspection(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/batch")
|
||||
async def batch_create_inspections(
|
||||
body: BatchInspectionCreate,
|
||||
tenant_id: str = Path(...),
|
||||
current_user: TokenData = Depends(require_auth),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
verify_tenant_access(tenant_id, current_user)
|
||||
|
||||
if not body.machine_ids and not body.area:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="machine_ids 또는 area 중 하나를 지정해야 합니다.",
|
||||
)
|
||||
|
||||
machine_stmt = select(Machine).where(Machine.tenant_id == tenant_id)
|
||||
if body.machine_ids:
|
||||
machine_stmt = machine_stmt.where(
|
||||
Machine.id.in_([UUID(mid) for mid in body.machine_ids])
|
||||
)
|
||||
if body.area:
|
||||
machine_stmt = machine_stmt.where(Machine.area == body.area)
|
||||
|
||||
machines_result = await db.execute(machine_stmt)
|
||||
machines = machines_result.scalars().all()
|
||||
|
||||
if not machines:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="해당하는 설비를 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
machine_ids_set = {m.id for m in machines}
|
||||
|
||||
templates_stmt = (
|
||||
select(InspectionTemplate)
|
||||
.options(selectinload(InspectionTemplate.items))
|
||||
.where(
|
||||
InspectionTemplate.tenant_id == tenant_id,
|
||||
InspectionTemplate.is_active == True,
|
||||
InspectionTemplate.subject_type == "equipment",
|
||||
InspectionTemplate.machine_id.in_(machine_ids_set),
|
||||
)
|
||||
)
|
||||
templates_result = await db.execute(templates_stmt)
|
||||
templates = templates_result.scalars().all()
|
||||
|
||||
if not templates:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="해당 설비에 연결된 활성 검사 템플릿이 없습니다.",
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
created_sessions = []
|
||||
|
||||
for template in templates:
|
||||
if not template.items:
|
||||
continue
|
||||
|
||||
session = InspectionSession(
|
||||
tenant_id=tenant_id,
|
||||
template_id=template.id,
|
||||
inspector_id=UUID(current_user.user_id),
|
||||
status="in_progress",
|
||||
started_at=now,
|
||||
)
|
||||
db.add(session)
|
||||
await db.flush()
|
||||
|
||||
for item in template.items:
|
||||
record = InspectionRecord(
|
||||
session_id=session.id,
|
||||
template_item_id=item.id,
|
||||
)
|
||||
db.add(record)
|
||||
|
||||
created_sessions.append(
|
||||
{
|
||||
"id": str(session.id),
|
||||
"template_id": str(template.id),
|
||||
"template_name": str(template.name),
|
||||
"machine_id": str(template.machine_id) if template.machine_id else None,
|
||||
"items_count": len(template.items),
|
||||
}
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"{len(created_sessions)}건의 검사가 생성되었습니다.",
|
||||
"created_count": len(created_sessions),
|
||||
"sessions": created_sessions,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{inspection_id}")
|
||||
async def delete_inspection(
|
||||
tenant_id: str = Path(...),
|
||||
|
||||
@@ -21,6 +21,9 @@ from src.auth.dependencies import require_auth, verify_tenant_access
|
||||
router = APIRouter(prefix="/api/{tenant_id}/machines", tags=["machines"])
|
||||
|
||||
|
||||
VALID_CRITICALITIES = ("critical", "major", "minor")
|
||||
|
||||
|
||||
class MachineCreate(BaseModel):
|
||||
name: str
|
||||
equipment_code: str = ""
|
||||
@@ -28,6 +31,8 @@ class MachineCreate(BaseModel):
|
||||
manufacturer: Optional[str] = None
|
||||
installation_date: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
area: Optional[str] = None
|
||||
criticality: str = "major"
|
||||
rated_capacity: Optional[str] = None
|
||||
power_rating: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
@@ -40,6 +45,8 @@ class MachineUpdate(BaseModel):
|
||||
manufacturer: Optional[str] = None
|
||||
installation_date: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
area: Optional[str] = None
|
||||
criticality: Optional[str] = None
|
||||
rated_capacity: Optional[str] = None
|
||||
power_rating: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
@@ -54,6 +61,8 @@ class MachineResponse(BaseModel):
|
||||
manufacturer: Optional[str] = None
|
||||
installation_date: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
area: Optional[str] = None
|
||||
criticality: Optional[str] = None
|
||||
rated_capacity: Optional[str] = None
|
||||
power_rating: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
@@ -84,6 +93,8 @@ def _machine_to_response(m: Machine, parts_count: int = 0) -> MachineResponse:
|
||||
manufacturer=str(m.manufacturer) if m.manufacturer else None,
|
||||
installation_date=_format_ts(m.installation_date),
|
||||
location=str(m.location) if m.location else None,
|
||||
area=str(m.area) if m.area else None,
|
||||
criticality=str(m.criticality) if m.criticality else "major",
|
||||
rated_capacity=str(m.rated_capacity) if m.rated_capacity else None,
|
||||
power_rating=str(m.power_rating) if m.power_rating else None,
|
||||
description=str(m.description) if m.description else None,
|
||||
@@ -136,6 +147,12 @@ async def create_machine(
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if body.criticality not in VALID_CRITICALITIES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"criticality는 {', '.join(VALID_CRITICALITIES)} 중 하나여야 합니다.",
|
||||
)
|
||||
|
||||
machine = Machine(
|
||||
tenant_id=tenant_id,
|
||||
name=body.name,
|
||||
@@ -144,6 +161,8 @@ async def create_machine(
|
||||
manufacturer=body.manufacturer,
|
||||
installation_date=install_dt,
|
||||
location=body.location,
|
||||
area=body.area,
|
||||
criticality=body.criticality,
|
||||
rated_capacity=body.rated_capacity,
|
||||
power_rating=body.power_rating,
|
||||
description=body.description,
|
||||
@@ -202,6 +221,8 @@ async def get_machine(
|
||||
manufacturer=str(machine.manufacturer) if machine.manufacturer else None,
|
||||
installation_date=_format_ts(machine.installation_date),
|
||||
location=str(machine.location) if machine.location else None,
|
||||
area=str(machine.area) if machine.area else None,
|
||||
criticality=str(machine.criticality) if machine.criticality else "major",
|
||||
rated_capacity=str(machine.rated_capacity) if machine.rated_capacity else None,
|
||||
power_rating=str(machine.power_rating) if machine.power_rating else None,
|
||||
description=str(machine.description) if machine.description else None,
|
||||
@@ -251,6 +272,15 @@ async def update_machine(
|
||||
pass
|
||||
if body.location is not None:
|
||||
machine.location = body.location
|
||||
if body.area is not None:
|
||||
machine.area = body.area
|
||||
if body.criticality is not None:
|
||||
if body.criticality not in VALID_CRITICALITIES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"criticality는 {', '.join(VALID_CRITICALITIES)} 중 하나여야 합니다.",
|
||||
)
|
||||
machine.criticality = body.criticality
|
||||
if body.rated_capacity is not None:
|
||||
machine.rated_capacity = body.rated_capacity
|
||||
if body.power_rating is not None:
|
||||
|
||||
@@ -65,6 +65,8 @@ class Machine(Base):
|
||||
manufacturer = Column(String(100), nullable=True)
|
||||
installation_date = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
location = Column(String(200), nullable=True)
|
||||
area = Column(String(50), nullable=True) # 구역 그룹핑: Bay 3, A라인 등
|
||||
criticality = Column(String(20), default="major") # critical | major | minor
|
||||
rated_capacity = Column(String(100), nullable=True)
|
||||
power_rating = Column(String(100), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
@@ -76,7 +78,11 @@ class Machine(Base):
|
||||
"EquipmentPart", back_populates="machine", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
__table_args__ = (Index("ix_machines_tenant_id", "tenant_id"),)
|
||||
__table_args__ = (
|
||||
Index("ix_machines_tenant_id", "tenant_id"),
|
||||
Index("ix_machines_tenant_area", "tenant_id", "area"),
|
||||
Index("ix_machines_tenant_criticality", "tenant_id", "criticality"),
|
||||
)
|
||||
|
||||
|
||||
class EquipmentPart(Base):
|
||||
|
||||
Reference in New Issue
Block a user