'완료'는 끝이 아니다 — 효과성 평가와 재조치 사슬
완료를 3개로 쪼개 지각도 분류로 박고, 그 위에 효과성 평가 한 단계를 더 올렸습니다. NEEDS_IMPROVEMENT 가 self-FK 로 이어지는 재조치 chain, 그리고 NC 상태를 chain leaf 로만 자동 계산하는 트릭.
2026년 7월 19일 · 정윤환 · 6분
조치 상태(ActionStatus)를 처음 설계할 때는 익숙한 3개로 시작했습니다.
PENDING → IN_PROGRESS → COMPLETED그런데 운영해 보니 두 가지가 빠져 있었어요.
- “기한 내에”완료됐는지가 통계에 안 들어옵니다. “완료 100건” 안에 한 달 늦은 게 30건이어도 합쳐서 100건으로 보입니다.
- “완료됐는데 효과가 있었나”가 추적이 안 됩니다. 환기팬 단 게 완료지만, 흄 농도가 그대로면 이건 완료가 아닙니다.
01 · Three completions
첫 보완 — “완료”를 셋으로 쪼갰습니다
PENDING
IN_PROGRESS
COMPLETED_ON_TIME # 기한 내 완료
COMPLETED_WITHIN_30 # 기한 후 30일 이내 완료
COMPLETED_AFTER_30 # 기한 후 30일 이후 완료
CANCELLED지각도 완료의 한 종류로 분류했습니다. 별도 “기한 내 완료율” 컬럼을 만들 필요가 없어졌어요. 통계는 enum 분포 그대로 떨어집니다. 30 일이라는 경계는 운영 합의로 정한 임의값 이지만 — 핵심은 “완료에는 세 종류가 있다” 를 데이터 타입 수준에서 박은 것입니다.
02 · Effectiveness layer
두 번째 보완 — 완료 위에 “효과성 평가” 한 단계를 더 올렸습니다
조치를 만들 때 propose_effectiveness_check = True 로 두면, 완료 후 작성자에게 효과성 평가 요청 알림이 발송됩니다. 결과는 둘 중 하나.
class ActionEffectivenessStatus(Enum):
EFFECTIVE = 'EFFECTIVE' # 효과적
NEEDS_IMPROVEMENT = 'NEEDS_IMPROVEMENT' # 개선 필요NEEDS_IMPROVEMENT 가 나오면, 같은 NC 안에 새 EXE 를 만듭니다. 그 새 EXE 는 previous_execution_id 로 이전 EXE 를 가리킵니다.
class ActionExecution(Base):
...
previous_execution_id = Column(
String(36),
ForeignKey("action_execution.id", ondelete="SET NULL"),
nullable=True,
comment='이전 EXE id. NEEDS_IMPROVEMENT 로 인한 재조치 chain 추적 (self-FK)',
)같은 NC 안에서 “환기팬 → 흄후드 동선 변경 → 국소배기 보강” 으로 이어지는 재조치 chain 이 self-FK 로 연결됩니다. 시도 횟수, 재조치 빈도, 한 번에 닫히는 비율 — 모두 이 chain 하나로 추적됩니다.
03 · Leaf-only derivation
NC 상태를 chain 의 leaf 로 계산하는 트릭
NC 상태를 다 합쳐 계산하면 안 됩니다. 첫 시도 EXE 가 “완료(효과 없음)” 인 채로 남아 있으니, 단순 평균을 내면 NC 가 계속 “완료” 상태로 보이거든요.
그래서 chain 의 leaf 만 봅니다.
def _resolve_chain_leaves(exes):
"""재조치 chain 의 leaf 만 추출.
EXE.previous_execution_id 가 가리키는 EXE 는 chain 의 이전 step.
같은 chain 내에서 가장 최신 EXE (= 다른 EXE 의 previous_execution_id 로
참조되지 않는 EXE) 만 leaf.
"""
referenced = {e.previous_execution_id for e in exes if e.previous_execution_id}
return [e for e in exes if e.id not in referenced]NC 의 상태는 이 leaf 들의 상태에서 derive 됩니다.
- leaf 가 하나도 없으면 → OPEN
- 미완료 leaf 가 하나라도 있으면 → IN_PROGRESS
- 모든 leaf 완료, 효과성 평가 불요 → VERIFIED
- 모든 leaf 완료, 평가 미정 → AWAITING_VERIFICATION
- leaf 중 NEEDS_IMPROVEMENT 가 있으면 → REWORK
이 로직은 action_lifecycle_service.recompute_nc_status 한 함수에 모여 있습니다. NC / RCA / EXE 의 create / update / delete 모든 시점에 호출됩니다. 단일 진입점. 상태가 변하면 ActionStatusHistory 에 한 줄 append. 사람이 손으로 NC 상태를 옮기지 않습니다 — 명시 종결(CLOSED / CANCELLED) 만 빼고 는요.
04 · Approval gate
승인을 한 단계 더 올린 이유
조직에 따라서는 “담당자가 조치 계획을 직접 짠 다음, 안전팀장이 승인해야 결과 입력으로 넘어간다” 가 필요합니다. 그래서 EXE 에는 또 별도의 ActionExecutionApprovalStatus 가 있어요.
NOT_REQUIRED # require_approval=False 인 경우 (대부분)
PENDING # 승인 대기 — 결과 기록 불가
APPROVED # 승인 완료 — 결과 기록 가능
REJECTED # 반려 — rejection_reason 에 사유 기록승인이 PENDING 인 동안에는 결과 입력 폼이 잠깁니다. 캔버스에서 그 EXE 의 결과 노드가 회색으로 비활성화됩니다. 데이터 모델의 enum 하나가 UI 가드 하나로 그대로 옮겨갑니다.
05 · Conclusion
결론 — 완료는 시점이 아니라 사슬
조치 한 건이 진짜로 닫히려면 이렇게 흐릅니다.
계획 → (승인) → 실행 → 완료 → (효과성 평가) → VERIFIED
└→ NEEDS_IMPROVEMENT → 새 EXE → ...이 흐름의 어느 칸 하나라도 시스템 밖에 두면, 부적합은 결국 “완료 처리 후 잊혀집니다.” 우리는 이 칸들을 한 칸씩 데이터 모델로 끌어들였습니다 — revision 으로 동시편집, previous_execution_id 로 chain, propose_effectiveness_check 로 사후평가, ActionStatusHistory 로 이력. 완료는 시점이 아니라 사슬이라는 것 을 모델이 먼저 인정해야, UI 가 따라옵니다.
Next
다음 편 — 부적합이 들어오는 출처 — BBS, 문화 진단, 리스크, 사고… — 를 어떻게 한 곳에 모았는지, 그리고 도중에 한 번 결정을 뒤집은 이야기.