- 발행일
7단계를 박아둔 템플릿이 5단계 환경에서 거짓말을 했다 — 절차 안내를 프로세스 정의에서 그리기
7단계를 박아둔 템플릿이 5단계 환경에서 거짓말을 했다
심사 목록 화면 맨 위에는 절차 안내가 있습니다. 기록 → 제출 → 사전검증 → 검증 → 사전인증 → 인증 → 배포. ISO 14817-2의 데이터 개념 라이프사이클을 그대로 받은 7단계예요.
그런데 이 시스템은 5단계로도 돕니다. 간편 등록절차를 켜면 사전인증과 인증이 빠져요. 운영 환경에 따라 어느 쪽인지가 갈립니다.
문제는 단계 수가 DB의 프로세스 정의에 있는데 화면은 템플릿에 박혀 있었다는 겁니다. 5단계로 도는 환경에서 목록을 열면 존재하지 않는 사전인증·인증이 안내에 그려져 있었어요. 신청자가 "아직 두 단계 남았구나" 하고 기다릴 자리에 실은 아무것도 없습니다.
단계 수가 데이터에 있는데 화면은 코드에 있었다
| 방법 | 장점 | 포기하는 것 | 판단 |
|---|---|---|---|
| ① 7단계 고정 유지 | 변경 0 | 5단계 환경에서 계속 거짓 | 기각 |
| ② 설정값(5/7)을 읽어 템플릿에서 분기 | 숫자는 맞음 | 단계 이름·역할은 여전히 코드에. 프로세스가 또 갈리면 세 번째 분기를 더해야 함 | 기각 |
| ③ 프로세스 정의(상태·전이)를 걸어 단계 사슬을 만듦 | 정의가 곧 화면. 갈래가 몇 개든 대응 | 정의를 걷는 코드가 필요하고, 정의가 이상하면 화면도 이상해짐 | 채택 |
| ④ 단계 목록을 별도 테이블로 관리 | 화면 전용이라 다루기 쉬움 | 프로세스 정의와 이중화 — 둘이 어긋날 자리를 새로 만듦 | 기각 |
②가 제일 흔한 선택일 텐데, 이건 틀린 값을 다른 틀린 값으로 바꾸는 일입니다. 숫자만 맞추고 단계 이름과 역할은 여전히 화면이 알고 있으니까요. 진짜 물음은 "5냐 7이냐"가 아니라 "이 화면이 단계를 안다는 게 맞나"였습니다.
프로세스 정의를 걸어 단계를 만든다
시작 상태에서 승인 전이를 따라가며 사슬을 만듭니다.
@classmethod
def process_steps(cls, process, current_name=None):
"""프로세스가 정의하는 단계 사슬
시작 상태에서 승인 전이를 따라가며 만든다. 반려 전이는 되돌아가는 길이라 건너뛴다.
각 단계의 역할은 그 단계에서 '승인'을 실행할 그룹이다 — 마지막 단계는 실행할
액션이 없으므로 역할이 없다.
신청 건 없이도 부를 수 있다 — 목록 화면의 절차 안내가 이걸로 그려진다.
단계 수는 `default_use_simple_workflow` 에 따라 5개나 7개가 되므로 화면에
박아 두면 안 된다.
"""
start = next((s for s in process.states.all() if s.type == "STR"), None)
if start is None:
return []
approve, _deny = cls._transition_map(process)
chain, seen, state = [], set(), start
while state is not None and state.pk not in seen:
seen.add(state.pk)
entry = approve.get(state.pk)
chain.append({"name": state.name, "role": cls._action_role(entry[1]) if entry else None})
state = entry[0].next_state if entry else None
seen 집합이 있는 건 정의가 순환할 수 있어서입니다. 상태 기계는 반려로 되돌아가는 길이 있으니 승인 전이만 따라가도 잘못 정의된 프로세스에서는 돌 수 있어요. 무한 루프 대신 거기서 멈춥니다.
classmethod인 게 작지만 중요합니다. 신청 건이 없어도 부를 수 있어야 목록 화면의 절차 안내를 그릴 수 있거든요. 처음엔 인스턴스 메서드로 만들었다가, 목록에는 신청 건이 없다는 걸 깨닫고 갈랐습니다.
역할도 같은 걸음에서 나온다
단계마다 "누가 하는 단계인가"를 같이 냅니다.
@staticmethod
def _action_role(action):
"""액션을 실행할 그룹 → 역할 이름"""
target = next(iter(action.action_targets.all()), None)
group_name = target.group.name if target and target.group else None
return STEP_ROLE_LABELS.get(group_name)
그 단계에서 '승인'을 실행할 수 있는 그룹이 곧 그 단계의 담당 역할입니다. 별도로 매핑 표를 두지 않았어요. 권한이 곧 역할이니까, 권한을 바꾸면 화면도 따라옵니다.
마지막 단계는 역할이 None입니다. 실행할 액션이 없으니 담당자도 없어요. 이게 "빠뜨렸다"로 안 읽히게 docstring에 적어 뒀습니다.
같은 출처를 알림도 씁니다. 관계자 카드의 역할과 알림 수신자를 같은 auth.Group에서 읽어요.
역할은 담당자 명단 auth.Group(`관리자그룹`·`위원회그룹`)에서 읽는다 —
알림 수신자를 뽑는 `workflow.notifications._reviewer_emails()` 와 같은 출처다.
화면에 "이 단계는 심의위원회"라고 떴는데 메일은 다른 사람에게 가는 상황을 구조적으로 막는 자리입니다.
dev에서는 맞았다 — 기본값과 저장값이 달랐다
이 버그가 오래 안 보인 이유가 있습니다. 개발 DB의 프로세스 32건이 전부 7단계 사슬이었어요. 그래서 화면의 7단계 안내가 정확했습니다.
# 기본값은 1(간편 5단계)로 둔다 — constance 는 관리자가 저장한 적 없는 키에 이 값을 쓰므로,
# ... 순간 복사되어 기존 건은 그대로라, 한 목록에 5단계와 7단계가 섞인다.
# 7단계로 갈 거면 코드가 아니라 관리 화면에서 값을 명시적으로 저장할 것.
코드 기본값은 **1(5단계)**인데 개발 DB에는 관리 화면에서 저장한 값 **0(7단계)**이 들어 있었습니다. 저장값이 기본값을 이기니까 개발에서는 7단계로 돌았고, 아무도 이상한 걸 못 봤어요.
저장된 적 없는 환경에서만 5단계가 되고 거기서만 화면이 거짓말을 합니다. 그래서 이 결함은 "새로 세운 환경에서만 보이는" 종류였습니다.
그리고 이 설정은 신청 건이 생길 때 프로세스가 복사되는 구조라, 값을 바꿔도 기존 건은 옛 사슬을 그대로 갖고 있습니다. 한 목록에 5단계 건과 7단계 건이 섞여요. 화면이 단계를 몰라야 하는 이유가 여기서 한 번 더 나옵니다 — 목록 하나에도 정답이 둘입니다.
알림은 행동해야 하는 사람에게만, 커밋 뒤에
상태 전이 알림 메일 4종(접수·심사 요청·반려·등록 완료)을 새로 넣었습니다. 두 가지를 정했어요.
첫째, 다음에 행동해야 하는 사람에게만 보냅니다. 전이가 일어날 때마다 관계자 전원에게 보내면 며칠 만에 아무도 안 읽는 메일이 됩니다.
둘째, 발송을 on_commit으로 미룹니다.
통지는 `on_commit` 으로 미룬다 — 호출부가 트랜잭션 안이라 그대로 보내면
롤백된 배정에 대한 메일만 나간다.
메일은 롤백이 안 됩니다. 트랜잭션 안에서 바로 보내면 저장이 실패했을 때 "배정됐습니다"라는 메일만 남고 배정은 없는 상태가 돼요. 받은 사람은 화면에 가서 아무것도 못 찾습니다.
검증
- 5단계·7단계 프로세스를 각각 만들어 목록 절차 안내가 그에 맞게 그려지는 것 확인. 5단계 환경에서 사전인증·인증이 안 나오는 것이 이번 변경의 핵심 확인 항목입니다.
- 신청 건의 현재 상태를 옮겨 가며
is_current·is_done표시가 따라가는 것 확인. - 취소된 건에서 모든 단계가 '지나가지 않음'으로 나오는 것 확인 — 현재 상태가 사슬에 없을 때의 동작입니다.
- 관리자 신청·심사 상세 4곳에서 기안자 마스킹이 적용되고 내부 pk가 안 보이는 것 확인. 사용자 화면은 마스킹하는데 관리 화면 4곳만 빠져 있었습니다.
- 관계자를 배정했다가 저장을 실패시켜 메일이 안 나가는 것 확인.
on_commit없이는 나갑니다. - 변경 범위 66개 파일. 그중 판단이 들어간 건 매니저 메서드 5개와 알림 4종이고 나머지는 화면 재구성입니다.
남은 것 · 한계
- 사슬은 승인 경로만 그립니다. 반려로 되돌아가는 길은 화면에 안 나와요. 이 시스템에는 반려할 수 없는 단계가 둘 있는데 그 사실도 절차 안내에서는 안 보입니다. 신청자가 제일 궁금해할 "반려되면 어디로 가나"가 빠진 셈입니다.
- 취소된 건이 '아직 시작 안 함'과 구별되지 않습니다. 둘 다 모든 단계가 회색이에요. 취소 사실은 다른 자리에 있지만 이 표시만 보면 못 압니다.
- 진행 표시 폭이 7단계에 맞춘 고정값입니다.
7 × 140 + 화살표·간격 150 = 1,130px로 본문 폭 1,152px에 맞췄어요. 단계가 8개인 프로세스가 생기면 넘칩니다. 정의에서 단계 수를 읽게 해놓고 폭은 여전히 7을 알고 있습니다. - 문서의 절차 안내는 아직 고정값입니다. 코드는 정의에서 읽는데
docs/의 설명은 7단계로 적혀 있어요. 경고 문장을 달아 뒀지만 문장은 값을 안 고칩니다. constance기본값 1과 개발 DB 저장값 0의 불일치는 그대로 뒀습니다. 코드 기본값을 0으로 바꾸면 지금 5단계로 도는 환경의 신규 건이 갑자기 7단계가 되면서 목록이 더 섞입니다. 어느 쪽도 깔끔하지 않아 현상 유지에 주석을 붙이는 쪽을 골랐어요.- 절차 안내가 정의와 일치하는지를 회귀로 안 잠갔습니다. 5단계 프로세스를 만들어 단계 수를 단언하는 테스트 한 건이면 되는데, 이번엔 손으로만 확인했습니다.
관련 글: ISO 14817-2 기반 7단계 워크플로우 — 기록부터 배포까지 상태 설계 · 승인될 때까지 아무것도 만들지 않기