# SuperFID 워커 앱 — 기능명세서 (SPEC)

> 화면별 정책·로직·분기 정의. 디자인 토큰·컴포넌트 규칙은 `DESIGN.md`.
> **스펙 정본 = superfid-wiki** (`github.com/superfid/superfid-wiki`) — `pages/product/spec/worker-app-ux-policy-2026-07.md`(앱 정책) · `worker-app-dev-spec-2026-07.md`(개발 명세: 도메인 모델·API·AC) · `inventory-core-policy-2026-07.md`(재고 코어). 유스케이스 원천 = GDrive `02_유스케이스/`(xlsx+카탈로그). 충돌 시 wiki가 우선.
> 최종 갱신 2026-07-20 · 화면 세트 53장 기준.

## 한눈에

**SuperFID 워커 앱** — 현장 작업자용 RFID 재고관리. 리더기(CF-H103, RFID+바코드 겸용)로 세고, 없으면 수동으로도 전 작업 가능.

1. 재고의 진실은 **말단 위치(구역)**에만 있다 — 부모는 읽기 전용 합산.
2. 동사는 **6개뿐**: 입고 · 반품 · 출고 · 판매 · 조사(구역 전체 맞춤) · 조정(한 상품 맞춤).
3. 온보딩은 **브랜드 코드 퍼스트**(브랜드당 1개) — 코드 → 브랜드 확인 → 이메일 OTP → (신규만) 이름·역할 → 매니저 승인.
4. 컨텍스트 선택은 **한 바텀시트 2단계 드릴인** (위치 → 구역) — 풀스크린 전환 금지.
5. 실패는 시끄럽게, 내부 식별자(코드·EPC)는 워커에게 절대 노출 금지.
6. **재고 요청 = 원장 밖 체크리스트** — 요청은 재고를 바꾸지 않고, 실물 이동은 항상 입고/출고로만 기록한다.

## 정책 요약표 (상세는 아래 §0~§10)

| 주제 | 규칙 한 줄 |
|---|---|
| 위치 계층 | 위치 > 구역(말단). 구역은 별도 엔티티가 아니라 자식 위치이며, 구역 있는 위치는 **구역 선택 강제** |
| 입력수단 | RFID·바코드=리더기 / 수동=검색+수량(+폰 카메라 단건 조회) |
| 폰 카메라 | 직접 추가에선 **상품 특정만**(수량=스테퍼) / 조정 수동에선 **"바코드 찍어서 세기"**(선택 SKU만 1개당 +1) |
| 카운터 | 라이브=고유 태그(EPC dedup)+총 읽힘, 수동 행 제외 / 확정 내역 N개=EPC+수동 합 |
| 조사 | 업로드 → **확정 화면에서만 확정**(2단계). 누락은 확정 시점에만 처리 |
| 조정 | **상품 선택 먼저** → RFID\|바코드\|수동. 세기 중 선택 SKU만 반영(타 상품 무시). 대편차=셀프 확인 1회 |
| 미등록 태그/바코드 | **알림만** — 푸터 상태 라인 "미등록 N건 · 재고 반영 안 됨". 워커 처리 없음, 해소=관리자 웹 |
| 리더기 끊김 | `재연결` / `수동으로 계속` 2택 (리더기 끊기면 바코드도 불가) |
| 오프라인 | 자동 저장 + 업로드 큐 + 멱등 재시도. 홈 조건부 배너로 진입 |
| 재고 탭 | 조회 전용. 기본=현재 위치 전체, 스코프 전환=시트 2단계(구역 행에 수량) |
| 탭 구성 | 하단 4탭 = **작업 · 재고 · 요청 · 설정** — 리더기는 탭이 아니라 작업 홈 우측 칩·설정 행에서 진입 |
| 재고 요청 | 원장 밖 체크리스트 — 상태 6종, 수정=접수 전만·취소=활성 내내, 전체 보드=인지용(중복 차단 없음), 알림 뱃지 없음 |

## 코어 플로우 맵

