Multi-tenant factory inspection system (SpiFox, Enkid, Alpet): - FastAPI backend with JWT auth, PostgreSQL (asyncpg) - Next.js 16 frontend with App Router, SWR data fetching - Machines CRUD with equipment parts management - Part lifecycle tracking (hours/count/date) with counters - Partial unique index for soft-delete support - 24 pytest tests passing, E2E verified Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
137 KiB
FactoryOps v2 — 개발 계획서 (Complete Plan)
작성일: 2026-02-09 (최종 업데이트: 2026-02-10) 기반 자료: 개발회의.md (멘토 회의), spifox-inspection-analysis.md (아키텍처 분석), integrated-analysis.md (3사 통합분석), Oracle 컨설팅 대상 고객사: 스피폭스(SpiFox), 엔키드(Enkid), 알펫(Alpet) ⚠️ 새 프로젝트 — 기존 factoryOps 코드베이스는 참고만 하며, 필요한 코드만 발췌하여 새로 작성합니다. ⚠️ PostgreSQL 전용 — SQLite fallback 없음. 테스트 DB도 PostgreSQL. JSONB, Array 등 PostgreSQL 네이티브 기능 적극 활용.
핵심 설계 원칙 (2026-02-10 추가)
"TYPE 필드로 분기하고, 테넌트 ID로는 절대 분기하지 않는다." — Oracle Architecture Consultation
- 업체별 차이는
lifecycle_type,data_type,inspection_mode,alarm_type등 엔티티의 TYPE 필드에서 결정 - 코어 앱에
if tenant == "spifox"같은 분기 = 0개 - 상세 분석:
planning/integrated-analysis.md참조
0. 프로젝트 셋업 절차
디렉토리 전환
mv /Users/johngreen/Dev/factoryOps /Users/johngreen/Dev/factoryops_backup
mkdir /Users/johngreen/Dev/factoryOps
cd /Users/johngreen/Dev/factoryOps
git init
새 프로젝트 디렉토리 구조
factoryOps/
├── main.py # FastAPI 앱 엔트리포인트 (최소화, 목표 100줄 이내)
├── requirements.txt # Python 의존성 (경량화)
├── alembic.ini # DB 마이그레이션 설정
├── alembic/
│ ├── env.py
│ └── versions/ # 마이그레이션 파일
├── src/
│ ├── __init__.py
│ ├── auth/ # ✨ 기존 참고하여 새로 작성
│ │ ├── __init__.py
│ │ ├── jwt_handler.py # JWT 토큰 생성/검증 (HS256, 24h 만료)
│ │ ├── password.py # bcrypt 해싱
│ │ ├── dependencies.py # FastAPI auth 의존성 (get_current_user, require_auth 등)
│ │ ├── models.py # Pydantic auth 모델 (User, Token, TokenData 등)
│ │ ├── service.py # 인증 비즈니스 로직 (login, create_user 등)
│ │ └── router.py # 인증 API 엔드포인트 (/api/auth/login, /me 등)
│ ├── database/
│ │ ├── __init__.py
│ │ ├── config.py # ✨ 새로 작성 (PostgreSQL 전용, asyncpg + SQLAlchemy async)
│ │ └── models.py # ✨ 새로 작성 (아래 스키마 참조)
│ ├── tenant/
│ │ ├── __init__.py
│ │ └── manager.py # ✨ 기존 참고하여 새로 작성 (간소화)
│ ├── api/ # ✨ 모두 새로 작성
│ │ ├── __init__.py
│ │ ├── machines.py # 설비 CRUD
│ │ ├── equipment_parts.py # 설비 부품 관리
│ │ ├── templates.py # 검사 템플릿 CRUD
│ │ ├── sessions.py # 검사 세션 (실행 + 자동저장)
│ │ ├── counters.py # 부품 카운터 + 교체
│ │ └── alarms.py # 알람 조회/확인
│ └── services/ # ✨ 비즈니스 로직 분리
│ ├── __init__.py
│ ├── counter_service.py # 카운터 갱신/리셋 로직 (optimistic locking)
│ └── alarm_service.py # 알람 발생/조회 로직
├── tests/
│ ├── conftest.py # pytest fixtures (PostgreSQL 테스트 DB, test client, test user)
│ ├── test_auth.py
│ ├── test_machines.py
│ ├── test_templates.py
│ ├── test_sessions.py
│ ├── test_counters.py
│ └── test_alarms.py
├── dashboard/ # Next.js 프론트엔드
│ ├── app/
│ │ ├── layout.tsx # ✨ 기존 참고하여 새로 작성 (Providers 래핑)
│ │ ├── page.tsx # 테넌트 선택 (superadmin) / 리다이렉트
│ │ ├── login/page.tsx # ✨ 기존 참고하여 새로 작성
│ │ ├── globals.css # ✨ 기존 참고하여 새로 작성 (Material Design 3 CSS 변수)
│ │ └── [tenant]/
│ │ ├── layout.tsx # 테넌트 레이아웃 (TopNav + AuthGuard)
│ │ ├── page.tsx # 대시보드 (설비 목록 + 부품 현황 + 교체 임박)
│ │ ├── machines/
│ │ │ └── [id]/page.tsx # 설비 상세 (부품 탭, 카운터 탭, 교체 이력)
│ │ ├── templates/
│ │ │ ├── page.tsx # 검사 템플릿 목록 (설비/품질 탭)
│ │ │ ├── new/page.tsx # 템플릿 생성
│ │ │ └── [id]/page.tsx # 템플릿 편집
│ │ ├── inspections/
│ │ │ ├── page.tsx # 검사 세션 목록 (진행중/완료 탭)
│ │ │ └── [id]/page.tsx # 검사 실행 화면 (자동저장)
│ │ ├── alarms/
│ │ │ └── page.tsx # 알람 목록 (필터: 타입/심각도/확인여부)
│ │ └── settings/
│ │ └── page.tsx # 테넌트 설정
│ ├── components/ # ✨ 새로 작성 (기존 패턴 참고)
│ │ ├── TopNav.tsx # ✨ 새로 작성 (설비/검사/알람 메뉴)
│ │ ├── AuthGuard.tsx # ✨ 기존 참고하여 새로 작성
│ │ ├── Toast.tsx # ✨ 기존 참고하여 새로 작성
│ │ ├── Card.tsx # ✨ 기존 참고하여 새로 작성
│ │ ├── MachineList.tsx # 설비 목록 (간소화 재작성)
│ │ ├── PartLifecycleGauge.tsx # 부품 수명 게이지 (원형/바, 새로)
│ │ ├── InspectionForm.tsx # 검사 실행 폼 (핵심, 새로, 자동저장)
│ │ ├── TemplateEditor.tsx # 템플릿 항목 편집기 (새로)
│ │ ├── AlarmBadge.tsx # 알람 뱃지 (새로)
│ │ └── CounterCard.tsx # 카운터 현황 카드 (새로)
│ ├── lib/
│ │ ├── api.ts # ✨ 기존 참고하여 새로 작성 (fetcher, api.get/post/put/delete)
│ │ ├── auth-context.tsx # ✨ 기존 참고하여 새로 작성 (JWT localStorage 관리)
│ │ ├── tenant-context.tsx # ✨ 기존 참고하여 새로 작성 (URL에서 tenant_id 추출)
│ │ ├── toast-context.tsx # ✨ 기존 참고하여 새로 작성
│ │ ├── hooks.ts # ✨ 새 도메인 훅 (useData 패턴 유지)
│ │ └── types.ts # ✨ 새 타입 정의
│ ├── package.json
│ ├── next.config.ts
│ └── tsconfig.json
├── planning/ # 작업 메모리
│ ├── task_plan.md
│ ├── findings.md
│ └── progress.md
├── CLAUDE.md # 프로젝트 가이드
└── .env.example # 환경변수 템플릿
1. 데이터베이스 스키마 (전체)
Note
: PostgreSQL 전용 — JSONB (인덱싱, 쿼리 가능), UUID 네이티브 타입, TIMESTAMP WITH TIME ZONE, GIN 인덱스 활용 SQLite 호환성 고려 불필요. PostgreSQL 전용 기능을 적극 사용합니다.
기본 테이블 (3개) — 새로 작성
-- 사용자
User
id UUID PK DEFAULT gen_random_uuid()
email VARCHAR(255) UNIQUE NOT NULL
password_hash VARCHAR(255) NOT NULL
name VARCHAR(100) NOT NULL
role VARCHAR(20) NOT NULL -- 'superadmin' | 'tenant_admin' | 'user'
tenant_id VARCHAR(50) NULL
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
-- 테넌트
Tenant
id VARCHAR(50) PK -- 예: 'spifox'
name VARCHAR(100) NOT NULL
industry_type VARCHAR(50) DEFAULT 'general'
is_active BOOLEAN DEFAULT true
enabled_modules JSONB NULL -- {"inspection": true, ...}
workflow_config JSONB NULL -- GIN 인덱스 사용 가능
created_at TIMESTAMPTZ DEFAULT now()
-- 설비
Machine
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
name VARCHAR(100) NOT NULL -- "1호기", "프레스 A-01"
equipment_code VARCHAR(50) DEFAULT '' -- 설비코드
model VARCHAR(100) NULL -- 모델명
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
INDEX(tenant_id)
도메인 테이블 (7개) — 새로 작성
-- 1. 설비 장착 부품 (EquipmentPart)
-- 재고 부품이 아닌, 설비에 실제 장착된 부품 인스턴스
-- 각 부품은 수명 관리 방식(시간/타발수/날짜)과 한계값을 가짐
EquipmentPart
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
machine_id UUID FK → machines.id NOT NULL
name VARCHAR(100) NOT NULL -- "상부 금형", "하부 금형", "플런저 팁"
part_number VARCHAR(50) NULL -- 부품번호 (optional, 재고 연결용)
category VARCHAR(50) NULL -- "mold", "tip", "sensor", "cylinder", "chemical", "heater"
lifecycle_type VARCHAR(20) NOT NULL -- "hours" | "count" | "date"
lifecycle_limit FLOAT NOT NULL -- 수명 한계값 (예: 10000타, 5000시간, 30일)
alarm_threshold FLOAT DEFAULT 90.0 -- 알람 시작 % (예: 90.0 = 90%에 도달하면 알람)
counter_source VARCHAR(20) DEFAULT 'manual' -- ✨ 신규: "auto_plc" | "auto_time" | "manual"
-- auto_plc: PLC에서 자동 갱신 (스피폭스 shotno, 엔키드 shots)
-- auto_time: 장착일 기준 경과시간 자동 계산 (알펫 히터, 약품)
-- manual: 사용자가 직접 입력
installed_at TIMESTAMPTZ NOT NULL -- 현재 부품 장착일
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
UNIQUE(tenant_id, machine_id, name)
INDEX(tenant_id)
INDEX(tenant_id, machine_id)
-- 2. 검사 템플릿 (InspectionTemplate)
-- 설비검사와 품질검사를 subject_type으로 구분하는 공용 템플릿
-- 하나의 엔진, 이중 대상 원칙 (선배 개발자 + Oracle 컨설팅 합의)
InspectionTemplate
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
name VARCHAR(200) NOT NULL -- "1호기 일일검사", "완제품 A-Type 검사"
subject_type VARCHAR(20) NOT NULL -- "equipment" | "product"
machine_id UUID FK → machines.id NULL -- subject_type='equipment'일 때
product_code VARCHAR(50) NULL -- subject_type='product'일 때 (미래 확장)
schedule_type VARCHAR(20) NOT NULL -- "daily"|"weekly"|"monthly"|"yearly"|"ad_hoc"
inspection_mode VARCHAR(20) DEFAULT 'measurement' -- ✨ 신규: "checklist" | "measurement" | "monitoring"
-- checklist: 빠른 OK/NG 체크 (스피폭스 순회검사)
-- measurement: 정밀 측정 폼 (엔키드 품질검사)
-- monitoring: 연속 모니터링 (알펫 라인 관측)
-- 📌 테넌트가 아닌 템플릿에 설정 → 같은 업체도 검사별로 다른 모드 사용 가능
version INTEGER DEFAULT 1 -- 버전 관리 (수정 시 증가)
is_active BOOLEAN DEFAULT true
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
INDEX(tenant_id, subject_type)
INDEX(tenant_id, machine_id)
-- 3. 검사 항목 정의 (InspectionTemplateItem)
-- 각 템플릿에 속하는 검사 항목들. 사용자가 자유롭게 추가/삭제 가능
-- data_type에 따라 입력 방식이 달라짐 (numeric, boolean, text, select)
InspectionTemplateItem
id UUID PK DEFAULT gen_random_uuid()
template_id UUID FK → inspection_templates.id ON DELETE CASCADE
sort_order INTEGER NOT NULL -- 표시 순서
name VARCHAR(200) NOT NULL -- "유압 압력", "금형 온도", "외관 상태"
category VARCHAR(100) NULL -- "안전", "작동", "품질", "청소"
data_type VARCHAR(20) NOT NULL -- "numeric"|"boolean"|"text"|"select"
unit VARCHAR(20) NULL -- "℃", "bar", "mm", "개" (numeric일 때)
spec_min FLOAT NULL -- 하한 스펙 (numeric일 때, NULL이면 체크 안 함)
spec_max FLOAT NULL -- 상한 스펙 (numeric일 때, NULL이면 체크 안 함)
warning_min FLOAT NULL -- ✨ 신규: 소프트 하한 (trend_warning용, 경고구간 시작)
warning_max FLOAT NULL -- ✨ 신규: 소프트 상한 (trend_warning용, 경고구간 시작)
trend_window INTEGER NULL -- ✨ 신규: 연속 N회 경고구간 진입 시 trend_warning 알람 (NULL=비활성)
select_options JSONB NULL -- data_type='select'일 때 ["양호","불량","N/A"]
equipment_part_id UUID FK → equipment_parts.id NULL -- 부품 연결 (optional)
is_required BOOLEAN DEFAULT true -- 필수 입력 여부
created_at TIMESTAMPTZ DEFAULT now()
INDEX(template_id, sort_order)
-- 4. 검사 세션 (InspectionSession)
-- 검사 실행 단위. "진행 중 저장"이 핵심 요구사항.
-- 세션 시작 시 template_snapshot에 템플릿+항목 전체를 JSON으로 스냅샷하여
-- 이후 템플릿이 수정되어도 기존 검사 이력이 보호됨 (버전 보호)
InspectionSession
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
template_id UUID FK → inspection_templates.id NOT NULL
template_snapshot JSONB NOT NULL -- 세션 시작 시 템플릿+항목 전체 스냅샷
status VARCHAR(20) NOT NULL DEFAULT 'in_progress'
-- "in_progress"|"completed"|"cancelled"
inspector_id UUID FK → users.id NOT NULL -- 검사자
started_at TIMESTAMPTZ NOT NULL
completed_at TIMESTAMPTZ NULL
results JSONB NULL -- 완료 시 최종 결과 (항목별 값 + 판정)
partial_results JSONB NULL -- 진행 중 자동저장 데이터
-- {item_id: {value: ..., updated_at: ...}}
lot_number VARCHAR(100) NULL -- ✨ 신규: 로트/시리얼 번호 (엔키드 IATF 추적성용, optional)
notes TEXT NULL -- 비고/메모
created_at TIMESTAMPTZ DEFAULT now()
updated_at TIMESTAMPTZ DEFAULT now()
INDEX(tenant_id, status)
INDEX(tenant_id, template_id)
INDEX(tenant_id, inspector_id, status)
-- 5. 부품 카운터 (PartCounter)
-- 설비별 부품별 1:1 mutable 레코드. 누적값을 추적하며 부품 교체 시 리셋.
-- version 필드로 optimistic locking (PLC 갱신과 수동 리셋 동시 발생 방지)
PartCounter
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
equipment_part_id UUID FK → equipment_parts.id UNIQUE NOT NULL
current_value FLOAT DEFAULT 0 -- 현재 누적값 (타발수, 시간 등)
lifecycle_pct FLOAT DEFAULT 0 -- 수명 진행률 (%) = current_value / lifecycle_limit * 100
last_reset_at TIMESTAMPTZ NOT NULL -- 마지막 리셋(교체) 시각
last_updated_at TIMESTAMPTZ NOT NULL -- 마지막 갱신 시각
version INTEGER DEFAULT 0 -- optimistic locking용 버전
INDEX(tenant_id)
INDEX(equipment_part_id)
-- 6. 부품 교체 이력 (PartReplacementLog)
-- 부품 교체 시점의 스냅샷을 보존. "언제, 누가, 누적값 얼마에서 교체했는가"
PartReplacementLog
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
equipment_part_id UUID FK → equipment_parts.id NOT NULL
replaced_by UUID FK → users.id NULL -- 교체한 사람
replaced_at TIMESTAMPTZ NOT NULL
counter_at_replacement FLOAT NOT NULL -- 교체 시점 카운터 값 (예: 9,850타)
lifecycle_pct_at_replacement FLOAT NOT NULL -- 교체 시점 수명 % (예: 98.5%)
reason VARCHAR(200) NULL -- 교체 사유 ("정기 교체", "마모", "파손" 등)
notes TEXT NULL
INDEX(tenant_id, equipment_part_id)
INDEX(tenant_id, replaced_at)
-- 7. 검사 알람 (InspectionAlarm)
-- 3가지 알람 유형:
-- part_lifecycle: 부품 수명 임계값 도달 (예: 90% 도달)
-- inspection_overdue: 정기검사 미실시 (예: 일일검사를 오늘 안 함)
-- spec_violation: 검사 결과 스펙 이탈 (예: 온도 65℃, 상한 60℃)
InspectionAlarm
id UUID PK DEFAULT gen_random_uuid()
tenant_id VARCHAR(50) FK → tenants.id NOT NULL
alarm_type VARCHAR(30) NOT NULL -- "part_lifecycle"|"inspection_overdue"|"spec_violation"|"trend_warning"
severity VARCHAR(10) NOT NULL -- "warning"|"critical"
machine_id UUID FK → machines.id NULL
equipment_part_id UUID FK → equipment_parts.id NULL
session_id UUID FK → inspection_sessions.id NULL
title VARCHAR(200) NOT NULL -- "1호기 상부금형 수명 92% 도달"
message TEXT NULL -- 상세 설명
is_acknowledged BOOLEAN DEFAULT false
acknowledged_by UUID FK → users.id NULL
acknowledged_at TIMESTAMPTZ NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
INDEX(tenant_id, is_acknowledged)
INDEX(tenant_id, alarm_type)
INDEX(tenant_id, created_at)
테이블 관계 다이어그램
Tenant (1) ──────┬──── (N) User
│
├──── (N) Machine
│ │
│ ├──── (N) EquipmentPart
│ │ │
│ │ ├──── (1) PartCounter
│ │ │
│ │ ├──── (N) PartReplacementLog
│ │ │
│ │ └──── (N) InspectionTemplateItem.equipment_part_id
│ │
│ └──── (N) InspectionTemplate.machine_id
│ │
│ ├──── (N) InspectionTemplateItem
│ │
│ └──── (N) InspectionSession
│
└──── (N) InspectionAlarm
2. Phase 분해
Phase 0: 프로젝트 부트스트래핑 (0.5일)
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| 0-1 | 기존 프로젝트 백업 | mv factoryOps factoryops_backup (기존 코드는 참고용으로 보존) |
infra | bash |
| 0-2 | 새 프로젝트 생성 | mkdir factoryOps && cd factoryOps && git init |
infra | bash |
| 0-3 | requirements.txt 작성 |
FastAPI==0.115.0, SQLAlchemy[asyncio]==2.0.36, asyncpg==0.30.0, PyJWT==2.9.0, bcrypt==4.2.0, alembic==1.14.0, uvicorn[standard], python-multipart, pytest==8.2.2, pytest-asyncio, httpx | infra | write |
| 0-4 | Next.js 프로젝트 초기화 | npx create-next-app@latest dashboard --typescript --app --no-tailwind 후 npm install swr clsx |
infra | bash |
| 0-5 | next.config.ts 설정 |
output: 'standalone', 기존 참고하여 작성 |
frontend | write |
| 0-6 | tsconfig.json 설정 |
@/* path alias, strict mode, 기존 참고하여 작성 |
frontend | write |
| 0-7 | .env.example 작성 |
DATABASE_URL (postgresql+asyncpg://...), TEST_DATABASE_URL, JWT_SECRET_KEY, SQL_ECHO, NEXT_PUBLIC_API_URL | infra | write |
| 0-8 | CLAUDE.md 작성 |
새 프로젝트 기술스택, 구조, 스킬 사용법 가이드 | docs | write |
| 0-9 | planning/ 디렉토리 생성 |
task_plan.md, findings.md, progress.md 초기화 | docs | write |
의존성: 없음 (최초 작업) 병렬 가능: 0-3(Python)과 0-4(Node.js)는 0-2 이후 병렬 가능 예상 소요: 0.5일
Phase 1: 인증 + DB 기반 (1.5일)
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| Backend | ||||
| 1-1 | src/database/config.py 작성 |
PostgreSQL 전용 (asyncpg). AsyncSession, async get_db(), async init_db(). SQLAlchemy async engine 사용 | backend | write |
| 1-2 | src/database/models.py 작성 |
User, Tenant, Machine 3개 테이블만 (목표 150줄). Base = declarative_base(). PostgreSQL UUID, TIMESTAMPTZ, JSONB 네이티브 타입 사용 | backend | write |
| 1-3 | src/auth/ 작성 |
기존 코드 참고하여 새로 작성. jwt_handler.py, password.py, dependencies.py, models.py, service.py, router.py,__init__.py. 핵심: login, logout, /me, create_user, list_users |
backend | write |
| 1-4 | (1-3에 통합) | — | — | — |
| 1-5 | src/tenant/manager.py 작성 |
기존 참고하여 간소화 작성. 핵심만 유지: get_tenant(), create_tenant(), list_tenants(), verify_tenant_access() | backend | write |
| 1-6 | main.py 작성 |
최소 FastAPI app. auth_router 등록, CORS middleware, init_db() on startup. 기존 2000줄 → 목표 60~80줄 | backend | write |
| 1-7 | Alembic 설정 | alembic init alembic, env.py에 models import + database URL 설정, 초기 마이그레이션 alembic revision --autogenerate -m "initial" |
backend | bash |
| 1-8 | tests/conftest.py 작성 |
PostgreSQL 테스트 DB (TEST_DATABASE_URL), async test SessionLocal, TestClient fixture (httpx.AsyncClient), create_test_user() fixture (superadmin + regular user), get_auth_headers() helper. 각 테스트 후 트랜잭션 롤백으로 격리 | backend | write |
| 1-9 | tests/test_auth.py 작성 |
로그인 성공/실패, 토큰 검증, /me 엔드포인트, 권한 체크 (superadmin vs user) | backend | write |
| Frontend | ||||
| 1-10 | 프론트엔드 기반 파일 작성 | 기존 참고하여 새로 작성:lib/api.ts, lib/auth-context.tsx, lib/tenant-context.tsx, lib/toast-context.tsx, app/layout.tsx, app/globals.css (Material Design 3), components/AuthGuard.tsx, components/Toast.tsx, components/Card.tsx |
frontend | write |
| 1-11 | app/login/page.tsx 작성 |
email/password 입력, POST /api/auth/login, 성공 시 리다이렉트. 기존 패턴 참고 | frontend | write |
| 1-12 | app/page.tsx 작성 |
테넌트 선택 페이지 (superadmin) 또는 자동 리다이렉트 (일반 사용자) | frontend | write |
의존성: Phase 0 완료 후
병렬 가능: 백엔드(1-11-9)와 프론트엔드(1-101-12) 완전 병렬
예상 소요: 1.5일
검증 기준:
pytest tests/test_auth.py전체 통과- 브라우저에서 로그인 → /me 응답 확인
/serve스킬로 서버 실행 가능
Phase 2: 설비 + 부품 관리 (2일)
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| Backend | ||||
| 2-1 | models.py에 EquipmentPart 모델 추가 |
위 스키마 정의 참조. Machine과 1:N 관계 설정. UNIQUE(tenant_id, machine_id, name) 제약 | backend | edit |
| 2-2 | src/api/machines.py — 설비 CRUD API |
backend | write | |
GET /api/{tenant_id}/machines |
테넌트 전체 설비 목록. 응답에 parts_count 포함 | |||
POST /api/{tenant_id}/machines |
설비 등록 (name, equipment_code, model) | |||
GET /api/{tenant_id}/machines/{id} |
설비 상세 + 장착 부품 목록 포함 | |||
PUT /api/{tenant_id}/machines/{id} |
설비 수정 | |||
DELETE /api/{tenant_id}/machines/{id} |
설비 삭제 (부품 있으면 soft delete 또는 거부) | |||
| 2-3 | src/api/equipment_parts.py — 부품 CRUD API |
backend | write | |
GET /api/{tenant_id}/machines/{machine_id}/parts |
설비의 전체 부품 목록 | |||
POST /api/{tenant_id}/machines/{machine_id}/parts |
부품 등록 (name, category, lifecycle_type, lifecycle_limit, alarm_threshold, installed_at). 동시에 PartCounter 레코드도 자동 생성 (current_value=0) | |||
GET /api/{tenant_id}/parts/{id} |
부품 상세 (카운터 포함) | |||
PUT /api/{tenant_id}/parts/{id} |
부품 정보 수정 | |||
DELETE /api/{tenant_id}/parts/{id} |
부품 삭제 (비활성화) | |||
| 2-4 | main.py에 machines, equipment_parts 라우터 등록 |
router include | backend | edit |
| 2-5 | Alembic 마이그레이션 생성 | alembic revision --autogenerate -m "add_equipment_part" |
backend | bash |
| 2-6 | tests/test_machines.py |
설비 CRUD 5개 엔드포인트 테스트, 테넌트 격리 확인 | backend | write |
| 2-7 | tests/test_equipment_parts.py |
부품 CRUD 테스트, 부품 등록 시 PartCounter 자동 생성 확인, 중복 이름 거부 확인 | backend | write |
| Frontend | ||||
| 2-8 | lib/types.ts 작성 |
Machine, EquipmentPart, PartCounter 타입 정의 | frontend | write |
| 2-9 | lib/hooks.ts 작성 |
useData<T>() 기본 훅 (기존 패턴 유지: SWR, 30s refresh, 2s dedup). useMachines(tenantId), useMachine(tenantId, machineId), useEquipmentParts(tenantId, machineId) 도메인 훅 |
frontend | write |
| 2-10 | components/TopNav.tsx 작성 |
새 프로젝트용 네비게이션 바. 메뉴: 대시보드, 설비, 검사 템플릿, 검사 실행, 알람. 사용자 정보 + 로그아웃. 알람 뱃지 자리(Phase 6에서 연결) | frontend | write |
| 2-11 | app/[tenant]/layout.tsx 작성 |
TopNav + AuthGuard 래핑, TenantProvider 설정 | frontend | write |
| 2-12 | app/[tenant]/page.tsx — 대시보드 |
설비 목록 카드 뷰 (설비명, 코드, 부품 수, 상태 요약). 클릭 시 설비 상세로 이동 | frontend | write |
| 2-13 | components/MachineList.tsx 작성 |
설비 목록 컴포넌트 (검색, 필터). 기존보다 간소화: 건강점수/등급 제거, 부품 수 + 알람 여부만 표시 | frontend | write |
| 2-14 | app/[tenant]/machines/[id]/page.tsx — 설비 상세 |
설비 기본 정보 + 부품 탭 (등록/수정/삭제). 부품별: 이름, 카테고리, 수명유형, 한계값, 장착일 표시. 부품 추가 모달 | frontend | write |
의존성: Phase 1 완료 후
병렬 가능: 백엔드(2-12-7)와 프론트엔드(2-82-14) 병렬. 프론트엔드는 타입 목업으로 먼저 개발 가능
예상 소요: 2일
검증 기준:
pytest tests/test_machines.py tests/test_equipment_parts.py전체 통과- 브라우저에서 설비 등록 → 부품 추가 → 목록 확인 플로우
Phase 3: 검사 템플릿 (기준정보) (2~3일)
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| Backend | ||||
| 3-1 | models.py에 InspectionTemplate + InspectionTemplateItem 추가 |
위 스키마 참조. Template ↔ Item 1:N cascade. Template.version 관리 로직 | backend | edit |
| 3-2 | src/api/templates.py — 템플릿 CRUD API |
backend | write | |
GET /api/{tenant_id}/templates |
목록 (쿼리: subject_type, schedule_type, machine_id 필터). 각 템플릿의 items_count 포함 | |||
POST /api/{tenant_id}/templates |
생성 (name, subject_type, machine_id, schedule_type, items:[...]). items 배열을 함께 받아 일괄 생성 | |||
GET /api/{tenant_id}/templates/{id} |
상세 (items 목록 sort_order 순 포함) | |||
PUT /api/{tenant_id}/templates/{id} |
수정 (name, schedule_type 등). version 자동 증가. 이미 이 템플릿으로 진행된 세션이 있으면 경고 메시지 반환 | |||
DELETE /api/{tenant_id}/templates/{id} |
비활성화 (is_active=false). 물리 삭제 아님 | |||
POST /api/{tenant_id}/templates/{id}/items |
항목 추가 (name, category, data_type, unit, spec_min, spec_max, select_options, equipment_part_id, is_required). sort_order 자동 계산 (현재 max + 1) | |||
PUT /api/{tenant_id}/templates/{id}/items/{item_id} |
항목 수정 | |||
DELETE /api/{tenant_id}/templates/{id}/items/{item_id} |
항목 삭제 + 나머지 sort_order 재정렬 | |||
PUT /api/{tenant_id}/templates/{id}/items/reorder |
항목 순서 변경. body: {item_ids: [id1, id2, id3, ...]} 순서대로 sort_order 재할당 | |||
| 3-3 | main.py에 templates 라우터 등록 |
backend | edit | |
| 3-4 | Alembic 마이그레이션 | alembic revision --autogenerate -m "add_inspection_template" |
backend | bash |
| 3-5 | tests/test_templates.py |
backend | write | |
| 템플릿 생성 (items 포함) + 조회 확인 | ||||
| 템플릿 수정 시 version 증가 확인 | ||||
| 항목 추가/수정/삭제/순서변경 테스트 | ||||
| subject_type 필터 테스트 | ||||
| equipment_part_id 연결 테스트 | ||||
| 비활성화(soft delete) 테스트 | ||||
| Frontend | ||||
| 3-6 | lib/types.ts에 추가 |
InspectionTemplate, InspectionTemplateItem 타입 정의 | frontend | edit |
| 3-7 | lib/hooks.ts에 추가 |
useTemplates(tenantId, filters?), useTemplate(tenantId, templateId) |
frontend | edit |
| 3-8 | app/[tenant]/templates/page.tsx — 템플릿 목록 |
탭: 설비검사 / 품질검사 (subject_type 필터). 카드 또는 테이블 뷰: 템플릿명, 대상 설비, 주기, 항목수, 버전. "새 템플릿" 버튼 | frontend | write |
| 3-9 | components/TemplateEditor.tsx — 핵심 편집기 |
frontend | write | |
| 템플릿 기본정보 입력: 이름, 대상유형(설비/품질), 설비 선택(드롭다운), 검사주기 | ||||
| 항목 목록: 드래그로 순서 변경 (또는 ↑↓ 버튼) | ||||
| 항목 추가 폼: 항목명, 카테고리, 데이터유형 선택 → 유형별 조건부 UI | ||||
| - numeric: 단위, 하한, 상한 입력 | ||||
| - boolean: 추가 설정 없음 | ||||
| - text: 추가 설정 없음 | ||||
| - select: 선택지 목록 편집 (추가/삭제) | ||||
| 부품 연결: equipment_part_id 선택 (해당 설비의 부품 드롭다운) | ||||
| 필수 여부 토글 | ||||
| 항목 삭제 (확인 다이얼로그) | ||||
| 3-10 | app/[tenant]/templates/new/page.tsx |
TemplateEditor를 사용한 생성 페이지. 저장 시 POST /templates → 목록으로 리다이렉트 | frontend | write |
| 3-11 | app/[tenant]/templates/[id]/page.tsx |
TemplateEditor를 사용한 편집 페이지. 기존 데이터 로드 → 수정 → PUT /templates/{id} | frontend | write |
의존성: Phase 2 완료 후 (Machine, EquipmentPart 필요) 병렬 가능: 백엔드와 프론트엔드 병렬. Phase 5와도 병렬 가능 (서로 의존 없음) 예상 소요: 2~3일 검증 기준:
pytest tests/test_templates.py전체 통과- 브라우저에서 템플릿 생성 → 항목 추가(각 데이터유형) → 순서변경 → 저장 → 재편집 플로우
Phase 4: 검사 실행 (세션) (3일) — 핵심 UX, 최고 우선순위
선배 개발자 경고: "진행 중인 검사가 초기화되면 시스템 사용률 90% 실패" 이 Phase가 전체 프로젝트의 성패를 좌우함
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| Backend | ||||
| 4-1 | models.py에 InspectionSession 추가 |
위 스키마 참조. template_snapshot JSON, partial_results JSON, status enum | backend | edit |
| 4-2 | src/api/sessions.py — 세션 API |
backend | write | |
POST /api/{tenant_id}/sessions |
세션 시작. body: {template_id}. 로직: (1) 템플릿+항목 조회, (2) JSON 스냅샷 생성, (3) InspectionSession 생성(status='in_progress', template_snapshot=스냅샷, started_at=now). 응답: 세션 ID + 스냅샷 | |||
GET /api/{tenant_id}/sessions |
목록. 쿼리: status(in_progress/completed/cancelled), template_id, inspector_id. 페이지네이션. 정렬: 최신 순 | |||
GET /api/{tenant_id}/sessions/{id} |
세션 상세. template_snapshot + partial_results(또는 results) 포함 | |||
PATCH /api/{tenant_id}/sessions/{id}/autosave |
자동저장. body: {partial_results: {item_id: {value: any, updated_at: str}}}. status가 'in_progress'일 때만 허용. 부분 업데이트(기존 partial_results에 merge) | |||
POST /api/{tenant_id}/sessions/{id}/complete |
세션 완료. 로직: (1) partial_results → results로 확정, (2) 필수 항목 미입력 체크, (3) 스펙 이탈 항목 체크 → 알람 생성(Phase 6), (4) status='completed', completed_at=now | |||
POST /api/{tenant_id}/sessions/{id}/cancel |
세션 취소. status='cancelled' | |||
GET /api/{tenant_id}/sessions/in-progress |
현재 사용자의 진행중 세션 목록. inspector_id=current_user, status='in_progress'. TopNav에서 진행중 세션 뱃지에 사용 | |||
| 4-3 | 템플릿 스냅샷 로직 | create_template_snapshot(template_id, db) 함수. 템플릿 기본정보 + 전체 항목(sort_order 순) JSON 직렬화. 스냅샷에 포함: template.name, template.version, template.schedule_type, items[{id, name, category, data_type, unit, spec_min, spec_max, select_options, equipment_part_id, is_required, sort_order}] |
backend | write |
| 4-4 | main.py에 sessions 라우터 등록 |
backend | edit | |
| 4-5 | Alembic 마이그레이션 | alembic revision --autogenerate -m "add_inspection_session" |
backend | bash |
| 4-6 | tests/test_sessions.py |
backend | write | |
| 세션 시작 → template_snapshot 정확히 저장되는지 확인 | ||||
| 자동저장(autosave) → partial_results 누적 확인 | ||||
| 자동저장 후 다시 GET → partial_results 복원 확인 | ||||
| 세션 완료 → results 확정, status 변경 확인 | ||||
| 완료 시 필수 항목 미입력 → 에러 반환 확인 | ||||
| 세션 취소 테스트 | ||||
| in-progress 목록 → 현재 사용자 것만 반환 확인 | ||||
| 이미 완료된 세션에 autosave 시도 → 에러 확인 | ||||
| Frontend | ||||
| 4-7 | lib/types.ts에 추가 |
InspectionSession, SessionStatus, PartialResult 타입 | frontend | edit |
| 4-8 | lib/hooks.ts에 추가 |
useSessions(tenantId, filters?), useSession(tenantId, sessionId), useInProgressSessions(tenantId) |
frontend | edit |
| 4-9 | components/InspectionForm.tsx — 프로젝트 핵심 컴포넌트 |
frontend | write | |
| 렌더링: template_snapshot 기반으로 폼 자동 생성. 항목별 입력 UI: | ||||
-numeric: 숫자 입력 + 단위 표시. spec_min/max 있으면 범위 표시. 범위 초과 시 빨간 테두리 + 경고 아이콘. warning_min/max 있으면 주황색 경고 |
||||
-boolean: 토글 스위치 (양호/불량 또는 OK/NG) |
||||
-text: 텍스트 입력 |
||||
-select: 드롭다운 (select_options 기반) |
||||
| 자동저장: 값 변경마다 2초 debounce → PATCH /sessions/{id}/autosave. SWR 낙관적 업데이트(mutate 호출). 저장 상태 표시("저장 중...", "저장됨 HH:MM:SS") | ||||
이탈 방지: beforeunload 이벤트 리스너. Next.js router 이벤트 후킹. 이탈 시도 시 "저장되지 않은 변경사항이 있습니다" 경고 |
||||
| 진행률: 입력 완료 항목 수 / 전체 항목 수 표시 (예: "8/12 완료") | ||||
| 완료 버튼: 필수 항목 미입력 시 비활성화 + 미입력 항목 하이라이트. 클릭 시 확인 다이얼로그 → POST /sessions/{id}/complete | ||||
| 카테고리 그룹핑: 항목을 category별로 그룹핑하여 섹션으로 표시 | ||||
| 부품 연결 표시: equipment_part_id가 있는 항목은 부품명 뱃지 표시 | ||||
| 모바일 반응형: 태블릿/모바일에서 터치 친화적 UI. 큰 입력 필드, 큰 버튼 | ||||
| 4-9a | ✨3가지 검사 레이아웃 (inspection_mode별) | frontend | write | |
ChecklistLayout.tsx — inspection_mode='checklist': 큰 OK/NG 버튼, 스와이프 진행, 모바일 최적화 (스피폭스 순회검사용) |
||||
MeasurementLayout.tsx — inspection_mode='measurement': 상세 폼, 이미지/X-ray 업로드, 로트번호 입력 (엔키드 품질검사용) |
||||
MonitoringLayout.tsx — inspection_mode='monitoring': 수치 테이블, 인라인 트렌드 스파크라인 (알펫 라인 모니터링용) |
||||
→next/dynamic으로 동적 로드. 같은 InspectionForm 컴포넌트를 감싸는 레이아웃 |
||||
| 4-10 | app/[tenant]/inspections/page.tsx — 검사 목록 |
frontend | write | |
| 탭: "진행 중" / "완료됨" / "취소됨" (status 필터) | ||||
| 진행 중 탭: 세션 카드(템플릿명, 검사자, 시작시간, 진행률). 클릭 시 검사 실행 화면으로 이동 → partial_results 자동 복원 | ||||
| 완료 탭: 세션 카드(템플릿명, 검사자, 완료시간, 결과 요약). 클릭 시 결과 상세 보기 | ||||
| "새 검사 시작" 버튼 → 템플릿 선택 모달 → POST /sessions → 검사 실행 화면으로 이동 | ||||
| 4-11 | app/[tenant]/inspections/[id]/page.tsx — 검사 실행 화면 |
frontend | write | |
| useSession()으로 데이터 로드 | ||||
| status='in_progress'면 InspectionForm 렌더링 (편집 모드) | ||||
| status='completed'면 결과 읽기 전용 표시 (스펙 이탈 항목 빨간 표시) | ||||
| status='cancelled'면 취소됨 안내 | ||||
| 4-12 | 진행중 세션 재진입 플로우 테스트 | 검사 목록에서 진행중 세션 클릭 → partial_results 자동 복원 → 이전에 입력한 값 그대로 표시 → 추가 입력 → 자동저장 → 완료 | frontend | /ui-test |
의존성: Phase 3 완료 후 (InspectionTemplate + Items 필요) 병렬 가능: 백엔드와 프론트엔드 병렬 예상 소요: 3일 (InspectionForm이 가장 복잡한 컴포넌트) UX 핵심 요구사항 체크리스트:
- 화면 이탈 시 자동저장됨 (절대 초기화 안됨)
- 진행중 세션 재진입 시 이전 입력값 복원됨
- 스펙 초과 시 즉시 시각적 경고
- 필수 항목 미입력 시 완료 불가
- 모바일/태블릿 터치 친화적
- 다중 세션 동시 진행 가능 (작업자가 여러 설비 순회)
Phase 5: 부품 카운터 + 교체 이력 (2~3일)
Phase 3과 병렬 개발 가능 — 서로 의존성 없음
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| Backend | ||||
| 5-1 | models.py에 PartCounter + PartReplacementLog 추가 |
위 스키마 참조. PartCounter.version으로 optimistic locking. PartCounter ↔ EquipmentPart 1:1 관계 | backend | edit |
| 5-2 | src/services/counter_service.py — 카운터 비즈니스 로직 |
backend | write | |
increment(db, equipment_part_id, delta, expected_version=None) |
||||
| → 카운터 조회, version 체크(expected_version이 주어지면), current_value += delta, lifecycle_pct 재계산, version += 1, last_updated_at = now | ||||
| → lifecycle_pct >= alarm_threshold 이면 check_alarm_threshold() 호출 | ||||
| → version 불일치 시 409 Conflict 반환 (optimistic locking) | ||||
reset(db, equipment_part_id, user_id, reason=None, notes=None) |
||||
| → 현재 카운터 스냅샷 생성 (counter_at_replacement, lifecycle_pct_at_replacement) | ||||
| → PartReplacementLog 레코드 생성 | ||||
| → 카운터 리셋: current_value=0, lifecycle_pct=0, last_reset_at=now, version += 1 | ||||
| → EquipmentPart.installed_at = now (새 부품 장착) | ||||
recalculate_lifecycle_pct(counter, equipment_part) |
||||
| → lifecycle_pct = (current_value / equipment_part.lifecycle_limit) * 100 | ||||
| → lifecycle_type='date'인 경우: (경과일수 / limit일수) * 100 | ||||
check_alarm_threshold(db, counter, equipment_part) |
||||
| → lifecycle_pct >= alarm_threshold 이면 InspectionAlarm 생성 (Phase 6에서 연결) | ||||
| → 이미 동일 부품에 대해 미확인 알람이 있으면 중복 생성하지 않음 | ||||
| 5-3 | src/api/counters.py — 카운터 API |
backend | write | |
GET /api/{tenant_id}/machines/{machine_id}/counters |
설비의 전체 부품 카운터 현황. 응답: [{part_name, category, lifecycle_type, lifecycle_limit, current_value, lifecycle_pct, last_reset_at, alarm_threshold}] | |||
GET /api/{tenant_id}/counters/critical |
알람 임계값 이상인 카운터 목록 (대시보드 "교체 임박" 섹션용) | |||
POST /api/{tenant_id}/parts/{part_id}/counter/increment |
카운터 수동 증가. body: {delta, expected_version?}. 응답: 갱신된 카운터 | |||
POST /api/{tenant_id}/parts/{part_id}/replace |
부품 교체. body: {reason?, notes?}. 로직: counter_service.reset() 호출. 응답: 교체 이력 레코드 + 리셋된 카운터 | |||
GET /api/{tenant_id}/parts/{part_id}/replacement-history |
교체 이력 목록 (최신 순). 응답: [{replaced_at, replaced_by_name, counter_at_replacement, lifecycle_pct_at_replacement, reason}] | |||
| 5-4 | main.py에 counters 라우터 등록 |
backend | edit | |
| 5-5 | Alembic 마이그레이션 | alembic revision --autogenerate -m "add_part_counter_and_replacement_log" |
backend | bash |
| 5-6 | tests/test_counters.py |
backend | write | |
| 카운터 증가 → current_value, lifecycle_pct 정확히 계산 | ||||
| 카운터 증가 → version 증가 확인 | ||||
| optimistic locking: 잘못된 expected_version → 409 Conflict | ||||
| 부품 교체(reset) → 카운터 0으로 리셋, 교체 이력 생성 확인 | ||||
| 교체 이력 → counter_at_replacement 스냅샷 정확한지 확인 | ||||
| 알람 임계값 도달 시 알람 생성 확인 (Phase 6 연결 후) | ||||
| critical 카운터 목록 API 테스트 | ||||
| Frontend | ||||
| 5-7 | lib/types.ts에 추가 |
PartCounter (확장), PartReplacementLog, CounterSummary 타입 | frontend | edit |
| 5-8 | lib/hooks.ts에 추가 |
useCounters(tenantId, machineId), useCriticalCounters(tenantId), useReplacementHistory(tenantId, partId) |
frontend | edit |
| 5-9 | components/PartLifecycleGauge.tsx — 수명 진행률 게이지 |
frontend | write | |
| 원형 게이지 또는 수평 진행바 | ||||
| 색상: 0 |
||||
| 중앙 또는 우측에 현재값/한계값 표시 (예: "8,500 / 10,000") | ||||
| 수명유형 아이콘 (hours→시계, count→카운터, date→달력) | ||||
| 5-10 | components/CounterCard.tsx — 카운터 현황 카드 |
frontend | write | |
| 부품명, 카테고리 뱃지 | ||||
| PartLifecycleGauge 포함 | ||||
| "교체" 버튼 (확인 다이얼로그 → POST /parts/{id}/replace) | ||||
| 마지막 교체일 표시 | ||||
| 5-11 | app/[tenant]/machines/[id]/page.tsx 수정 — 카운터 탭 추가 |
frontend | edit | |
| 기존 부품 탭 옆에 "카운터" 탭 추가 | ||||
| 카운터 탭: CounterCard 목록 (게이지 + 교체 버튼) | ||||
| 교체 이력 탭: 교체 이력 타임라인 (날짜, 교체자, 교체 시점 카운터값, 사유) | ||||
| 5-12 | app/[tenant]/page.tsx 수정 — 대시보드에 "교체 임박 부품" 섹션 |
frontend | edit | |
| useCriticalCounters()로 알람 임계값 이상 카운터 목록 | ||||
| 카드 형태로 표시: 설비명, 부품명, 수명%, 한계값 | ||||
| 클릭 시 해당 설비 상세 카운터 탭으로 이동 |
의존성: Phase 2 완료 후 (EquipmentPart 필요). Phase 3과 병렬 가능! 병렬 가능: 백엔드와 프론트엔드 병렬 예상 소요: 2~3일 레이스 컨디션 대응 전략:
Note
: PostgreSQL MVCC + 이 패턴으로 동시성 문제를 안전하게 처리함.
-- PostgreSQL 네이티브 쿼리
UPDATE part_counters
SET current_value = current_value + :delta,
lifecycle_pct = (current_value + :delta) / :lifecycle_limit * 100,
version = version + 1,
last_updated_at = now()
WHERE id = :counter_id AND version = :expected_version
RETURNING id, current_value, lifecycle_pct, version;
-- RETURNING 절로 업데이트 결과 즉시 반환 (PostgreSQL 전용 기능)
-- 결과가 없으면 → 409 Conflict 반환
Phase 6: 알람 시스템 (2일)
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| Backend | ||||
| 6-1 | models.py에 InspectionAlarm 추가 |
위 스키마 참조. 3가지 alarm_type, 2가지 severity | backend | edit |
| 6-2 | src/services/alarm_service.py — 알람 서비스 |
backend | write | |
create_alarm(db, tenant_id, alarm_type, severity, title, message, machine_id=None, equipment_part_id=None, session_id=None) |
알람 생성. 동일 부품/세션에 미확인 중복 알람이 있으면 생성 안 함 | |||
check_part_lifecycle_alarms(db, tenant_id) |
전체 부품 카운터 순회 → lifecycle_pct >= alarm_threshold인 것 찾아 알람 생성. 배치용 (스케줄러 또는 API 호출) | |||
check_overdue_inspections(db, tenant_id) |
schedule_type별 마지막 세션 완료 시간 체크 → 기한 초과 시 알람 생성. daily: 오늘 완료 세션 없으면, weekly: 이번 주 완료 세션 없으면 등 | |||
create_spec_violation_alarm(db, session, item_name, value, spec_min, spec_max) |
검사 완료 시 스펙 이탈 항목에 대해 호출. severity: spec 초과 정도에 따라 warning/critical | |||
get_alarm_summary(db, tenant_id) |
미확인 알람 타입별 개수. 응답: {part_lifecycle: 3, inspection_overdue: 1, spec_violation: 2, total: 6} | |||
| 6-3 | src/api/alarms.py — 알람 API |
backend | write | |
GET /api/{tenant_id}/alarms |
알람 목록. 쿼리: alarm_type, severity, is_acknowledged, machine_id. 페이지네이션. 최신 순 | |||
GET /api/{tenant_id}/alarms/summary |
미확인 알람 개수 요약 (TopNav 뱃지용). 응답: {total: N, by_type: {...}} | |||
POST /api/{tenant_id}/alarms/{id}/acknowledge |
알람 확인 처리. acknowledged_by=current_user, acknowledged_at=now | |||
POST /api/{tenant_id}/alarms/acknowledge-bulk |
다중 알람 일괄 확인. body: {alarm_ids: [...]} | |||
| 6-4 | Phase 4 연결: 세션 완료 로직에 스펙 이탈 알람 추가 | sessions.py의 complete 엔드포인트에서 각 결과값을 template_snapshot의 spec_min/max와 비교. 이탈 항목마다 alarm_service.create_spec_violation_alarm() 호출 |
backend | edit |
| 6-5 | Phase 5 연결: 카운터 서비스에 수명 알람 추가 | counter_service.py의 check_alarm_threshold()에서 alarm_service.create_alarm() 호출. alarm_type='part_lifecycle', severity는 lifecycle_pct >= 100이면 'critical', 그 외 'warning' |
backend | edit |
| 6-6 | main.py에 alarms 라우터 등록 |
backend | edit | |
| 6-7 | Alembic 마이그레이션 | alembic revision --autogenerate -m "add_inspection_alarm" |
backend | bash |
| 6-8 | tests/test_alarms.py |
backend | write | |
| 알람 수동 생성 + 조회 테스트 | ||||
| 중복 알람 방지 테스트 (같은 부품에 미확인 알람 있으면 중복 생성 안 됨) | ||||
| 알람 확인(acknowledge) 테스트 | ||||
| 일괄 확인 테스트 | ||||
| 알람 요약(summary) 테스트 | ||||
| 필터(alarm_type, severity, is_acknowledged) 테스트 | ||||
| 카운터 증가 → 임계값 도달 → 알람 자동 생성 E2E | ||||
| 세션 완료 → 스펙 이탈 → 알람 자동 생성 E2E | ||||
| Frontend | ||||
| 6-9 | lib/types.ts에 추가 |
InspectionAlarm, AlarmType, AlarmSeverity, AlarmSummary 타입 | frontend | edit |
| 6-10 | lib/hooks.ts에 추가 |
useAlarms(tenantId, filters?), useAlarmSummary(tenantId) |
frontend | edit |
| 6-11 | components/AlarmBadge.tsx — TopNav 알람 뱃지 |
frontend | write | |
| useAlarmSummary()로 미확인 알람 총 개수 조회 | ||||
| 0이면 표시 안 함, 1 이상이면 빨간 원형 뱃지로 개수 표시 | ||||
| 클릭 시 /alarms 페이지로 이동 | ||||
| 30초 자동 갱신 (SWR refreshInterval) | ||||
| 6-12 | app/[tenant]/alarms/page.tsx — 알람 목록 |
frontend | write | |
| 필터 바: 알람 유형(부품수명/검사미실시/스펙이탈), 심각도(경고/긴급), 확인여부(미확인/확인) | ||||
| 알람 카드 목록: 아이콘(유형별), 제목, 메시지, 생성시간, 설비명, 부품명 | ||||
| 미확인 알람: "확인" 버튼 (POST /alarms/{id}/acknowledge) | ||||
| 일괄 확인: 체크박스 선택 → "선택 확인" 버튼 | ||||
| severity에 따른 색상: warning=주황, critical=빨강 | ||||
| 6-13 | TopNav에 AlarmBadge 통합 | TopNav.tsx에 AlarmBadge 컴포넌트 추가 | frontend | edit |
의존성: Phase 4(세션 완료 시 스펙 이탈) + Phase 5(카운터 임계값) 완료 후 6-4, 6-5 연결. 단, 6-1~6-3(알람 기본 CRUD)은 Phase 4/5와 병렬 가능 병렬 가능: 알람 모델/API 기본 구조는 Phase 3 이후 바로 시작 가능. 연결(6-4, 6-5)만 Phase 4/5 이후 예상 소요: 2일
Phase 7: 통합 테스트 + 마무리 (1일)
| # | 작업 | 상세 | 카테고리 | 스킬 |
|---|---|---|---|---|
| 7-1 | 전체 pytest 실행 + 실패 수정 | pytest tests/ -v 전체 통과 확인 |
test | /test |
| 7-2 | E2E 플로우 테스트 (브라우저) | test | /ui-test | |
| 플로우 1: 로그인 → 설비 등록 → 부품 등록 → 카운터 확인 | ||||
| 플로우 2: 템플릿 생성 (항목 추가, 부품 연결) → 저장 → 재편집 | ||||
| 플로우 3: 검사 시작 → 항목 입력 → 다른 페이지 이동 → 돌아옴 → 이전 입력 복원 → 완료 | ||||
| 플로우 4: 카운터 증가 → 임계값 도달 → 알람 생성 → 알람 확인 | ||||
| 플로우 5: 부품 교체 → 카운터 리셋 → 교체 이력 확인 | ||||
| 7-3 | API 문서 검증 | FastAPI 자동생성 /docs (Swagger) 확인. 모든 엔드포인트 정상 표시, 요청/응답 스키마 정확한지 | test | /ui-test |
| 7-4 | 모바일 반응형 확인 | 태블릿 뷰포트(768px)에서 검사 실행 화면 사용성 확인. 터치 타겟 충분한 크기, 스크롤 적절, 입력 편의성 | test | /ui-test |
| 7-5 | 시드 데이터 스크립트 | scripts/seed.py — 3개 테넌트 시드 데이터: |
backend | write |
| 스피폭스: 설비 5대 (프레스 3, 건조 1, 세척 1), 부품 각 3~5개 (금형, 팁, 센서), 템플릿 3개 (일일 checklist, 주간 measurement, 월간), 샘플 카운터 값 | ||||
| 엔키드: 설비 3대 (Toyo 650T, Toshiba 350T, Toyo 200T), 부품 각 2~3개 (금형, 플런저팁, 슬리브), 템플릿 2개 (일일 checklist, 품질검사 measurement + X-ray select 항목) | ||||
| 알펫: 설비 4대 (전처리, 라미네이팅, 코팅, 리코일러), 부품 각 2~3개 (히터 소자, 화학약품, 롤), 템플릿 2개 (일일 monitoring, 주간 measurement) | ||||
| 7-6 | 정리 | 사용하지 않는 import 제거, 코드 포매팅, 주석 정리 | cleanup | edit |
의존성: Phase 6 완료 후 예상 소요: 1일
3. Task 의존성 그래프
Phase 0 (부트스트래핑, 0.5일)
│
▼
Phase 1 (인증 + DB, 1.5일)
│
▼
Phase 2 (설비 + 부품, 2일)
│
├──────────────────────────────┐
▼ ▼
Phase 3 (검사 템플릿, 2~3일) Phase 5 (카운터 + 교체, 2~3일)
│ │
▼ │ ← 이 두 Phase는 서로 의존 없음!
Phase 4 (검사 실행, 3일) │ 병렬 개발로 ~2일 단축 가능
│ │
├──────────────────────────────┘
▼
Phase 6 (알람, 2일)
│ ← Phase 4의 세션완료 + Phase 5의 카운터 서비스에 알람 연결
▼
Phase 7 (통합 테스트, 1일)
병렬 실행 최적화 요약
| 병렬 기회 | 설명 | 절약 시간 |
|---|---|---|
| Phase 3 + Phase 5 | 검사 템플릿과 카운터 시스템은 서로 의존 없음 | ~2일 |
| 각 Phase 내 BE + FE | 백엔드와 프론트엔드 항상 병렬 가능 (타입 목업 활용) | 각 Phase에서 ~0.5일 |
| Phase 6 기본 CRUD | 알람 모델/API 기본 구조는 Phase 2 이후 시작 가능 (연결만 나중에) | ~0.5일 |
4. 예상 일정 요약
| Phase | 내용 | 순차 시 | 병렬 시 | 누적 (병렬) |
|---|---|---|---|---|
| 0 | 프로젝트 부트스트래핑 | 0.5일 | 0.5일 | 0.5일 |
| 1 | 인증 + DB 기반 | 1.5일 | 1.5일 | 2일 |
| 2 | 설비 + 부품 관리 (+counter_source) | 2.5일 | 2.5일 | 4.5일 |
| 3 | 검사 템플릿 (+inspection_mode, warning, trend) | 3.5일 | 3.5일 (Phase 5와 병렬) | 8일 |
| 5 | 카운터 + 교체 이력 | 3일 | — (Phase 3과 동시) | — |
| 4 | 검사 실행 (+3개 레이아웃, lot_number) | 4일 | 4일 | 12일 |
| 6 | 알람 시스템 (+trend_warning) | 2.5일 | 2.5일 | 14.5일 |
| 7 | 통합 테스트 + 멀티테넌트 시드 | 1.5일 | 1.5일 | 16일 |
| 합계 | 19일 | 16일 | 약 3.2주 |
기존 13일 → 16일 (+3일): 3개 고객사 멀티테넌트 지원을 위한 추가 작업 상세 변경 내역: 섹션 9 "3개 고객사 멀티테넌트 전략" 참조
5. 각 작업의 Agent 위임 추천
| 카테고리 | 추천 방식 | 사용 스킬/도구 | 비고 |
|---|---|---|---|
| 프로젝트 셋업 (Phase 0) | 직접 실행 | bash + write | 디렉토리 생성, 초기화, npm init |
| DB 모델 작성 | 직접 실행 | write | SQLAlchemy 모델 정의 |
| API 라우터 작성 | 직접 실행 | write | FastAPI 엔드포인트, Pydantic 스키마 |
| 서비스 로직 | 직접 실행 | write | counter_service.py, alarm_service.py |
| 테스트 작성 + 실행 | 직접 작성 →/test 실행 |
write + /test | pytest |
| Alembic 마이그레이션 | 직접 실행 | bash | alembic revision --autogenerate |
| 프론트엔드 페이지 | /frontend-design 스킬 |
/frontend-design | 레이아웃 + Material Design 3 스타일 |
| InspectionForm (핵심) | /frontend-design + /ui-test |
/frontend-design → /ui-test | 자동저장, debounce, 반응형, 이탈방지 |
| TemplateEditor | /frontend-design |
/frontend-design | 드래그 순서변경, 조건부 UI |
| PartLifecycleGauge | /frontend-design |
/frontend-design | 원형 게이지, 색상 전환 |
| 서버 실행 | /serve 스킬 |
/serve | Podman 기반 |
| UI 검증 | /ui-test 스킬 |
/ui-test | Playwright 브라우저 테스트 |
| 커밋 | /commit 스킬 |
/commit | 각 Phase 완료 시 |
| 코드 리뷰 | /review 스킬 |
/review | Phase 4, 6 완료 후 |
6. 기존 코드 참고 가이드
⚠️ 기존 factoryOps 프로젝트는 참고만 합니다. 코드를 복사하지 않고, 패턴과 로직만 발췌하여 새로 작성합니다.
참고할 패턴 (기존 코드에서 발췌할 것)
| 영역 | 참고 파일 (기존) | 발췌할 패턴 |
|---|---|---|
| JWT 인증 | src/auth/jwt_handler.py |
HS256 알고리즘, 24h 만료, 페이로드 구조 |
| 비밀번호 해싱 | src/auth/password.py |
bcrypt 해싱 로직 |
| API 구조 | src/auth/router.py |
FastAPI 라우터 패턴 (login, /me 등) |
| 테넌트 격리 | src/tenant/manager.py |
tenant_id 기반 데이터 격리 패턴 |
| SWR 패턴 | dashboard/lib/api.ts |
fetcher + useData<T>() 패턴 (30s refresh, 2s dedup) |
| URL 헬퍼 | dashboard/lib/api.ts |
getTenantUrl() (/api/{tenant_id}/... 패턴) |
| CSS 변수 | dashboard/app/globals.css |
Material Design 3 CSS 변수 시스템 |
| React Context | dashboard/lib/auth-context.tsx |
AuthContext, TenantContext, ToastContext 패턴 |
| 어댑터 | src/adapters/speedfox_adapter.py |
BaseAdapter 인터페이스, 데이터 변환 패턴 |
새로 작성할 때 주의사항
- PostgreSQL 전용:
aiosqlite,sqlite3관련 코드 일체 불필요 - Async 우선: SQLAlchemy async session, asyncpg 드라이버 사용
- 간소화: 기존의 Score/Action/Stats/Ontology 등 불필요한 모델 포함하지 않음
- main.py 목표 60~80줄: 최소 구성 (app 생성, CORS, routers, init_db)
7. 기술 의존성 (requirements.txt / package.json)
Backend (Python)
# Core
fastapi==0.115.0
uvicorn[standard]==0.30.0
python-multipart==0.0.9
# Database (PostgreSQL 전용, async)
sqlalchemy[asyncio]==2.0.36
asyncpg==0.30.0 # PostgreSQL async 드라이버
alembic==1.14.0
# Auth
PyJWT==2.9.0
bcrypt==4.2.0
# Testing
pytest==8.2.2
pytest-asyncio==0.24.0 # async 테스트 지원
httpx==0.27.0 # async TestClient
⚠️ psycopg2-binary 대신 asyncpg 사용 — FastAPI의 async 핸들러와 자연스럽게 결합. SQLite 관련 패키지(aiosqlite 등) 불필요.
Frontend (Node.js)
{
"dependencies": {
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"swr": "2.3.8",
"clsx": "2.1.1"
},
"devDependencies": {
"typescript": "5.x",
"@types/react": "19.x",
"@types/node": "22.x"
}
}
8. API 엔드포인트 전체 목록
인증 (Phase 1)
POST /api/auth/login # 로그인
POST /api/auth/logout # 로그아웃
GET /api/auth/me # 현재 사용자 정보
POST /api/admin/users # 사용자 생성 (superadmin)
GET /api/admin/users # 사용자 목록 (superadmin)
설비 (Phase 2)
GET /api/{tenant_id}/machines # 설비 목록
POST /api/{tenant_id}/machines # 설비 등록
GET /api/{tenant_id}/machines/{id} # 설비 상세
PUT /api/{tenant_id}/machines/{id} # 설비 수정
DELETE /api/{tenant_id}/machines/{id} # 설비 삭제
설비 부품 (Phase 2)
GET /api/{tenant_id}/machines/{machine_id}/parts # 부품 목록
POST /api/{tenant_id}/machines/{machine_id}/parts # 부품 등록 (+카운터 자동생성)
GET /api/{tenant_id}/parts/{id} # 부품 상세
PUT /api/{tenant_id}/parts/{id} # 부품 수정
DELETE /api/{tenant_id}/parts/{id} # 부품 비활성화
검사 템플릿 (Phase 3)
GET /api/{tenant_id}/templates # 템플릿 목록 (subject_type 필터)
POST /api/{tenant_id}/templates # 템플릿 생성 (항목 포함)
GET /api/{tenant_id}/templates/{id} # 템플릿 상세 (항목 포함)
PUT /api/{tenant_id}/templates/{id} # 템플릿 수정 (버전 증가)
DELETE /api/{tenant_id}/templates/{id} # 템플릿 비활성화
POST /api/{tenant_id}/templates/{id}/items # 항목 추가
PUT /api/{tenant_id}/templates/{id}/items/{item_id} # 항목 수정
DELETE /api/{tenant_id}/templates/{id}/items/{item_id} # 항목 삭제
PUT /api/{tenant_id}/templates/{id}/items/reorder # 항목 순서 변경
검사 세션 (Phase 4)
POST /api/{tenant_id}/sessions # 세션 시작 (템플릿 스냅샷 저장)
GET /api/{tenant_id}/sessions # 세션 목록 (status 필터)
GET /api/{tenant_id}/sessions/in-progress # 현재 사용자 진행중 세션
GET /api/{tenant_id}/sessions/{id} # 세션 상세
PATCH /api/{tenant_id}/sessions/{id}/autosave # 자동저장 (partial_results)
POST /api/{tenant_id}/sessions/{id}/complete # 세션 완료
POST /api/{tenant_id}/sessions/{id}/cancel # 세션 취소
부품 카운터 (Phase 5)
GET /api/{tenant_id}/machines/{machine_id}/counters # 설비 카운터 현황
GET /api/{tenant_id}/counters/critical # 교체 임박 카운터 목록
POST /api/{tenant_id}/parts/{part_id}/counter/increment # 카운터 증가
POST /api/{tenant_id}/parts/{part_id}/replace # 부품 교체 (리셋+이력)
GET /api/{tenant_id}/parts/{part_id}/replacement-history # 교체 이력
알람 (Phase 6)
GET /api/{tenant_id}/alarms # 알람 목록 (type/severity/acknowledged 필터)
GET /api/{tenant_id}/alarms/summary # 미확인 알람 개수 요약
POST /api/{tenant_id}/alarms/{id}/acknowledge # 알람 확인
POST /api/{tenant_id}/alarms/acknowledge-bulk # 다중 알람 일괄 확인
총 엔드포인트 수: 32개
9. 3개 고객사 멀티테넌트 전략 (2026-02-10 추가)
상세 분석:
planning/integrated-analysis.md참조
9.1 업체별 설비검사/품질검사 로직 차이 → 해결 전략 요약
| 차이 영역 | 해결 메커니즘 | 코드 분기 여부 |
|---|---|---|
| 카운터 갱신 (PLC vs 시간 vs 날짜) | EquipmentPart.lifecycle_type + counter_source |
❌ TYPE 분기 |
| 품질 판정 (이진 vs 불량코드 vs 수치) | InspectionTemplateItem.data_type + spec_min/max |
❌ TYPE 분기 |
| 알람 유형 (임계값 vs 추세) | InspectionAlarm.alarm_type (4종) + 템플릿 설정 |
❌ TYPE 분기 |
| 검사 UX (빠른체크 vs 정밀 vs 모니터링) | InspectionTemplate.inspection_mode (3종) |
❌ TYPE 분기 |
| PLC 데이터 형식 | 어댑터 계층 (SpeedfoxAdapter / EnkiedAdapter / AlpetAdapter) | ✅ (설계상 분리) |
| 테넌트 정책 (동시세션수, 첨부파일 등) | Tenant.workflow_config JSON |
❌ 설정값 |
9.2 스키마 변경 요약 (기존 대비)
추가된 컬럼 (7개):
EquipmentPart.counter_sourceVARCHAR(20) — 카운터 갱신 방식InspectionTemplate.inspection_modeVARCHAR(20) — 검사 UX 모드InspectionTemplateItem.warning_minFLOAT — 트렌드 경고 소프트 하한InspectionTemplateItem.warning_maxFLOAT — 트렌드 경고 소프트 상한InspectionTemplateItem.trend_windowINTEGER — 연속 N회 경고구간 체크InspectionSession.lot_numberVARCHAR(100) — 로트/시리얼 (IATF 추적성)InspectionAlarm.alarm_type—trend_warning값 추가 (기존 3종 → 4종)
추가된 프론트엔드 레이아웃 (3개):
dashboard/app/[tenant]/inspections/[id]/_layouts/
ChecklistLayout.tsx # inspection_mode='checklist'
MeasurementLayout.tsx # inspection_mode='measurement'
MonitoringLayout.tsx # inspection_mode='monitoring'
추가된 어댑터 (1개):
src/adapters/alpet_adapter.py— 알펫 연속공정 PLC 데이터 변환
9.3 구현 순서 (기존 Phase에 통합)
| Phase | 기존 내용 | 추가 작업 | 추가 소요 |
|---|---|---|---|
| Phase 2 | 설비 + 부품 | counter_source 컬럼 추가, auto_time 계산 로직 |
+0.5일 |
| Phase 3 | 검사 템플릿 | inspection_mode 컬럼 + warning_min/max/trend_window 추가 |
+0.5일 |
| Phase 4 | 검사 세션 | lot_number 컬럼, 3개 레이아웃 (ChecklistLayout 등) |
+1일 |
| Phase 6 | 알람 | trend_warning 알람 유형 + 트렌드 평가 로직 |
+0.5일 |
| Phase 7 | 통합 테스트 | 엔키드/알펫 시드 데이터, 멀티테넌트 E2E 테스트 | +0.5일 |
| 합계 | +3일 |
수정된 일정: 기존 13일 → 16일 (약 3.2주)
9.4 주의사항
- 알펫 설비 계층: 알펫의 Machine은 개별 기계가 아닌 라인 구간 (전처리존, 라미네이팅 스테이션 등). Machine 모델로 표현 가능하나, 이름 규칙으로 구분 (예: "ALPET-LINE1-PRETREAT", "ALPET-LINE1-LAMI")
- 엔키드 로트 추적: IATF 16949은 검사 결과 → 특정 로트/시리얼 연결 필수.
lot_number필드로 대응 - 스피폭스 대규모 알람 성능: 304대 × 부품 → 알람 평가는 검사 제출/카운터 갱신 시점에만 (PLC 원시 데이터 수신 시에는 평가 안 함)
10. 미결 질문 (구현 전 확인 필요)
-
병렬 개발 전략: Phase 3(검사 템플릿)과 Phase 5(카운터)를 병렬로 진행할지, 순차로 할지?
- 병렬: ~2일 단축 (16일)
- 순차: 더 안전하지만 더 오래 걸림 (18.5일)
-
품질검사 범위: 이번 구현에서
subject_type='product'도 실제 UI/로직을 만들지?- 추천: equipment만 우선 구현. 모델에 subject_type 필드만 두고, product 관련 UI/로직은 다음 Phase에서 확장
- 엔키드가 product 검사 필요하므로 Phase 1+ 이후 확장 예정
- 둘 다 하면 2~3일 추가
-
검사 미실시(overdue) 알람의 기준: 스케줄러(cron)로 주기적 체크할지, API 호출 시 체크할지?
- 옵션 A: 별도 스케줄러 (APScheduler 등) — 정확하지만 인프라 복잡도 증가
- 옵션 B: 대시보드 로딩 시 체크 — 단순하지만 대시보드 접속 안 하면 알람 안 생김
- 옵션 C: 알람 목록 API 호출 시 체크 — 절충안
- 추천: Phase 1에서는 옵션 C (on_access), 향후 스케줄러로 확장
-
알펫 PLC 태그 리스트: AlpetAdapter 구현 전 알펫에서 PLC 태그 사양서 수집 필요
-
엔키드 X-ray 이미지 저장소: 첨부파일 스토리지 설계 (S3? 로컬? 용량 예측 필요)
-
연속공정 세션 단위: 알펫의 검사 세션 = 코일 1롤? 시간 구간? 교대 1회?