schemalism
PART 01·of 04

동료를 본다는 어색함 — BBS 카드를 마스터·사업장·관찰 3단으로 쪼갠 이유

마스터 카드 하나를 모두에게 쥐어주면 의미가 없고, 사업장마다 처음부터 만들면 표준이 무너집니다. core / org_site / obs 3단 분리, 그리고 관찰이 카드를 '참조'가 아니라 '복사'하게 만든 snapshot 사상.

2026년 8월 2일 · 정윤환 · 6

BBS(Behavior-Based Safety, 행동 기반 안전)는 한 줄로 말하면 “동료가 동료를 본다” 입니다. 관리자가 작업자를 감독하는 게 아니라, 같이 일하는 사람이 같이 일하는 사람의 행동을 관찰합니다.

이게 종이 체크리스트로 운영될 때는 단순합니다. 양식을 인쇄해서 들고 다니며 체크하고, 끝나면 사무실에 제출합니다. 그런데 이걸 디지털화하는 순간, 누구의 카드인가 라는 질문이 생깁니다.

  • A 화학공장은 “안전모, 보안경, 내화복” 을 핵심 행동으로 봅니다.
  • B 물류센터는 “지게차 후진 경광등, 적재함 결속, 무리한 들기” 가 핵심입니다.
  • 같은 회사 안에서도 압출 라인과 포장 라인은 봐야 할 행동이 다릅니다.

마스터 카드 하나를 모두에게 쥐어주는 건 의미가 없습니다. 그렇다고 사업장마다 카드를 처음부터 만들면 표준이 무너집니다.

01 · Three tiers

그래서 카드를 3단으로 쪼갰습니다

core → org_site → obs
위는 살아 움직이고, 아래는 그날에 고정된다
tree
bbs/core  (마스터 템플릿, 어드민이 관리)
   │
   │ "선택해서 사업장에 배포"
   ▼
bbs/org_site  (사업장 배포 카드, 사업장 관리자가 커스터마이징)
   │
   │ "현장에서 관찰자가 사용"
   ▼
bbs/obs  (관찰 1건의 행동 평가 기록, 평가 결과가 스냅샷됨)
  • bbs/core — 마스터 템플릿. 우리(플랫폼) 어드민이 관리합니다. 산업/도메인 단위의 표준 행동 카드와 행동 항목. 고객사와 무관.
  • bbs/org_site — 사업장 배포 카드. 사업장 관리자가 마스터 중에서 골라와 자기 현장에 맞게 다듬습니다. 행동 항목의 이름·설명·이미지를 사업장 맥락으로 오버라이드하고, is_selected 로 안 쓸 행동은 꺼둡니다.
  • bbs/obs — 관찰 1건. 관찰자가 동료를 본 결과. 각 행동마다 SAFE/UNSAFE 평가가 붙고, 사진·메모·위험등급이 함께 기록됩니다.

3단이 다 필요한 이유는 단순합니다. 한 단을 빼면 다른 두 단이 깨집니다. core 가 없으면 사업장마다 카드를 0 에서 만들고, org_site 가 없으면 마스터를 다 같이 써야 하고, obs 가 없으면 그날 그 관찰이 무엇이었는지 시간이 지나면 알 수 없습니다.

02 · Snapshot, not reference

가장 결정적인 한 줄 — 관찰은 카드를 “복사” 한다

핵심은 obs 가 org_site 의 카드 행동을 참조 하지 않고 복사 한다는 점입니다.

