schemalism
PART 03·of 04

'완료'는 끝이 아니다 — 효과성 평가와 재조치 사슬

완료를 3개로 쪼개 지각도 분류로 박고, 그 위에 효과성 평가 한 단계를 더 올렸습니다. NEEDS_IMPROVEMENT 가 self-FK 로 이어지는 재조치 chain, 그리고 NC 상태를 chain leaf 로만 자동 계산하는 트릭.

2026년 7월 19일 · 정윤환 · 6

조치 상태(ActionStatus)를 처음 설계할 때는 익숙한 3개로 시작했습니다.

enum
PENDING → IN_PROGRESS → COMPLETED

그런데 운영해 보니 두 가지가 빠져 있었어요.

  1. “기한 내에”완료됐는지가 통계에 안 들어옵니다. “완료 100건” 안에 한 달 늦은 게 30건이어도 합쳐서 100건으로 보입니다.
  2. “완료됐는데 효과가 있었나”가 추적이 안 됩니다. 환기팬 단 게 완료지만, 흄 농도가 그대로면 이건 완료가 아닙니다.

01 · Three completions

첫 보완 — “완료”를 셋으로 쪼갰습니다

enum
PENDING
IN_PROGRESS
COMPLETED_ON_TIME       # 기한 내 완료
COMPLETED_WITHIN_30     # 기한 후 30일 이내 완료
COMPLETED_AFTER_30      # 기한 후 30일 이후 완료
CANCELLED

지각도 완료의 한 종류로 분류했습니다. 별도 “기한 내 완료율” 컬럼을 만들 필요가 없어졌어요. 통계는 enum 분포 그대로 떨어집니다. 30 일이라는 경계는 운영 합의로 정한 임의값 이지만 — 핵심은 “완료에는 세 종류가 있다” 를 데이터 타입 수준에서 박은 것입니다.

02 · Effectiveness layer

두 번째 보완 — 완료 위에 “효과성 평가” 한 단계를 더 올렸습니다

조치를 만들 때 propose_effectiveness_check = True 로 두면, 완료 후 작성자에게 효과성 평가 요청 알림이 발송됩니다. 결과는 둘 중 하나.

python
class ActionEffectivenessStatus(Enum):
    EFFECTIVE = 'EFFECTIVE'                  # 효과적
    NEEDS_IMPROVEMENT = 'NEEDS_IMPROVEMENT'  # 개선 필요

NEEDS_IMPROVEMENT 가 나오면, 같은 NC 안에 새 EXE 를 만듭니다. 그 새 EXE 는 previous_execution_id 로 이전 EXE 를 가리킵니다.

python
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)',
    )
Re-action chain · self-FK
leaf 만 NC 상태로 올라간다

같은 NC 안에서 “환기팬 → 흄후드 동선 변경 → 국소배기 보강” 으로 이어지는 재조치 chain 이 self-FK 로 연결됩니다. 시도 횟수, 재조치 빈도, 한 번에 닫히는 비율 — 모두 이 chain 하나로 추적됩니다.

03 · Leaf-only derivation

NC 상태를 chain 의 leaf 로 계산하는 트릭

NC 상태를 다 합쳐 계산하면 안 됩니다. 첫 시도 EXE 가 “완료(효과 없음)” 인 채로 남아 있으니, 단순 평균을 내면 NC 가 계속 “완료” 상태로 보이거든요.

그래서 chain 의 leaf 만 봅니다.

python
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 가 있어요.

enum
NOT_REQUIRED  # require_approval=False 인 경우 (대부분)
PENDING       # 승인 대기 — 결과 기록 불가
APPROVED      # 승인 완료 — 결과 기록 가능
REJECTED      # 반려 — rejection_reason 에 사유 기록

승인이 PENDING 인 동안에는 결과 입력 폼이 잠깁니다. 캔버스에서 그 EXE 의 결과 노드가 회색으로 비활성화됩니다. 데이터 모델의 enum 하나가 UI 가드 하나로 그대로 옮겨갑니다.

05 · Conclusion

결론 — 완료는 시점이 아니라 사슬

조치 한 건이 진짜로 닫히려면 이렇게 흐릅니다.

flow
계획 → (승인) → 실행 → 완료 → (효과성 평가) → VERIFIED
                                  └→ NEEDS_IMPROVEMENT → 새 EXE → ...

이 흐름의 어느 칸 하나라도 시스템 밖에 두면, 부적합은 결국 “완료 처리 후 잊혀집니다.” 우리는 이 칸들을 한 칸씩 데이터 모델로 끌어들였습니다 — revision 으로 동시편집, previous_execution_id 로 chain, propose_effectiveness_check 로 사후평가, ActionStatusHistory 로 이력. 완료는 시점이 아니라 사슬이라는 것 을 모델이 먼저 인정해야, UI 가 따라옵니다.

Next

다음 편 — 부적합이 들어오는 출처 — BBS, 문화 진단, 리스크, 사고… — 를 어떻게 한 곳에 모았는지, 그리고 도중에 한 번 결정을 뒤집은 이야기.