근무형태 판정 규칙
직원의 근무형태(근무 그룹 › 근무형태)는 여러 경로로 정해지고, 근태는 그중 어느 것을 쓸지 정해진 순서로 고릅니다. 이 문서는 배정이 정해지는 순서, 근태가 근무를 고르는 순서, 그리고 그 결과가 화면 어디에 어떤 이름으로 보이는지를 한 곳에 적습니다. 코드도 한 곳입니다: 배정은 packages/api/src/utils/membership-shift.ts, 판정은 packages/api/src/services/shift-resolution.service.ts 만 근무를 쓰고 읽습니다.
두 층이 있습니다
| 층 | 무엇을 정하나 | 저장 위치 | 출처 값 |
|---|---|---|---|
| 배정 | 직원에게 어떤 근무가 붙어 있나 | 주 소속 줄 team_members.shiftGroupId/shiftId + 직원 사본 employees.shiftGroupId/shiftId | shiftAssignedVia (배정 출처) |
| 판정 | 그날 근태에 어떤 근무를 쓰나 | 출퇴근 기록 attendance.shiftId + metadata.shiftSource | shiftSource (판정 출처) |
배정은 직원 정보이고, 판정은 날짜마다 다시 합니다. 근무표(간호 스케줄러)가 그날 칸을 채우면 배정과 무관하게 그 근무를 씁니다.
1. 배정이 정해지는 순서
직원이 팀 소속을 얻을 때(등록 · 팀원 추가 · 엑셀 · 시트) 그 소속 줄의 근무는 아래 순서로 정해집니다 (decideJoinShift).
- 직접 준 값 — 등록·수정 화면에서 고른 근무, 엑셀의 근무 열 (
manual·bulk_upload) - 줄에 이미 있는 근무 — 되살린 옛 소속은 그대로
- 직원의 기존 근무를 옮겨 담기 — 이 줄이 주 소속이 되고 직원에게 근무가 있으면 그 값 (
carried, 원래 출처가 있으면 그것을 유지) - 근로형태 기본 근무 — 설정 › 근로형태 (
work_pattern_default) - 직무 기본 근무 — 설정 › 직무 설정 (
job_duty_default) - 팀 대표 근무 — 팀 관리 「대표 근무」 (
team_representative) - 없으면 미지정
팀보다 직무가, 직무보다 근로형태가 앞섭니다. 병원은 같은 팀(통합병동) 안에서도 직무(간호사 · 간호조무사 · 요양보호사)에 따라 근로형태가 갈리고, 한 직무(간호사) 안에서도 근로형태(통합병동 · 나이트 전담 · 외래)마다 근무가 다르기 때문입니다.
소속을 거치지 않고 「직원의 근무」를 바로 바꾸는 경로도 있습니다. 모두 주 소속 줄과 직원 사본을 함께 쓰고 출처를 남깁니다.
| 경로 | 화면 | 출처 값 |
|---|---|---|
| 직원 정보 수정의 근무 배정 | 관리자 › 직원 관리 | manual |
| 일괄 배정 툴바 | 관리자 › 직원 관리 · 팀 관리 | bulk_assign |
| 직원 마스터 시트 「근무 그룹」「근무형태」 열 | 급여 › 시트 | sheet |
| 근무 시간 자기 선택 | 직원 앱 › 더보기 › 근무 시간 설정 | self |
| 마이그레이션 이전 데이터 | — | migration |
직원 사본과 주 소속 줄은 항상 같습니다. 주 소속이 바뀌면 그 줄의 근무와 출처가 직원 사본으로 복사되고, 반대로 빈 주 소속 줄에는 직원 사본이 옮겨 담깁니다 (syncPrimaryMembership). 근태 · 연차 · 급여 시트는 직원 사본만 읽습니다.
2. 근태가 근무를 고르는 순서
출퇴근 기준 시각을 정할 때(앱 출근 · 관리자 일괄 입력 · 지문 단말기(세콤))와 직원 앱 「오늘 근무」, 연장근무의 휴일 판정이 같은 순서를 씁니다 (resolveEffectiveShift).
- 그날 근무표 칸의 근무형태 — 간호 스케줄러 (
schedule) - 직원에게 배정된 근무형태 (
employee_shift) - 근무 그룹만 배정된 직원 — 그날 요일에 적용되는 그룹 형태가 하나면 그것, 여럿(3교대)이면 출근 시각에 가장 가까운 형태 (
group_shift). 그날 적용되는 형태가 없으면 쉬는 요일 (off_day) - 직원 배정이 아예 없으면, 배정이 골랐을 기본 근무를 같은 순서로 — 근로형태 기본 근무 (
work_pattern_default) → 직무 기본 근무 (job_duty_default) → 주 소속 팀의 대표 근무 (team_representative). 기본 근무가 그룹만이면 3번처럼 그날 요일 형태를 고릅니다(그날 형태가 없으면 쉬는 요일) - 조직 기본 근무형태 (
org_default_shift) - 조직 기본 기준시간 — 근무형태 없이 기준 시각만 (
org_default_time)
4번은 1번 절의 배정 순서 4·5·6과 같은 단계입니다(2026-09-22). 배정은 소속이 생길 때 기본값을 직원에게 써 두지만, 기본값이 생기기 전에 등록된 직원이나 배정을 비운 직원은 직원 값이 비어 있습니다. 그때 근태가 조직 기본으로 떨어지지 않고 배정이 골랐을 값을 그대로 고르며, 출처에 「직무 기본 근무」처럼 어느 단계였는지 남습니다. 직원 값이 있으면(형태든 그룹만이든) 기본 근무는 보지 않습니다 — 배정을 바꾸려면 직원 정보 수정에서 바꿉니다.
지문(세콤)도 위 순서 그대로입니다(2026-09-22). 예전에는 자동 추정이 끼는 별도 순서(재계산의 이전 기록 → 근무표 칸 → 같은 날 기존 기록 → 자동 추정 → 직원 근무형태 → 그룹 → 조직 기본)를 썼는데, 같은 직원·같은 날에 앱 출근과 지문이 다른 근무를 고를 수 있어 없앴습니다. 지문 시각으로 근무를 고르는 것은 근무 그룹만 배정된 3교대 직원의 3번 단계뿐이고, 직원에게 배정된 근무형태가 있으면 지문 시각과 무관하게 그 형태입니다(그날 다른 근무를 했다면 근무표에 지정합니다). 배정 형태의 쉬는 요일에 찍힌 지문은 형태 없이 받아 요일별로 계산하고, 근무표 칸은 쉬는 요일이어도 적용합니다. 예전 지문 기록에 남은 inferred(지문 자동 추정) · previous(이전 기록 유지) 출처는 표시용으로만 남습니다.
지문 재계산(근무표 우클릭 「근태 재계산」)도 날짜마다 다시 판정합니다. 예전에는 지운 기록의 근무를 되살려 썼기 때문에 직원 배정을 바꿔도 재계산에 반영되지 않았습니다. 관리자가 자동 기록의 근무를 손으로 바꿔 둔 날은 재계산이 판정값으로 되돌리니, 그런 날은 수정 창에서 수동 입력으로 두거나 근무표에 지정합니다.
출근 기록이 이미 있는 날은 그 기록에 남은 근무를 씁니다. 판정은 기록을 만들 때 한 번이고, 퇴근·수정은 기록의 근무를 다시 정하지 않습니다.
3. 화면에서 보이는 곳
| 화면 | 보이는 것 |
|---|---|
| 직원 앱 › 근태 홈 「현재 상태」 | 근무형태 · 시간 아래에 「근무표 배정」「직원 근무 배정 · 직무 기본 근무」처럼 판정 출처와 배정 출처 |
| 관리자 › 근태 현황 › 직원별 근태 창의 근무 열 | 근무형태 배지 · 기준 시각 아래에 판정 출처 |
| 관리자 › 직원 정보 수정 › 근무 배정 | 소속 줄마다(소속이 없으면 직원 단위 칸 아래) 「배정 출처: 직무 기본 근무」. 근무가 비어 있으면 「근태는 직무 기본 근무 「…」으로 판정돼요」, 개별 지정이 기본과 다르면 「직무 기본 근무 「…」 ≠ 개별 지정 「…」」 안내와 「직무 기본으로 맞추기」 버튼 |
| 관리자 › 직원 관리 목록 근무형태 칸 | 개별 배정이 있으면 그 이름(툴팁 = 배정 출처). 배정이 비었거나 그룹만이면 근태가 실제로 쓰는 직무 기본 근무를 회색 「기본」으로, 개별 배정이 기본과 다르면 「≠ 직무 기본」 표시 |
| 관리자 › 직무 설정 | 기본 근무가 없는 직무 「없음」, 그룹만인데 스케줄러 그룹이 아니면 「유형 미지정」 경고. 「기본 근무 재적용」 = 아래 5절 |
| 관리자 · hr-system › 근태 수정 창의 듀티(3교대) 표시 | 서버 판정(attendance.effectiveShift)의 근무형태가 듀티면 휴일 4칸을 합산해 숨김 |
라벨 정본은 @hereby/core SHIFT_RESOLUTION_SOURCE_LABELS · SHIFT_ASSIGNMENT_SOURCE_LABELS 입니다.
5. 직무 기본 근무를 직원에게 맞추는 때
직무(또는 근로형태)가 있는 직원은 근무형태도 그 기본 근무를 따라야 합니다(2026-09-22 결함 — 직무 백필로 직무만 들어온 직원 150여 명이 「그룹만」으로 남아 근태가 출근 시각에 가까운 다른 형태를 붙였습니다). 한 규칙(services/job-duty-shift-sync.service.ts decideDefaultShiftReapply)을 네 경로가 씁니다.
| 경로 | 언제 |
|---|---|
| 직원 정보 수정 | 근무를 직접 바꾸지 않고 직무·근로형태만 바꿨을 때 |
| 엑셀 대량 수정 | 직무·근로형태 열이 바뀐 행(템플릿에 근무 열이 없다) |
| 직원 마스터 시트 커밋 | 「직무」·「근로형태」 셀만 바뀐 행(근무 열이 없을 때) |
| 직무 설정 › 「기본 근무 재적용」 | 직무가 있는 직원 전체(또는 한 직무)를 미리보기 뒤 일괄 적용 — 배포 후 정리용 |
바꾸는 경우: 근무가 비어 있음 · 그룹만 배정(기본 그룹과 같고 기본에 형태가 있을 때) · 배정된 형태나 그룹이 비활성 · 배정이 기본 근무(근로형태·직무·팀 대표)에서 왔는데 지금 기본과 다름. 두는 경우(오버라이드): 직접 선택 · 일괄 배정 · 엑셀 대량 추가 · 시트 · 직원 앱 자기 선택 · 이전 데이터(migration) · 기존 근무 유지(carried)로 활성 형태가 온전히 배정된 값. 출처를 모르는 값을 조용히 덮지 않기 위해서이고, 화면이 「≠ 직무 기본」으로 보여 줍니다. 재적용 미리보기는 이 인원을 「직접 고른 근무가 기본과 다른 직원 N명」으로만 알립니다.
기본 근무 자체는 근로형태 기본 근무 → 직무 기본 근무 순서이고(1·2절과 같다), 그룹이 비활성이면 기본 없음, 형태가 비활성·그룹 밖이면 그룹만입니다. 적용된 배정의 출처는 「근로형태 기본 근무」·「직무 기본 근무」로 남습니다.
4. 날짜 없는 판정 — 기간 단위로 읽는 경로
다음 경로는 날짜 단위 판정이 아니라 기간 단위로 「이 직원이 쓰는 근무형태(들)」를 읽습니다. 2026-09-22 부터 같은 서비스의 loadAssignedShiftCandidates 하나를 씁니다 — 순서는 2번 절에서 근무표 칸만 뺀 것입니다: 직원 배정 근무형태 → 직원 근무 그룹(활성 형태 전부) → 근로형태 기본 근무 → 직무 기본 근무 → 주 소속 팀 대표 근무 → 조직 기본 근무형태 → 없음. 형태까지 정해졌으면 하나, 그룹만이면 그 그룹의 활성 형태 전부가 후보입니다.
- 연차 차감의 근무패턴 (
leave-ledger.service.tsloadWorkPattern) — 후보가 형태 하나면 그 형태의 패턴, 그룹이면 형태들의 보수적 합집합(하나라도 듀티면 듀티 · 요일은 합집합 · 공휴일 근무는 OR). 근무표는 기간 안에 칸이 있는지 여부만 따로 봅니다(hasDutySchedule). - 1일 소정근로시간 (
employee-daily-standard.service.ts) — 후보가 형태 하나면 그 근무시간, 그룹이면 형태들의 근무시간이 하나로 모일 때만 그 값, 아니면 조직 설정workHours.dailyStandard(기본 8시간). 예전엔 조직 기본 근무형태와 기본 근무 셋을 보지 않았습니다. - 휴가 유형의 허용 근무형태 판정 (
leave-request.router.ts) — 후보 중 하나라도 유형의 허용 목록에 들면 신청 가능. 후보가 없으면 막지 않습니다. - 관리자 · hr-system 의 근태 수정 창 듀티 판정 — 화면이 자체 판정하지 않고
attendance.effectiveShift(2번 절의 날짜 판정, 오늘 기준)를 불러 그 근무형태의 연차 차감 방식이 듀티인지로 정합니다.