```

## 0. 핵심 모델

- **위치 계층**: 위치 > 구역(말단). 구역은 별도 `section` 엔티티가 아니라 `parent_id`가 있는 자식 위치다. **재고 진실은 말단에만 존재**, 부모는 읽기 전용 합산.
- **기본 구역 상시**: 모든 위치는 생성 시부터 `기본 구역`을 가진다. 작업하려면 구역이 하나뿐이어도 반드시 구역을 선택하며 상위 위치는 항상 읽기 전용 합산이다.
- **서버 판정 우선**: 작업 선택 가능 여부는 `GET /v1/locations`의 `isLeaf && selectable`을 따른다. `childCount`나 클라이언트 추론으로 이를 덮어쓰지 않는다.
- **API 모델**: `parentId=null`이면 상위 위치, 값이 있으면 구역이다. `parentName`, `isLeaf`, `childCount`, `selectable`을 함께 사용하며 별도 `sectionId`나 `/sections` endpoint는 없다.
- **6동사 → 이벤트 코드** (코드는 UI에 절대 노출 금지):
  | 동사 | 코드 | 정의 |
  |---|---|---|
  | 입고 | IN_RECEIVE | 상품을 들여옴 |
  | 반품 | IN_RETURN | +N. 판매와 연결하지 않음 |
  | 출고 | OUT_ETC | 이동·폐기·협찬 등 판매가 아닌 나감 (목적지 추적 없음) |
  | 판매 | OUT_SALE | 판매 집계 포함 |
  | 조사 | SET_AUDIT | **위치/구역 스코프** — 전부 세어 절대값 덮어쓰기, 미스캔=누락 처리 |
  | 조정 | SET_MANUAL | **SKU 스코프** — 한 상품을 세거나 입력해 절대값 맞춤 |
  - 조사≠조정 = 스코프 차이. 둘 다 입력수단은 RFID/바코드/수동 모두 가능. 조정은 작업자 허용, 사유 선택(옵션), 대편차 시 확인.
- **입력 수단 3종**: ① RFID(리더기) ② 바코드(리더기 — H103은 바코드 스캐너 겸용) ③ 수동(검색+수량, 폰 카메라 바코드 단건 조회 포함). **폰 카메라 연속 스캔 모드는 없다** — 바코드 연속 스캔은 리더기 전용, 카메라는 수동 추가의 상품 특정용 단건.
- **카운터 시맨틱 (잠금)**: 라이브 스캔 = `고유 태그`(EPC dedup) + `총 읽힘`(raw). 수동 추가 행은 태그 카운터에 포함하지 않음. 확정된 내역의 "N개" = 저장통 합(EPC+수동). 예: 고유 1,284 + 수동 6 → 내역 1,290개.
- **계정/역할 = 포함 관계(관리자 ⊇ 매니저 ⊇ 작업자)**: 관리자=웹 전용 권한(결제·조직·롤백) 보유, **앱에서는 매니저와 동일 동작**(앱에 관리자 전용 기능 없음 — 앱은 is_manager만 판단). 매니저=앱 설정탭 연결 요청 승인 + 작업 전부. 작업자=앱 작업. 계정 생성: 관리자=워크스페이스 개설(웹·Stripe) / 작업자=앱 셀프 가입(브랜드 코드→승인) / 매니저=관리자가 웹에서 멤버 승격. 로그인 = 개인 이메일 + OTP 6자리(전 역할 공통).

## 1. 온보딩 (코드 퍼스트)

`브랜드 코드(첫 화면) → 브랜드 확인+이메일 → 인증코드 → [분기] → (신규) 내 정보 → 승인 대기`

| 단계 | 정책 |
|---|---|
| 브랜드 코드 | **브랜드(계정)당 1개** 6자리, 점장·관리자에게 사전 전달. 코드는 비밀 아님(보안=매니저 승인). 승인 = 브랜드의 매니저 누구나(근무지는 역할 한 줄로 참고). 오입력 방어 = 다음 화면의 브랜드 확인 카드 |
| 브랜드 확인+이메일 | 코드가 가리키는 브랜드 카드 표시. 개인 이메일 입력 → OTP 발송 |
| 인증코드 | 6자리 OTP, 재발송 타이머. **분기**: 이 워크스페이스에 이미 연결된 계정 → 바로 작업 홈 / 신규 → 내 정보 |
| 내 정보 | 이름(작업 기록용)·역할(자유 한 줄, 승인 참고용) 모두 필수. CTA=연결 요청 |
| 승인 대기 | 매니저가 앱 설정 탭에서 승인. 승인 시 푸시 → 작업 홈. 탈출구: "다른 코드로 다시 요청" |
| 재로그인 | 앱이 세션·워크스페이스 기억. 코드 화면은 첫 설치/로그아웃 시에만 |

## 2. 작업 탭

- **홈**: 히어로(킥커=위치, 타이틀=구역˅) + 리더기 칩(탭 → 리더기 화면) + 6동사 타일(2×3 균등, 위계 없음) + 최근 작업 + 조건부 배너.
- **위치/구역 선택 = 한 바텀시트 2단계 드릴인**: 위치 → 같은 시트의 구역 리스트(기본 구역 포함, ‹ 뒤로). 구역이 하나뿐이어도 위치를 작업 대상으로 즉시 적용하지 않는다. 풀스크린 전환 금지.
- **빈 구역(재고 0·첫 활성화 전)**: 홈이 빈 상태로 표시 — "첫 조사 시작" / "입고 시작" 2버튼.
- **업로드 대기 배너**: 대기·실패 건이 있을 때만 홈에 노출 → 업로드 대기 화면. (업로드 실패 시에도 자동 표시)

## 3. 동사 공통 (입고·반품·출고·판매)

- 앱바: ‹ 동사 + 위치 칩(항상 풀 표기 `위치 · 구역`, 말줄임 허용).
- 모드 토글 RFID|바코드(리더기). RFID 모드에만 `읽는 범위` 프리셋: **단품/박스/선반** (신호세기 매핑, 바코드 모드에선 숨김).
- 스캔 상태 칩 = `● H103 · 스캔 중` (리더기 표시는 여기, 앱바 아님).
- 목록: 회색 이니셜 서클 + 상품명(2줄 클램프) + 수량 + 행 삭제. 수동 추가 행 = `수동` 칩.
- **직접 추가**: 항상 노출되는 버튼(모드 아님) → 시트(SKU 검색 + 수량 스테퍼 + 담기). 검색창 스캔 아이콘 → **폰 카메라 단건 조회**(같은 시트 2단계 — 대기/인식 2상태, 풀스크린 전환 아님). 카메라는 상품 특정만: 1회 인식, 동일 바코드 재스캔=무동작, 다른 바코드=결과 교체(수량 리셋). 수량은 스테퍼로만(반복 찍기 카운트 금지 — 연속 스캔 모드 반려와 일관).
- **미등록 태그/바코드** = **알림만** — 푸터 상태 라인 "미등록 N건 · 재고 반영 안 됨"(정보성, 탭 액션 없음) + 작업 내역 상태 표기. 워커가 할 처리 없음(카운트·재고 미포함이므로 제외도 불필요). 태그↔상품 연결·해소는 전부 관리자 웹. (Tony 2026-07-11: 확인 필요 화면 폐기)
- 자동 저장. CTA `업로드` → 완료 화면(작업명·반영 수량 + 입력수단별 집계 RFID·바코드·직접 추가 + 미등록(별도, 미반영) · 숫자 천단위 대응).

## 4. 조사 · 조정

- **조사**: 선택한 구역 전체를 세어 덮어쓰기. 흐름 = 세기 → `업로드` → **조사 확정 화면에서만 `확정`** (이중 확정 금지). 확정 화면 = 정의 리스트 카드(차이 −N개 크게 / 현재 재고 / 맞춰질 수량) + 사유 칩. 미스캔분은 확정 시 누락 처리(확정 전에는 처리 안 함). 사유 칩: 판매누락/이동/폐기·분실/스캔 누락/불명/선택 안 함.
- **조정**: **상품 선택이 먼저** (검색+바코드로 찾기 → 상품 행 선택) → 세기 화면(RFID|바코드|수동 토글, 바꾸기=재선택). 수동=스테퍼 "N개로 맞춤" + 보조 "바코드 찍어서 세기"(선택 SKU만 1개당 +1, 다른 상품 바코드는 세지 않음 — 리더기 없는 낱개 세기 수단; 여러 상품 연속 스캔과 다름). 대편차 시 확인 시트("N개 감소합니다. 맞습니까?" + 사유 추가).
- **SKU 필터(잠금)**: 조정 세기 중 리더기가 읽는 것 중 **선택 SKU의 태그/바코드만 카운트에 반영**, 다른 상품 관측은 무시(카운트·재고·위치 어디에도 미반영 — 타 태그 위치 갱신은 정식 스캔 작업에서만).

## 5. 재고 탭 (조회 전용)

- **기본 화면 = 현재 작업 위치의 전체 구역 상품 리스트** + 검색(라이브 필터) + 바코드 찾기.
- 스코프 전환 = 작업 탭과 동일한 시트 2단계: 위치(단일 선택 + `전체 위치` 옵션) → 구역(행에 구역별 수량 표시, `전체 구역=합산` 포함). 구역별 소계는 시트 행이 겸함(별도 화면 없음).
- 쓰기 없음. 수량은 말단 합산 원칙으로 상위=하위 합과 항상 일치해야 함.

## 6. 재고 요청 탭 (원장 밖 체크리스트)

- **본질**: 요청은 재고를 절대 바꾸지 않는다 — 투두 체크리스트. 실물 이동은 보내는 쪽 출고 / 받는 쪽 입고로만 기록되며, 요청과 어떤 연결도 만들지 않는다(요청↔입고 매칭 금지). 요청 수량 10 ≠ 실제 보낼 수량(7이든 20이든 관리자 자유).
- **단위**: 요청 항목 = SKU 1개 + 수량 + 대상 위치(=**제출 시점**의 작업 위치로 고정 — 이후 작업 위치를 바꿔도 불변, **위치 레벨 — 구역 아님**) + 요청자 + 시각. SKU·위치·요청자는 id로 보관하되 **표시명은 제출 시점 스냅샷 라벨** — 이후 삭제·개명·퇴사에도 마지막 이름 그대로 표시(전파 없음, 정리는 관리자 웹). 여러 SKU를 담아 보내면 항목 N개가 생성된다(제출 그룹 id만 데이터로 보존 — 관리자 웹 일괄 처리용, 앱 UI는 평평한 행). **제출 그룹 id의 용도는 관리자 웹에서 여러 항목의 상태를 한 번에 바꾸는 것뿐이며, 출고 단위·배송 건·이행 단위로 사용하거나 별도 상태·필드를 붙이지 않는다.**.
- **상태 6종 어휘 = 업무체** (Tony 2026-07-20 확정 — 구 어휘 대기/보낼 예정/보류/보냄/안 보냄/취소됨 폐기): 혼동 2건을 수리했다 — ① 대기↔보류가 둘 다 "기다림"으로 읽혀 *관리자 미확인* vs *보고서 결정 보류*가 구분되지 않던 것 → **접수 전 / 검토 중** ② 보냄↔안 보냄이 한 글자 차이라 리스트 스캔 시 오독되던 것 → **발송 / 발송 불가**.
- **상태 6종** (변경 주체 = 관리자 웹, 예외는 명시): 접수 전 → 발송 예정(예정일 옵션 — 없으면 행 날짜 칩·상세 예정일 행 생략, 상태 라벨만) / 검토 중 / 발송 / 발송 불가. + 취소(작업자).
  - **관리자 전이 자유**: 관리자는 활성 요청(접수 전·발송 예정·검토 중)을 관리자 상태 4종 어디로든 바꿀 수 있다(검토 중→발송 예정, 발송 예정→발송 불가 등). **종결(발송·발송 불가·취소)은 최종 — 재오픈 없음**, 필요하면 새 요청.
  - 활성(=전체 보드 노출) = 접수 전·발송 예정·검토 중. 발송·발송 불가·취소 = 종결.
  - **시간 경과 자동 전이 없음.** 예정일 지남 = amber 표기뿐 — 전용 행위 없음("다시 요청" 버튼 기각, Tony 2026-07-20: 더 필요하면 상시 노출된 요청하기로).
  - **작업자 수신확인 없음 (Tony 2026-07-20 확정 — 크루 제안 "받았어요" 기각).** 실물이 도착하면 어차피 입고 처리가 일어난다 — 요청 종결은 관리자(발송/발송 불가)만 한다. "받았어요"류를 추가하면 경합·오조작 유스케이스가 급증(단순함 원칙 위반). 작업자가 할 수 있는 유일한 행위 = **취소(메모 철회)**. 관리자가 발송 표시를 잊은 좀비 요청 = 관리자 웹의 보드 위생 문제이지 작업자 앱 기능이 아니다.
  - **수량 수정 = 요청자 본인이, 접수 전에서만** — 관리자는 수량을 고치지 않는다(몇 개를 보내든 자유이므로 고칠 필요가 없다). 관리자가 확인한 수량이 뒤에서 바뀌면 그게 진짜 혼란.
  - **취소 = 활성(접수 전·발송 예정·검토 중) 내내 가능** (Tony 2026-07-20). 시스템 규칙 = "종결 전까지"; UI 문구 "발송 전까지"는 그 근사(작업자에게 더 직관적). 경합은 먼저 종결한 쪽이 이긴다(관리자 발송/발송 불가 선행 → 취소 무효·무알림). **모든 워커 액션(수량 저장·취소)은 서버 현재 상태 기준으로 판정** — 시트를 연 사이 상태가 바뀌었으면 액션은 무시되고 시트가 현재 상태로 조용히 새로고침된다(에러 알럿 없음). 디커플링 덕분에 어느 쪽이 이겨도 무해 — 이미 떠난 실물은 입고로 처리하면 그만. 취소 = 종결이므로 활성 목록(내 요청·전체 보드)에서 즉시 제외되고 이전 요청에 남는다(같은 규칙, 예외 없음).
  - **오래된 활성 = 접기(표시 전용, Tony 2026-07-20)**: 제출 후 **30일**이 지난 활성 요청은 내 요청·전체 보드 각각에서 "오래된 요청 N건" 익스팬더로 접는다(이전 요청과 같은 문법·같은 숫자 — 앱의 시간 규칙은 **30일 하나뿐**). **상태는 바뀌지 않는다** — 여전히 활성이고, 펼치면 평소와 동일한 행이며 취소도 그대로 된다(수량 수정은 원래대로 접수 전에서만). 관리자가 종결하면 접힌 그룹에서 곧바로 이전 요청으로 이동한다. **compose의 같은 SKU 중복 힌트는 접힘 여부와 무관하게 데이터 기준으로 판정**한다(표시 목록으로 계산 금지 — 접힌 요청도 힌트에 잡혀야 재요청 증식이 막힌다). 시간 경과로 인한 상태 전이는 여전히 없다.
  - 🔴 **나이 표현은 이 접기 하나뿐**: 회색조·정렬 강등·경과일 칩·별도 마커 등 활성 요청의 나이를 드러내는 다른 장치를 만들지 않는다(7번째 준상태 방지). 관리자가 스스로 선언한 예정일의 amber "지남"만 예외 — 그건 나이가 아니라 관리자 자신의 약속과의 비교다.
  - **누적의 소유자 = 관리자 웹**: 접기는 워커 리스트의 신호/잡음만 지키는 표시 규칙이며, 좀비 요청 자체를 줄이는 건 관리자 웹 보드 위생의 책임이다. 워커 앱은 이를 경보·집계하지 않는다(워커의 유일한 대응은 취소).
  - 🔴 **관리자 웹으로 넘기는 요구사항**: **검토 중은 시계가 없는 유일한 상태**다(접수 전=큐의 머리라 압력을 받고, 발송 예정=예정일이라는 시계가 있다). 검토 중은 "지금 결정하지 않겠다"는 결정 자체여서 되살릴 트리거가 명세에 없다 — 관리자 웹 보드에 **검토 중 경과일 정렬·필터**가 없으면 워커 앱을 아무리 접어도 좀비는 계속 생성된다.
  - **종결 행 = 즉시 이전 요청으로** (Tony 2026-07-20, 자정 보존 규칙 폐기): 내 요청·전체 보드에는 활성만 남고, 종결(발송·발송 불가·취소)은 곧바로 "이전 요청"으로 이동한다. 시간 규칙 없음.
  - 발송 불가 = 관리자 메모 강력 권장(사유 없는 거절 방지).
- **탭 화면 3단 구조** (Tony 2026-07-20 확정): ① **내 요청 = 내 활성만**(접수 전·발송 예정·검토 중; 행 meta는 수량·시각 — **대상 위치가 현재 작업 위치와 다를 때만 위치를 덧붙인다**) ② **진행 중 · 모든 위치 = 전 위치 활성**(내 항목=내 요청 칩) ③ **이전 요청(접힘, "N건 보기")= 종결 전부 — 내 것 + 모든 위치**(발송·발송 불가·취소, 내 것=내 요청 칩, 최신순). **표시 창 = 최근 30일**(그 이전 이력은 관리자 웹 — 앱 무한 리스트 금지). 전체 보드는 **인지용 — 행 전부 탭 불가**(내 칩 행 포함; 내 것의 액션 레인은 내 요청 섹션 하나뿐). 내 활성은 보드에도 칩으로 재노출된다(목업은 대표 1건만 축약 표기). 같은 SKU 중복 요청도 차단하지 않는다. 이전 요청에서 발송 불가(내 것)만 탭 가능(사유 시트); 발송·취소는 탭 없음. **빈 상태 스코프**: 통합 빈 화면(req-empty)은 3개 섹션이 모두 비었을 때만 — 하나라도 차 있으면 각 섹션이 인라인 빈 문구를 갖고 나머지는 그대로 노출.
- **작성**: 요청하기 → SKU 검색 + 수량 스테퍼 담기 → "요청 보내기 N건". 대상 위치 기본 = 현재 작업 위치(칩); **작업 위치 미설정이면 compose 진입 시 위치 선택 시트가 먼저 뜬다**(작업 탭과 같은 시트 문법). 같은 SKU의 진행 중 요청이 있으면 담은 행에 정보 한 줄(비차단, **스코프 = compose의 대상 위치 한정**).
  - **수량 하한 = 1** (0 불가 — 철회는 취소 버튼으로만, 경로 이원화 금지). 상한 없음.
  - **같은 SKU 재담기 = 기존 행 수량 갱신**(중복 행 없음 — compose 내 SKU 유일, "N건" = distinct SKU 수).
  - **제출 = 첫 탭에서 버튼 비활성**(이중 제출 가드), 화면 전환까지 유지.
  - **스테이징 = 인메모리 비지속** — 앱 종료·이탈 시 폐기, 제출로만 확정(지속 초안 없음).
  - **오프라인 = 요청 쓰기 전면 차단**(작성·수량 수정·취소 모두) — 상세 시트는 읽기 전용(버튼 비활성), 스캔 업로드 큐에 태우지 않는다(원장 인프라와 분리). 담던 중 끊기면 보내기 버튼 비활성.
- 🔴 **금지 (디커플링 방어선 — Tony 제1우선순위)**: 알림 뱃지 없음(v1 무알림 기조) · 출발 위치/"출고 예정" 표기 금지 · 이벤트 코드 없음(요청은 원장 이벤트가 아님).
  - **재고 수량 노출 금지**: 요청 탭의 어떤 화면에도 재고 수량(현재고·가용수량·본사 재고)을 표시하지 않는다 — 재고 조회는 재고 탭 전용이다. ("요청할 때 본사 재고 보이면 헛요청 안 하잖아"가 가장 예측 가능한 붕괴 요구.)
  - **관리자 메모의 지위**: 메모는 보낼지 말지에 대한 관리자의 한 줄 사유일 뿐이며, 재고 수량·입고 일정 등 원장 상태를 근거로 제시하거나 약속하는 채널이 아니다.
  - **지표 결합 금지**: 요청 데이터는 입고·출고 원장과 결합해 이행률·충족률 지표를 만들지 않는다 — 같은 SKU·위치·기간의 근접성은 매칭 근거가 아니다.
  - **관리자 웹 보드의 용도 한정**: 요청 보드는 상태 변경과 위생 관리 전용이며, 출고 작업의 입력(피킹 리스트·출고 지시·체크리스트)으로 사용하지 않는다.
- **화면 없는 상태(SPEC-ONLY)**: 요청 취소 = 확인 다이얼로그 없이 즉시(재요청으로 가역, OS 알럿 금지 — 넣더라도 인라인 경량 확인) · 발송 행 = 탭 없음(전할 정보도 행위도 없는 유일한 상태라 상세 시트 자체가 없음 — 상태 라벨이 전부). **`발송`은 관리자가 이 메모를 종결했다는 표시일 뿐 실물 도착·수량의 근거가 아니며, 실물의 진실은 오직 입고 기록에만 있다.**

## 7. 리더기

- H103 = RFID + 바코드 겸용 핸드리더. **리더기는 선택** — 없어도 수동(카메라 단건 포함)으로 전 작업 가능.
- 연결 화면: 주변 검색 → 연결. 신호·배터리 표시. "리더기 없이 계속하기" 제공.
- **연결 끊김(작업 중)**: 시트 = `재연결` / `수동으로 계속` 2버튼만 (리더기가 끊기면 바코드도 불가하므로 "바코드로 계속" 없음). 작업은 자동 저장 유지.
- 표기 규칙: 기기 화면=풀네임 `CF-H103`, 상태 칩=축약 `H103`.
- **진입 = 탭 아님**: 작업 홈 우측 리더기 칩 · 설정 리더기 행 (2026-07-20 리더기 탭 → 요청 탭 교체).

## 8. 작업 내역

- 오늘/최근 7일. 행 = 동사 아이콘 + 요약(N개/N개로 맞춤) + 위치·시각 + 상태(완료/확인 필요 N).
- **상세 = 목록 위 바텀시트** (풀페이지 아님): 결과·입력·발생시각·업로드 상태·사유. 확인 필요 상태 탭 → 확인 필요 화면.

## 9. 설정 · 승인

- 작업자 설정: 계정 카드(이름/이메일·매장 — 프로필 사진·아바타 없음) · 조직 · 언어 · 리더기 · 로그아웃 · 버전. 사용자 프로필/역할 기능 없음(발명 금지).
- 매니저 설정: + `연결 요청 N건` → 승인 화면(이름·역할·이메일·경과시간, 거절/승인). 역할 뱃지 없음 — 메타 줄 텍스트.

## 10. UI 공통 규칙 (요약 — 상세는 DESIGN.md)

- **헤더 4패턴**: ① 위치 스코프형(킥커=상위 위치, 타이틀=현재 노드˅; 리더기 칩=작업 홈만) ② 단순형(페이지 타이틀) ③ 작업 중(‹+동사+위치 칩) ④ 결과/인증(무헤더/브랜드마크). 시트의 베이스 화면 = 원 화면 그대로.
- **긴 텍스트**: 모든 텍스트 슬롯은 말줄임 또는 2줄 클램프 보유(위치명·상품명·이메일·역할 무제한 길이 가정). 숫자는 절대 말줄임 금지 — 칸이 숫자에 맞춰 늘어남.
- **컬러**: 블루 #2E44FF = CTA/활성/포커스/선택만. 이벤트 3색(증가 터콰이즈/감소 크림슨/맞춤 슬레이트)은 메달리언 서클만. 글로우·그림자 금지. 유저 아바타·상품 사진 썸네일 금지(이니셜 서클).
- **내부 식별자(이벤트 코드·EPC 원문) UI 노출 금지**. 용어: 누락(미스캔·missing 금지), 태그(EPC 금지).

## 11. 화면 인벤토리 (53)

> 진입점은 href/onclick 역추적 기준. "탭바"=하단 4탭(작업·재고·요청·설정) 공통. "gallery"=흐름 미배선, 갤러리에서만 도달하는 상태 변형/데모. 이탈점은 주요 CTA만 표기.

### 온보딩 (5화면 · 코드 퍼스트)
> 흐름상 6단계지만 물리 화면은 5장 — 인증코드에서 **기존 멤버 분기는 전용 화면 없이 작업 홈으로 직행**.

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| login-join.html | 브랜드 코드 입력 | 브랜드당 1개 6자리 코드로 시작(첫 화면) | 앱 콜드스타트·로그아웃 후 최초 화면 | → login | 무헤더/브랜드마크 |
| login.html | 로그인(브랜드 확인+이메일) | 코드가 가리킨 브랜드 확인 + 이메일 OTP 발송 | ← login-join | → login-code | 조직 확인 카드 |
| login-code.html | 인증코드 | 6자리 OTP + 재발송 타이머 | ← login | → login-name(신규), 기존 멤버→work-home(미배선) | 분기 지점 |
| login-name.html | 내 정보 입력 | 이름·역할 필수, CTA=연결 요청 | ← login-code | → login-pending | 폼 |
| login-pending.html | 승인 대기 | 매니저 승인 대기, 승인 시 푸시→홈 | ← login-name | → login-join(다른 코드), 승인→work-home(미배선) | 무헤더/대기 |

### 작업 탭 — 홈·선택 (5화면)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| work-home.html | 작업 홈 | 히어로(위치/구역)+리더기 칩+6동사 타일 | 탭바, 온보딩 후 | 6동사, 타이틀˅→work-section, history | 리더기 칩=홈만 |
| work-location.html | 위치 선택 시트 | 위치 단일 선택(드릴인 1단계) | 홈 타이틀 재선택 | →work-section(다중 구역), →work-section-flat(기본 구역만) | 바텀시트 |
| work-section.html | 구역 선택 시트 | 같은 시트 2단계 — 구역 리스트 | ← work-location, 홈 타이틀˅ | 구역→work-home, →work-section-empty | 동일 시트 드릴인 |
| work-section-flat.html | 기본 구역 선택 시트 | 구역 1개인 위치의 명시적 말단 선택 | ← work-location | 기본 구역→work-home | 동일 시트 드릴인 |
| work-section-empty.html | 빈 구역 | 첫 활성화(재고 0) | ← work-section | 첫 조사/입고 시작 | 엠프티 스테이트 |

### 동사 화면 — 입고·반품·출고·판매 (9화면)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| work-scan.html | 입고(RFID) | 태그 스캔 입고 | 입고 타일 | 토글→barcode, →unknown-items, →manual, 업로드→done | 읽는 범위 프리셋 |
| work-scan-barcode.html | 입고(바코드) | 리더기 바코드 입고 | ← 토글 | 토글→RFID, →manual, 업로드→done | 읽는 범위 숨김 |
| work-manual.html | 직접 추가 시트 | SKU 검색+수량 스테퍼 담기 | 각 스캔 화면 직접 추가 버튼 | 스캔 아이콘→카메라 2단계 | 상시 버튼(모드 아님) |
| work-camera-barcode-scanning.html | 카메라 스캔(대기) | 미인식 상태: placeholder+담기 비활성 | ← work-manual 스캔 아이콘 | back→manual, 인식→결과 | 시트 2단계·대기 상태 |
| work-camera-barcode.html | 카메라 스캔(인식) | 스캔 결과 카드+수량→담기 | ← 대기 상태 인식 | 담기/back→manual·scan | 시트 2단계·결과 상태 |
| work-return-scan.html | 반품 | +N 입고(판매 미연결) | 반품 타일 | →manual, 업로드(정적) | 동사 변형 |
| work-out-scan.html | 출고 | 이동·폐기·협찬 비판매 나감 | 출고 타일 | →manual, 업로드(정적) | 목적지 추적 없음 |
| work-sale-scan.html | 판매 | 판매 집계 포함 나감 | 판매 타일 | →manual, 업로드(정적) | 동사 변형 |
| work-done.html | 작업 완료 | 업로드 성공 요약(천단위 대응) | 입고·조정 업로드 | 홈으로/이어서 | 무헤더/결과형 |

### 조사 · 조정 (9화면)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| work-survey.html | 재고 조사 | 구역 전체 세어 덮어쓰기 | 조사 타일 | 업로드→survey-confirm, →unknown-items, →manual | 절대 카운트 |
| work-survey-long.html | 조사(긴 목록) | 고정 vs 스크롤 데모 | gallery(상태 변형) | 업로드→survey-confirm | 수치 스코프 미정리(잔여 예외) |
| work-survey-confirm.html | 조사 확정 | 여기서만 확정(이중 확정 금지) | ← survey | 확정→done | dl-row 카드(차이/현재/맞춤)+사유 칩 |
| work-adjust.html | 조정·상품 선택 | 맞출 SKU 먼저 선택 | 조정 타일 | 행→adjust-rfid | 1단계 |
| work-adjust-rfid.html | 조정·RFID | 태그로 세어 맞춤 | ← 선택/토글 | 토글, 맞춤→done, 스왑→재선택 | 바꾸기=스왑 아이콘 |
| work-adjust-barcode.html | 조정·바코드 | 바코드로 세어 맞춤 | 토글 | 토글, 맞춤→done | 토글 변형 |
| work-adjust-manual.html | 조정·수동 | 스테퍼 N개로 맞춤 | 토글 | 맞춤→adjust-confirm(대편차 데모), 바코드 찍어서 세기→adjust-camera | 수동 스테퍼+카메라 보조 |
| work-adjust-camera.html | 조정·바코드 찍어서 세기 | 폰 카메라로 선택 SKU만 1개당 +1 | ← adjust-manual 보조 버튼 | 토글, 맞춤(정적) | 리더기 없는 낱개 세기·SKU 필터 |
| work-adjust-confirm.html | 대편차 확인 시트 | "N개 감소합니다. 맞습니까?"+사유 | ← 수동(대편차) | back/확정(정적) | 대편차 시에만 |

### 재고 요청 (12화면)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| req.html | 재고 요청 탭 | 내 요청 상태 + 전체 진행 중(모든 위치) | 탭바 | 요청하기→req-new, 행→req-detail(-planned) | 원장 밖 체크리스트 |
| req-new.html | 새 요청 | SKU 검색·수량 담기·N건 보내기 | ← req 요청하기 | 보내기→req | 진행 중 요청 힌트(비차단) |
| req-detail.html | 요청 상세(접수 전) | 수량 수정(접수 전만)·취소(활성 내내) | ← req 접수 전 행 | 저장/취소→req | 바텀시트 |
| req-detail-planned.html | 요청 상세(발송 예정) | 예정일·관리자 메모 · 요청 취소(보내기 전) | ← req 발송 예정 행 | 취소→req, 닫기→req | 바텀시트·수량 수정 불가 |
| req-detail-overdue.html | 요청 상세(예정일 지남) | amber 지남 표기 · 요청 취소만 — 자동 전이 없음 | ← req 지남 행 | 취소→req, 닫기→req | 바텀시트·전용 행위 없음 |
| req-detail-hold.html | 요청 상세(검토 중) | 관리자 검토 중 · 메모 · 요청 취소(보내기 전) | ← req 검토 중 행 | 취소→req, 닫기→req | 바텀시트·수량 수정 불가 |
| req-detail-notsent.html | 요청 상세(발송 불가) | 종결 · 관리자 메모(사유) 전면 — 행위 없음 | ← req-history 발송 불가 행 | 닫기→req | 바텀시트·재요청은 요청하기로 |
| req-offline.html | 요청 탭(오프라인) | 목록 조회는 가능 · 요청하기 비활성(큐 미사용) | 오프라인 시 | — | D11: 스캔 업로드 큐와 분리 |
| req-aged.html | 오래된 요청(펼침) | 30일 넘은 활성 — 표시만 접힘·상태 불변·취소 가능 | ← req "오래된 요청 N건 보기"(내 요청·보드 양쪽) | 행→해당 상세, 접기→req | 나이 표현은 이 접기 하나뿐 |
| req-history.html | 이전 요청(펼침) | 종결 전부 = 내 것(칩) + 모든 위치 · 최신순 | ← req "이전 요청 N건 보기" | 발송 불가(내 것)→사유 시트, 접기→req | 발송·취소 = 탭 없음 |
| req-empty.html | 재고 요청 빈 상태 | 첫 사용 통합 빈 상태 + 요청하기 | 첫 진입(요청 0건) | →req-new | 엠프티 스테이트 |
| req-new-search.html | 새 요청 검색·담기 | 검색 결과 → 수량 → 담기 | ← req-new 검색창 | 담기→req-new | 검색 상태 |

### 재고 탭 (4) · 리더기 (3) · 내역·오프라인 (3) · 설정·매니저 (3)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| stock.html | 재고 기본 | 현재 위치 전체 구역 상품+검색 | 탭바 | 타이틀˅→stock-location | 쓰기 없음 |
| stock-location.html | 재고 위치 선택 시트 | 위치 단일 선택+전체 위치 | ← stock 타이틀 | →stock-location-aggregate(드릴인) | 시트 1단계 |
| stock-location-aggregate.html | 재고 구역 선택 시트 | 구역별 수량 행+전체 구역 합산 | ← stock-location | →stock-section, →stock(전체) | 시트 2단계=소계 겸함 |
| stock-section.html | 구역 재고 | 특정 구역 스코프 | ← 구역 시트 | 타이틀˅→stock-location | 말단 뷰 |
| reader.html | 리더기 상태 | 연결·신호·배터리 | 작업 홈 리더기 칩·설정 리더기 행 | back→work-home, 검색→reader-pair | 독 없음(push 화면) |
| reader-pair.html | 리더기 연결 | 주변 검색→연결 | ← reader | →reader, 리더기 없이 계속 | 기기=CF-H103 풀네임 |
| work-reader-disconnected.html | 리더기 끊김 시트 | 재연결/수동 2택 | 스캔 중 끊김(상태 오버레이) | 재연결/수동→스캔 | 바코드로 계속 없음 |
| history.html | 작업 내역 | 오늘/7일 이벤트 목록 | 홈 전체 보기, 탭 | 행→history-detail, 확인필요→unknown-items | 리스트 |
| history-detail.html | 작업 상세 | 결과·입력·시각·상태·사유 | ← history 행 | back→history | 바텀시트 |
| work-offline-queue.html | 업로드 대기 | 오프라인 큐·재시도·정정 유도 | 홈 조건부 배너, 실패 시 자동 | 지금 업로드/다시 시도 | 위치 칩 없음(기기 큐) |
| settings.html | 작업자 설정 | 계정·조직·언어·리더기·로그아웃 | 탭바 | 탭바 | 아바타·역할 기능 없음 |
| settings-manager.html | 매니저 설정 | +연결 요청 N건 | 매니저 역할 변형 | →approvals | 역할=메타 텍스트 |
| approvals.html | 연결 요청 승인 | 거절/승인 | ← settings-manager | back→settings | 거절=흰 배경+보더 |


---

## 부록 A. AI-FIRST MACHINE BLOCK

> 사람용 본문과 동일 내용의 기계가독 요약. 조회용 정본은 superfid-wiki — 이 블록은 이 폴더 안에서의 빠른 lookup 용도.

```json
{
  "meta": {"product": "SuperFID worker app", "screens": 53, "viewport": "360x800", "updated": "2026-07-20",
           "tabs": ["작업", "재고", "요청", "설정"], "reader_entry": ["work-home chip", "settings row"],
           "spec_authority": "github.com/superfid/superfid-wiki (worker-app-ux-policy-2026-07, worker-app-dev-spec-2026-07, inventory-core-policy-2026-07)"},
  "hierarchy": {"storage": "single locations table; section = child location(parentId != null); no sectionId or /sections",
                "levels": ["location(root)", "section(child leaf)"],
                "stock_truth": "leaf only; parents = read-only aggregates",
                "default_section": "every location owns a non-deletable 기본 구역",
                "work_target": "server isLeaf && selectable section only; root is never a work target",
                "api_fields": ["parentId", "parentName", "isLeaf", "childCount", "selectable"]},
  "verbs": [
    {"ko": "입고", "code": "IN_RECEIVE", "dir": "in",  "store": "delta",  "epc": "->in_stock"},
    {"ko": "반품", "code": "IN_RETURN",  "dir": "in",  "store": "delta",  "note": "+N, never matched to sales"},
    {"ko": "출고", "code": "OUT_ETC",   "dir": "out", "store": "delta",  "note": "move/dispose/sponsor unified, no destination", "epc": "->out"},
    {"ko": "판매", "code": "OUT_SALE",  "dir": "out", "store": "delta",  "epc": "->sold", "note": "included in net-sales"},
    {"ko": "조사", "code": "SET_AUDIT", "dir": "set", "store": "absolute-anchor", "scope": "leaf-location", "flow": "scan->upload->confirm(only there); missing computed at confirm only"},
    {"ko": "조정", "code": "SET_MANUAL","dir": "set", "store": "absolute-anchor", "scope": "sku", "flow": "pick SKU first -> count via rfid|barcode|manual; non-target observations ignored; large-delta self-confirm"}
  ],
  "ui_bans": ["verb codes in UI", "raw EPC in UI", "미스캔/missing wording", "user avatars/photos", "product photo thumbnails", "brand glow shadows", "emoji icons", "sheet->fullscreen transitions for context picking"],
  "input_methods": {
    "reader": {"model": "CF-H103", "modes": ["rfid", "barcode"], "optional": true,
               "disconnect_sheet": ["재연결", "수동으로 계속"]},
    "phone_camera": {"manual_add": "single-shot SKU lookup only; qty via stepper; same-barcode rescan = no-op; different barcode = replace result",
                      "adjust_manual": "바코드 찍어서 세기: selected SKU only, +1 per shot"}
  },
  "counters": {"live": "unique tags (EPC dedup) + total reads; manual rows excluded",
               "committed": "N = EPC + manual sum"},
  "unknown_items": {"display": "notification only — footer status line '미등록 N건 · 재고 반영 안 됨' (no tap action) + history status", "worker_actions": [], "resolution": "admin web only", "stock_impact": "none"},
  "offline": {"autosave": true, "queue": "idempotent retry via origin_event_key", "entry": "conditional home banner + auto on failure"},
  "requests": {
    "nature": "todo checklist OUTSIDE the inventory ledger; never moves stock; no request<->입고 linkage; requested qty != shipped qty by design",
    "item": "one SKU + qty + target location(requester's current, location-level not section) + requester + time; multi-SKU compose creates N items (submission_group_id stored for admin-web bulk only)",
    "states": {"active": ["접수 전", "발송 예정(date optional)", "검토 중"], "closed": ["발송", "발송 불가", "취소(worker, from any active state)"],
               "rules": ["no time-based auto transition", "예정일 past -> amber '예정일 지남' marker ONLY — no dedicated action (다시 요청 button rejected; the ever-present 요청하기 covers it)",
                          "NO worker receipt-ack (Tony 2026-07-20: arrival is handled by 입고 anyway; closing = admin only — receipt-ack multiplies edge cases)",
                          "qty edit only in 접수 전 (admin-confirmed qty never mutates behind admin)",
                          "cancel allowed through ALL active states 접수 전/발송 예정/검토 중 (Tony 2026-07-20); system rule = until CLOSED, UI copy '보내기 전까지' is the intuitive approximation; race = first finalizer wins, harmless by decoupling (shipped goods just get 입고-processed); 취소 leaves my list AND the all-board immediately (closed = closed, no special case)",
                          "admin may move any ACTIVE request to any of the 4 admin states; closed states are FINAL (no reopen — new request instead)",
                          "qty edit = requester only (admin never edits qty — ships any amount freely)",
                          "target location = frozen at submit time",
                          "closed rows leave the active lists IMMEDIATELY and appear under '이전 요청' (= ALL closed: mine + every location, 발송/발송 불가/취소, newest first, mine chipped 내 요청; only my 발송 불가 rows are tappable → reason sheet)",
                          "취소 removed from my list immediately", "발송 불가 needs admin note (strongly encouraged)",
                          "every worker action judged against CURRENT server state; stale sheet -> action ignored + silent refresh",
                          "history window = last 30 days (older = admin web)",
                          "aged ACTIVE (>30d since submit) folds into an '오래된 요청 N건' expander in BOTH 내 요청 and the all-location board — DISPLAY ONLY: state unchanged, still active, still cancellable when expanded; admin close moves it straight to 이전 요청; the app has exactly ONE time number (30d)",
                          "compose duplicate hint is computed from DATA regardless of fold state (never from the rendered list) — a folded active must still trigger the hint",
                          "NO other age affordance for actives (no graying, no demotion sort, no elapsed-day chip, no marker) — the only age-like signal is the admin's own 예정일 amber '지남'",
                          "accumulation ownership = admin web board hygiene; the worker app never alerts or aggregates it. ADMIN-WEB REQUIREMENT: 검토 중 is the only state with no clock — the admin board needs elapsed-day sort/filter for it or zombies keep being produced",
                          "board rows ALL non-tappable incl. my chipped rows (action lane = 내 요청 only); my actives re-shown chipped",
                          "unified empty screen only when ALL 3 sections empty; else per-section inline empty line",
                          "발송 예정 without date -> omit date chip & detail date row",
                          "compose: qty min 1 no 0; same-SKU re-add merges qty (SKU unique per compose); submit disables on first tap; staging in-memory non-persistent; no work-location -> location sheet first; duplicate hint scoped to target location",
                          "offline = ALL request writes blocked (create/edit/cancel), detail sheets read-only; entity refs stored by id, display names snapshotted at submit"]},
    "board": "3 sections: my ACTIVE requests / ALL-location ACTIVE (awareness only, duplicates allowed, mine chipped) / '이전 요청' expander = ALL closed incl. mine",
    "compose": "search SKU + stepper -> '요청 보내기 N건'; same-SKU active request shown as non-blocking info line; offline = compose disabled (NOT on scan upload queue)",
    "bans": ["notification badge", "source location / 출고 예정 wording", "event codes on request screens",
             "stock quantities anywhere in the request tab (on-hand/available/HQ stock) — stock lookup lives in the 재고 tab",
             "admin memo asserting ledger state (stock counts, inbound schedules) or promising delivery — it is a one-line reason only",
             "joining request data with 입고/출고 ledger for fulfillment/충족률 metrics — same SKU+location+period proximity is NOT a match",
             "using the admin request board as a picking list / 출고 지시 / checklist — status change + hygiene only",
             "submission_group_id as a shipment/fulfillment unit or carrying its own status fields"],
    "spec_only_states": {"발송 불가 detail": "planned sheet minus 예정일, admin-note-forward (reason)", "검토 중 detail": "planned sheet minus 예정일", "cancel": "no confirm dialog (reversible); inline confirm at most", "발송 row": "not tappable (terminal, no action)"}},
  "flows": {
    "onboarding": ["login-join(brand code = one per brand, first screen)", "login(company confirm + email)", "login-code(OTP)", "BRANCH existing-member -> work-home", "login-name(new: name+role)", "login-pending(manager approval)"],
    "receive": ["work-home", "work-scan(RFID) <-> work-scan-barcode", "+직접 추가 -> work-manual -> camera(scanning->result) -> 담기", "미등록 = footer status line (info only)", "업로드 -> work-done"],
    "survey": ["work-home", "work-survey", "업로드 -> work-survey-confirm", "확정(only here) -> done"],
    "adjust": ["work-home", "work-adjust(pick SKU)", "work-adjust-rfid <-> -barcode <-> -manual", "manual: 바코드 찍어서 세기 -> work-adjust-camera", "large delta -> work-adjust-confirm"],
    "stock": ["stock(current location, read-only)", "titlesel -> stock-location(single-select)", "-> stock-location-aggregate(zone sheet w/ counts)", "-> stock-section"]
  },
  "header_patterns": {"scope": "kicker=parent location + title=current node + chevron; reader chip only on work homes",
                       "simple": "page title (+back on sub-pages)", "in_task": "back + verb + location chip(ellipsis)",
                       "result_auth": "headerless / brandmark+title"},
  "long_text": {"clamp2": [".row-name", ".pick-nm", ".ac-nm", ".req-role"],
                "ellipsis1": "all meta/kicker/title/chip/label slots (see DESIGN.md §6)",
                "numbers": "never truncated; cells expand"},
  "open_decisions": [
    {"id": 1, "q": "음수 재고 비차단 확인 UI를 v1에 넣을지", "default": "PM recommends v1"},
    {"id": 2, "q": "조정 대편차 임계값 / 멱등 충돌 정책", "default": "개발 착수 시 확정"}
  ]
}
```