python
class BbsObsBehavior(Base):
    bbs_org_site_card_behavior_id = Column(
        String(36),
        nullable=True,
        index=True,
        comment='평가 대상이 된 사업장 카드 행동 ID. 모듈 간 FK 미설정. '
                '카드 행동이 삭제되어도 관찰 기록은 보존 (snapshot 식)'
    )

    name = Column(String(120), nullable=False,
                  comment='행동 명칭. 관찰 체크리스트에 표시')
    description = Column(Text, nullable=True,
                  comment='행동 설명')
    thumbnail_image_filepath = Column(Text, nullable=True,
                  comment='행동 예시 이미지 썸네일 경로 (snapshot). 카드 행동에서 복사')
    original_image_filepath = Column(Text, nullable=True,
                  comment='행동 예시 이미지 원본 경로 (snapshot). 카드 행동에서 복사')

ID 연결은 조회 편의 일 뿐, 행동의 이름·설명·이미지 자체가 obs_behavior 에 함께 박힙니다. 6개월 뒤 사업장 관리자가 그 카드를 정비하면서 행동 이름을 바꿔도, 작년 가을의 관찰 기록은 그때 그 이름 그대로 남습니다. 카드는 살아 움직이지만, 관찰은 그날에 고정됩니다.

이 분리가 BBS 데이터를 측정 가능한 시계열로 만듭니다. “작년 3분기 우리 사업장에서 SAFE 비율이 어땠지?” 는 그때 카드가 어떻게 생겼었는지가 아니라, 그날 관찰자가 무엇을 보고 어떻게 평가했는지에 대한 질문입니다. 스냅샷 정책이 없으면 그 질문 자체가 깨집니다.

03 · Card kinds

카드 유형이 또 갈라지는 이유 — TASK 만 다르다

대부분의 카드는 행동 묶음 입니다 — GENERAL_BEHAVIOR (일반 안전 행동), CRITICAL_RULES (생명규칙), PPE (보호구). 사업장 관리자가 만들고, 모든 관찰자가 사용합니다.

TASK 카드만 두 가지가 더 갈라집니다.

tree
TASK
├── OBSERVED (관찰작업): 카드와 독립적으로 존재 가능한 작업
└── STANDARD (표준작업): 카드와 1:1로 만들어지는 작업, 작업 절차 자체를 표현

표준작업의 경우 카드 안에 작업 순서·작업 내용·비고 라는 세 컬럼이 추가됩니다.

python
class BbsOrgSiteCardBehavior(Base):
    ...
    standard_work_step    = Column(String(50), nullable=True, comment='표준작업 순서')
    standard_work_content = Column(Text,       nullable=True, comment='표준작업 내용')
    standard_work_remarks = Column(Text,       nullable=True, comment='표준작업 비고')

같은 테이블에 컬럼이 더 붙는 게 부담스러워 보이지만, 표준작업도 결국은 “이 순서대로 행동을 한다” 의 묶음이라 본질은 행동 카드와 같습니다. 표를 따로 만들면 화면이 둘로 갈라지고, 그러면 “이건 관찰입니까, 절차입니까?” 라는 운영 혼란이 시작됩니다. 한 테이블에 두 의미를 담는 부담보다 두 테이블로 갈라지는 혼란이 더 크다고 판단했습니다.

04 · Permission follows kind

누가 카드를 만들 수 있는가도 카드 유형이 정한다

  • 일반 카드 (GENERAL_BEHAVIOR / CRITICAL_RULES / PPE) — 사업장 관리자만 만듭니다. 사업장 전체에 깔리는 표준이라.
  • 작업관찰 카드 (TASK) — 모든 멤버 가 만들 수 있습니다. “내 작업에 대한 관찰 카드” 는 그 작업을 가장 잘 아는 작업자 본인이 만드는 게 맞습니다.

권한은 데이터 모델에 박혀있지 않습니다. 서비스 레이어에서 카드 유형을 보고 분기합니다. 카드 유형은 enum 한 칸이지만, “누가 만드는가” “무엇을 위한 카드인가” 가 함께 결정됩니다.

Next

다음 편 — 그 관찰 1건의 행동 한 줄에 왜 40개 컬럼이 박혔는지 — 통계 빠르기보다 더 중요한 “그때 그 모습의 보존” 이라는 사상.