# 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가 우선.
> 🔴 **예외 — §6 재고 요청**: wiki의 재고요청 문서는 구판(이행 모델)이라 **정책 v6 정본이 우선**한다(v5 = 기반 이력) —
> tony-knowledge `business/rfid-saas/_context/2026-08-11_request-policy-kanban-draft.md` (Tony 전 조항 확정
> 2026-08-11, 칸반 기조 ①~⑮ + 크루 채택 10건) + **v6 override `2026-08-12_request-tab-restoration-v6.md`**(⑫ 폐기·탭 복원). wiki 반영 전까지 이 절이 그 요약이다.
> 최종 갱신 2026-08-12 (재고 요청 절 = **정책 v6** — 구 ⑫ 단일목록 폐기, 2층 탭 복원) · 화면 세트 80장(데모 1 포함) 기준.

## 한눈에

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

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

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

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

## 코어 플로우 맵

```

## 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개.
- **계정·권한 = 2축**(정본 = 인증정책 §1.5): 표시 role(admin/operator — 판정 불사용) + 접근 등급(no_access/worker/manager/admin — 모든 기능 판정). 관리자·매니저·작업자 = 등급의 표시명. 가입 승인 결과 = 항상 작업자(승격 = 웹 admin의 등급 변경). 조직 개설 = 웹 전용(개설자 admin 자동 — 구 결제 연동 서술 폐기) / 입장 = 브랜드 코드 셀프 가입(승인 게이트) 또는 웹 이메일 초대.

## 1. 온보딩 (구글 퍼스트 — 인증정책 v4 ✅⑥, 구 코드 퍼스트·이메일 OTP 폐기)

`[구글로 계속하기](첫 화면) → 멤버십 있으면 홈 직행 / 없으면 → 브랜드 코드 → 브랜드 확인+내 정보(이름 1회 수정·역할 한 줄) → 매니저 승인 대기`

| 단계 | 정책 |
|---|---|
| 구글 로그인 | 첫 화면 = 1버튼. 성공 분기: 활성 멤버십 ≥1 → 작업 홈 / pending → 승인 대기 / 0 → 브랜드 코드 / 전부 끊김 → 접근 종료. 첫 로그인 = 온라인 필수 |
| 브랜드 코드 | 멤버십 0일 때만. 코드 = 비밀 아님(보안 = 승인 게이트). 계정 칩 + "계정 바꾸기" 상설. 개설은 웹 전용(보조 링크). 형식 = 6자리 영숫자(✅B 확정) |
| 브랜드 확인+내 정보 | 브랜드 카드(오입력 방어) + 이름(구글 프리필·**1회 수정** — 기록용 실명 유도) + 역할 한 줄(승인 판단 재료 — 저장 컬럼 dev 델타) + 이메일 읽기 전용 |
| 승인 대기 | 요청 시각 표시 · 탈출구 2개(다른 코드/다른 계정). 거절 상태 = 별도 화면(다시 요청 가능 — 실측: 거절 행은 재요청을 막지 않음). 승인 시 홈 직행 |
| 재로그인·새 폰 | 세션 유지 90일 · 새 기기 = 구글 1탭(멤버십 있으면 코드 불요) |
| 승인 규칙 | 승인 = 매니저 이상 · 앱 세트(✅A 확정) · **결과 = 항상 작업자**(매니저 승격 = 웹 관리자) · 기존 grant 덮어쓰기 금지 |

**상태 카피 표 (구현 명세 — 별도 목업 없는 상태 변형):** 로그인 진행 중="로그인 중..." · 로그인 실패=
"로그인을 마치지 못했어요. 다시 시도해 주세요." · 첫 로그인 오프라인="첫 로그인은 인터넷 연결이 필요해요." ·
코드 없음="이 코드를 찾을 수 없어요. 매니저에게 다시 확인해 주세요."(대표 목업 = login-brand-error) ·
이미 연결="이미 연결된 브랜드예요."+[작업 시작] · 이미 요청함=대기 화면으로 이동 · 접근 종료(코드 정지 변형)=
"브랜드 연결이 잠시 중단됐어요. 매니저에게 확인해 주세요." · compose 50 SKU 도달="한 번에 50개까지 담을 수
있어요"(담기 비활성) · 5xx/타임아웃="지금 보낼 수 없어요 · 다시 시도"(스테이징 유지·수동 재시도) ·
목록 증분 로딩="불러오는 중..."(첫 200건). 수량 키패드 = OS 네이티브(목업 무의미 — 값 탭 시 열림).

## 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. 재고 요청 탭 (원장 밖 칸반 카드 — 정책 v6, Tony 확정 2026-08-12 탭 복원 + 크루 이터레이션)

- **기조 = 칸반 카드**: 카드를 만들고 옮기는 어떤 행위도 재고를 바꾸지 않는다(inventory event 0건). 실물
  이동은 지금처럼 출고/입고로만 기록. 카드 상태는 사실의 증명이 아니라 담당자의 말 — `발송` = "보냈다고
  관리자가 말함"이지 도착 증명이 아니다. 시스템은 카드와 원장을 절대 대조하지 않는다(매칭·이행률 금지).
- **카드** = SKU 1개 + 수량 + 대상 위치 + 요청자 + 제출 시각. **대상 위치 = 요청 탭에서 선택한 위치**(확정
  ⑭ — 제출 시점 고정·이후 불변, 위치 레벨). 출발 위치 필드는 존재하지 않는다. 표시명은 제출 시점 스냅샷.
  `submission_group_id`는 선택 편의용일 뿐(그룹 상태·원자성 없음).
- **상태 = 내부 6종 · 작업자 표시 5종** (✅ Tony 확정 2026-08-12 — enum·DB 무변경, 표시 매핑만):

  | 작업자 표시 | 내부 enum | 탭 |
  |---|---|---|
  | `요청됨` | `pending` | 진행 중 |
  | `검토 중` | `reviewing` | 진행 중 |
  | `발송 예정` | `planned` + `sent` | 진행 중 |
  | `발송 불가` | `not_sent` | 종료됨 |
  | `취소됨` | `cancelled` | 종료됨 |

  - `접수 전` → **`요청됨`**: 부정형은 "무엇의 전인지"가 5가지로 읽히고, 관리자가 열어봐도 전이만 안 하면
    계속 `접수 전`이라 **거짓말할 수 있는 라벨**이었다.
  - `발송`은 별도 상태에서 내린다 — **시스템은 도착을 모르므로 완료를 선언할 자격이 없다.** 종료됨에는
    `발송 불가`·`취소됨`만 남는다(= 안 되는 것만 종결). 보냈다는 카드는 진행 중에서 30일 후 접힌다.
  - `취소` → **`취소됨`**: 버튼처럼 읽히던 것을 상태형으로.
  - `요청됨`에서 관리자는 4종 어디로든 전이 가능(= 최초 관리자 터치, **비가역** — 요청됨 복귀 없음, 그
    순간 수량 수정권 종료). **관리자 4종끼리 자유 이동 — 종결 재오픈 허용**(구 "종결은 최종" 폐기).
    `취소`만 절대 종결(요청자 의사 — 누구도 되살릴 수 없음). **관리자는 취소로 전이 불가**(선택지에 없음;
    대리 철회 = 발송 불가 + 메모 사유).
  - 시간 경과 자동 전이 없음.
  - 🔴 **예정일 폐기 (✅ Tony 확정 2026-08-12)** — 시스템이 검증하지 않는 미래 약속이라 틀리면 그 자체가
    새 관리 대상이 된다. 날짜·`지남` amber·경과 필터 전부 제거. "언제 오나"는 관리자 메모 한 줄 또는 사람이
    말한다. 이로써 정책 39개 조항(상태 이탈 시 삭제·`WHERE status=발송 예정` 원자 조건·상태+날짜 동일
    트랜잭션·일괄 예정일 1개·취소 카드 차단)과 DB CHECK 제약이 함께 소멸한다.
  - **상태 이력**: 상태값이 실제로 바뀐 전이마다 변경자(id+표시명 스냅샷)·시각 append(작업자 취소 포함).
    같은 상태 재지정 = no-op(이력·revision 없음). 카드 표시 = 현재 상태로 마지막 진입한 변경자·시각 1건,
    **상세 시트에만**(목록 행 금지). `요청됨` 카드는 요청자·제출 시각. 전체 이력 열람은 관리자 웹 전용.
- **동시성**: 모든 상태 변경·수량 수정은 revision 조건부 갱신(불일치 = 409, 클라는 알럿 없이 시트를 닫지
  않고 재렌더). 메모는 분리된 단건 커맨드(revision 무관, 원자적 상태 조건 집행). ⚠️ 구 "예정일 옵션 동일
  트랜잭션 원자 저장" 조항은 예정일 폐기와 함께 소멸.
- **작업자 권한**: 생성 · 수량 수정(`요청됨`+본인만) · 취소(현재 활성+본인만, **확인 1단계** — "취소하면
  되살릴 수 없어요. 필요하면 새로 요청하세요"). 취소 가능 구간 표준 카피 = **"관리자가 처리를 끝내기 전까지
  취소할 수 있어요"** ("발송 전까지"는 금지 문구). 조회 위치와 작업 위치가 달라도 본인 카드 수정·취소 가능.
- 🔴 **탭 화면 = 2층 탭** (✅ Tony 확정 2026-08-12 — **구 ⑫〈위치 선택 + 단일 목록〉 폐기**. ⑫는 이미
  구현돼 있던 Android 탭을 없애는 방향이었고, Tony가 "탭은 하는 걸로 확정했잖아"로 뒤집었다. 정본 = Android
  `StockRequestStateHolder.kt:20-32`):

  | 1차 탭 | 2차 스코프 | 목록 내용 | 행이 답하는 것 |
  |---|---|---|---|
  | `진행 중` | `내 요청` (기본) | 내 활성 전부 — **위치 무관** | 어느 위치 |
  | `진행 중` | `선택 위치` | 헤더에서 고른 위치의 활성 전부(동료 포함) | 누가 |
  | `진행 중` | `모든 위치` | 조직 전체 활성 | 어디 + 누가 |
  | `종료됨` | (스코프 행 없음) | `발송 불가`·`취소됨`만 · **모든 위치** 30일 | 어디 + 누가 |

  - **정렬 = 최신순** (✅ Tony 확정 2026-08-12 — 구 "오래된 순" 폐기. Android `StockRequestStateHolder.kt:85-108` 정합).
  - `현재 위치` → **`선택 위치`로 개칭**. 이 스코프는 작업 위치가 아니라 **헤더 위치 선택기에서 고른 위치**로
    거르기 때문(`StockRequestScreen.kt:305`)에 구 이름은 거짓이었다. 위치 선택기 = ① `선택 위치` 스코프의 기준
    ② 새 요청 대상. 두 역할 외에는 목록을 지배하지 않는다.
  - **안내문은 위치 종속 문장이면 안 된다** — 기본 스코프 `내 요청`이 위치 무관 목록이므로 "이 위치로 보내달라는
    요청이에요"는 그 탭에서 거짓이 된다.
  - **빈 상태 2종**(Android `StockRequestScreen.kt:213·481·510` 정합): 전체가 비었을 때만 탭·스코프를 숨기고
    전면 빈 상태(`req-empty`), 스코프 하나만 비면 **탭을 유지**한 채 섹션 빈 상태(`req-scope-empty`).
  - **오래된 요청 익스팬더**(마지막 움직임 = revision 증가 시각 기준 30일, 표시 전용·상태 불변·펼치면 취소
    가능)는 `내 요청` 스코프에 붙는다. ⚠️ Android 미구현 = 정책 산물(dev 델타).
  - **구 `이전 요청` 익스팬더 폐기** → `종료됨` 탭이 그 역할을 대체한다. 취소 직후 목적지도 `종료됨`
    (Android 스낵바 액션 `"종료됨에서 보기"` `StockRequestScreen.kt:197`와 정합).
  - 탭 규칙 (✅ §10-4 Tony 확정 2026-08-11 저녁 — 구 "타인 카드 탭 불가" 폐기): **표시된 모든 카드(활성 +
    종료됨 30일 창, 타인 포함) = 읽기 전용 상세 열람.** 액션 lane(수량 수정·취소)은 본인 활성 카드만.
    ⚠️ Android는 아직 타인 카드 `onClick=null`(`StockRequestScreen.kt:359-363`) = dev 델타.
    타인 카드 상세 = 조회 전용 시트(버튼 0개, "동료의 요청" 표기) — 교대 인계 메모 열람·대리 철회 사유
    가독·무반응 UX 제거가 근거.
  - 위치 선택 = 작업·재고 탭과 같은 시트 문법. **탭 진입 시 선택 위치 = 현재 작업 위치로 리셋**(구경
    상태가 남아 오요청되는 것 방지); 다른 위치를 보는 동안 헤더 아래 정적 안내 한 줄. **위치 시트 행에
    "내 진행 중 N건" 표기**(0이면 생략 — 오요청 회수·겸임 매니저용). 재오픈 카드는 종료됨 탭에서 즉시
    사라져 활성 목록에 나타남(스크롤 중 소실 = 정상, 보정 없음).
  - 섹션 분할·30일·상대일은 서버가 조직 타임존(IANA, 기본 Asia/Seoul, 현지 자정 경계)으로 계산.
- **compose**: 요청하기 → SKU 검색+수량 스테퍼 담기 → "〈위치명〉로 요청 보내기 N건"(제출 버튼까지 대상
  위치명 명시 — 확정 ⑭). **수량 값 탭 = 숫자 키패드 직접 입력**(120·200개를 ±로만 못 만들게 하지 않는다;
  상세 시트 수량 수정 동일). 힌트 화면 하단 카피 "이미 요청된 상품도 필요하면 그대로 보내도 돼요"(비차단
  명시). 수량 하한 1 · 같은 SKU 재담기=수량 갱신 · 이중 제출 가드 · 인메모리 스테이징
  (담은 행 삭제 자유) · 검색 화면 뒤로가기 = compose 복귀(스테이징 유지). **중복 힌트** = 대상 위치 한정,
  현재 활성 카드 전부(접힌 카드 포함, 데이터 기준), 제출 시점 상대일 포함("김하늘이 어제 60개 요청함 ·
  진행 중"), 비차단. 중복 카드 발생은 정상 — 사후 병합·차단 없음(서버측 중복 제한 금지).
- **오프라인** (확정 ⑮): 요청 쓰기 전면 차단 · 마지막 동기화된 **활성 목록**만 읽기 전용 표시(오래된·이전
  익스팬더는 온라인 전용) · **캐시된 카드의 상세 시트는 읽기 전용으로 열람 가능**(쓰기 버튼만 비활성 —
  관리자 메모 확인이 오프라인 탭의 핵심 용도) · 위치 전환은 캐시된 위치만(캐시 없는 위치 차단) · 재연결 시
  현재 선택 위치 새로고침.
- **조용한 갱신의 화면 언어**: 409 재렌더된 시트 안에 정적 한 줄("방금 관리자가 확인해서 수량 수정이
  닫혔어요"류) · 취소 확정 후 목록 상단 정적 한 줄("요청을 취소했어요 · 종료됨에서 볼 수 있어요") —
  알럿·토스트 금지, 화면 안 텍스트만.
- **"언제 오나"는 사람이 말한다**: 상태 + 관리자 메모 한 줄(검증되지 않는 코멘트, 덮어쓰기)이
  전부. 수량 잠금 카피는 대안까지: "관리자가 확인한 뒤에는 수량을 바꿀 수 없어요 — 취소하고 새로 요청하거나,
  필요한 만큼 더 요청하세요."(활성 카드 한정 — 종결 카드엔 "필요하면 새로 요청" 만).
- **✅ §11 추가 계약(2026-08-12 전수조사 크루 — Tony 확정, 정본 = 정책 §11)**: 수량 1~9,999 ·
  compose 50 SKU · 일괄 100장 · 앱 목록 = 커서 증분(첫 200·전량 도달) · 제출 = 전건 원자+submissionKey 멱등 ·
  5xx = 스테이징 유지+수동 재시도 · 새로고침 = 진입/포그라운드/당김(주기 폴링 없음) · 30일 경계 = 31일째부터 ·
  서버는 정수·타임스탬프만(상대일 문자열 = 클라) · SKU/위치 비활성·퇴사자 접근 차단 등 생멸 규칙.
- 🔴 **금지 (디커플링 방어선)**: 요청 화면 어디에도 재고 수량 노출 금지 · 요청↔입출고 링크·매칭·지표 금지 ·
  출발 위치/ETA/배송 추적 구조화 금지(메모 자유 텍스트만) · 이력에서 이행·횟수·수량 계산 금지 · **앱 무알림**
  (관리자 웹 인라인 카운트·뱃지는 조회 피드백으로 허용 — 외부 푸시·메일은 양쪽 금지) · 이벤트 코드 노출 금지.

## 7. 리더기

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

## 8. 작업 내역

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

## 9. 설정 · 승인

- 작업자 설정: 계정 카드(이름/이메일·현재 위치 + **내 역할 1단어 읽기 전용 표시**) · 조직(전환 시트) · 언어 · 리더기 · 로그아웃 · 버전. 역할 **편집** 기능 없음(발명 금지 — 읽기 전용 표시는 필수, 이름 수정은 온보딩 1회 예외).
- 매니저 설정: + `연결 요청 N건` → 승인 화면(이름·역할 한 줄·**이메일 로컬파트**·경과시간, 거절/승인 + 연결된 멤버 섹션의 **접근 끊기** — ★결정 A 권고안). 끊기 대상 = 작업자만(매니저 이상·본인 = "관리자만 바꿀 수 있어요"). 조직 행 = 전환 시트(겸직 — 업로드 대기 시 전환 차단, 조직 1개면 시트 없음). 계정 카드 메타 = 내 역할 1단어.

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

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

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

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

### 온보딩 (7화면 · 구글 퍼스트)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| login.html | 시작하기 | 구글 1버튼(첫 화면) + 계정 주의 마이크로카피 | 콜드스타트·로그아웃 후 | 성공 분기 4종(홈/대기/코드/종료) | 무헤더/브랜드마크 |
| login-brand.html | 브랜드 코드 | 멤버십 0 전용 · 계정 칩+다른 계정 · 개설=웹 안내 | ← login(멤버십 0) | →login-confirm, 다른 계정→login | 6자리(✅B) |
| login-brand-error.html | 브랜드 코드 · 오류 | 코드 없음 상태(상태 카피 표 대표) | 코드 불일치 시 | 재입력 | 상태 변형 |
| login-confirm.html | 브랜드 확인+내 정보 | 오입력 방어 + 이름 1회 수정 + 역할 한 줄 | ← login-brand | 연결 요청→login-pending | 역할 저장=dev 델타 |
| login-pending.html | 승인 대기 | 요청 시각 · 탈출구 2개 | ← login-confirm | 다른 코드→login-brand, 다른 계정→login | 승인 시 홈 직행 |
| login-rejected.html | 연결 거절 | 다시 요청(실측 성립) · 사유 미수집 | 거절 시 | 다시 요청→login-confirm | 상태 변형 |
| login-ended.html | 접근 종료 | 비활성·코드 정지 공용 + 미업로드 경고 | 멤버십 소멸 시 | 다른 브랜드→login-brand, 로그아웃→login | 응답 계약=dev 델타 |

### 작업 탭 — 홈·선택 (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/확정(정적) | 대편차 시에만 |

### 재고 요청 (31화면 · 정책 v6)

| 파일 | 화면명 | 목적 | 진입점 | 이탈점 | 상태 특이 |
|---|---|---|---|---|---|
| req.html | 재고 요청 탭 | 기본 = 진행 중·내 요청(위치 무관·최신순) | 탭바 + 2층 탭 | 타이틀˅→req-location, 요청하기→req-new, 행→상세, 탭→req-closed, 스코프→req-scope-location·req-scope-all, 익스팬더→req-aged | 내 요청엔 칩 불필요(전부 본인) |
| req-detail-other.html | 상세(동료 활성·읽기 전용) | 타인 카드 조회 전용 — 메모·변경자·요청자, 버튼 0개 | ← 타인 활성 행 | 닫기→req | §10-4 · 요청됨 타인도 대표 재사용 |
| req-detail-other-notsent.html | 상세(동료 발송 불가·읽기 전용) | 대리 철회·거절 사유가 동료에게 읽히는 창구 | ← req-closed 타인 종결 행 | 닫기→req-closed | §10-4 |
| req-detail-other-pending.html | 상세(동료 요청됨·읽기 전용) | 요청자·제출 시각만 — 버튼 0개 | ← 타인 요청됨 행 | 닫기→req | §10-4 |
| req-detail-other-sent.html | 상세(동료 발송 예정·읽기 전용) | "보냈다고 표시" — 입고 확인 안내 | ← req 타인 행 | 닫기→req | §10-4 |
| req-location.html | 위치 선택 시트 | 목록 대상 위치 전환(단일 선택) | ← req 타이틀˅ | 행→req | 작업·재고 탭과 같은 시트 문법 |
| req-new.html | 새 요청(compose) | SKU 담기·수량·제출 — 버튼까지 대상 위치명 명시(⑭) | ← req 요청하기 | 검색→req-new-search, 보내기→req | 중복 힌트=상대일 포함·비차단 |
| req-new-search.html | 새 요청 검색 | 검색 결과→수량→담기 | ← req-new 검색창 | 담기·뒤로→req-new(스테이징 유지) | 검색 상태 |
| req-detail.html | 상세(요청됨) | 수량 수정(본인)+취소 | ← req 내 행 | 취소→req-cancel-confirm, 저장→req | 요청자·제출 시각 표시 |
| req-detail-planned.html | 상세(발송 예정) | 관리자 메모·마지막 변경자 | ← req 내 행 | 취소→req-cancel-confirm-planned | 수량 잠금 |
| req-detail-hold.html | 상세(검토 중) | 관리자 메모·마지막 변경자 | ← req 내 행 | 취소→req-cancel-confirm-hold | 수량 잠금 |
| req-detail-aged-pending.html | 상세(오래된 요청됨) | 접힘은 표시 전용 — 수량 수정·취소 그대로 | ← req-aged 내 행 | 취소→req-cancel-confirm | 배경=req-aged 목록 |
| req-detail-aged-hold.html | 상세(오래된 검토 중) | 접힘은 표시 전용 — 취소 그대로 | ← req-aged 내 행 | 취소→req-cancel-confirm-hold | 취소 확인은 상태별 대표 시트 재사용 |
| req-detail-sent.html | 상세(발송 예정·발송 표시) | 본인 활성 열람 — 취소 가능·수량 잠김 — 변경자·메모("실제 도착은 입고 기록") | ← req 내 행 | 닫기→req | 취소 가능(수량 잠김) |
| req-detail-notsent.html | 상세(발송 불가·읽기 전용) | 사유 메모 전면 | ← req 내 행 | 닫기→req | 취소 가능(수량 잠김) |
| req-detail-cancelled.html | 상세(취소·읽기 전용) | 취소한 작업자·시각 | ← req-closed 내 행 | 닫기→req-closed | lastStatusActor=취소자 |
| req-cancel-confirm.html | 취소 확인(요청됨) | 1단계 비가역 확인 — "취소하면 되살릴 수 없어요" | ← req-detail | 돌아가기/확정→req | 확정⑦ |
| req-cancel-confirm-planned.html | 취소 확인(발송 예정) | 〃 | ← req-detail-planned | 〃 | 〃 |
| req-cancel-confirm-hold.html | 취소 확인(검토 중) | 〃 (오래된 검토 중도 재사용) | ← req-detail-hold·aged-hold | 〃 | 〃 |
| req-aged.html | 오래된 요청(펼침) | 마지막 움직임(revision 증가 시각) 30일 — 상태 불변 | ← req 익스팬더 | 내 행→aged 상세, 접기→req | 나이 표현은 이 접기 하나뿐 |
| req-closed.html | 종료됨 탭 | **발송 불가·취소됨만** · 모든 위치 30일 · 최신순 · 스코프 행 없음 | 1차 탭 | 행→종결 상세, 탭→req | 발송 불가=amber/취소됨=흐림 |
| req-scope-location.html | 진행 중 · 선택 위치 | 헤더에서 고른 위치의 활성 전부(동료 포함) · 행은 요청자를 답한다 | 2차 스코프 | 행→상세, 스코프→req·req-scope-all | 수정/취소는 내 요청에서 |
| req-scope-all.html | 진행 중 · 모든 위치 | 조직 전체 활성 · 행은 위치+요청자를 답한다 | 2차 스코프 | 행→상세, 스코프→req·req-scope-location | 조직 현황 열람 |
| req-scope-empty.html | 선택 위치 빈 상태 | 스코프만 비었을 때 — **탭 유지** | 2차 스코프 | →req-new, 모든 위치 안내 | 전체 빈 상태와 구분 |
| req-other-location.html | 타위치 열람 중 | 작업 위치가 아닌 위치를 보는 상태 — 헤더 아래 정적 안내 한 줄 | 위치 시트에서 타위치 선택 | 행→상세, 타이틀˅→req-location | §6 위치 지속성 |
| req-cancel-done.html | 취소 직후 | 목록 상단 정적 안내("요청을 취소했어요…"), 취소 카드는 목록에서 사라짐 | 취소 확정 직후 | 다음 새로고침에 안내 소멸 | 알럿·토스트 금지 |
| req-detail-conflict.html | 상세(409 재렌더) | 수량 수정 중 관리자 확인 → 시트 유지·새 상태 재렌더 + 정적 안내 | 409 수신 시 | 취소→확인 시트 | §3 조용한 갱신 |
| req-stress.html | 스트레스 상태 모음 | 5자리 수량·긴 상품명 클램프·동명이인·같은 SKU 3장·초장문 위치명 | gallery(상태 변형) | 행→상세 | 데이터 경계 검증용 |
| req-empty.html | 전체 빈 상태 | 요청이 하나도 없을 때만 — **이 화면에만 탭이 없다**(Android StockRequestScreen.kt:213·481·510) | 첫 진입 | →req-new | 엠프티 스테이트 |
| req-offline.html | 오프라인 | 마지막 동기화 활성 목록 읽기 전용·쓰기 전면 차단(⑮) | 오프라인 시 | 캐시된 내 행→읽기 전용 상세 | 익스팬더 2종=온라인 전용 |
| req-detail-offline.html | 상세(오프라인·읽기 전용) | 캐시 카드 열람 — 마지막 동기화 기준·쓰기 버튼 없음 | ← req-offline 행 | 닫기→req-offline | ⑮·§10-4 대표 시트 |

### 재고 탭 (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 | 작업자 설정 | 계정(역할 1단어)·조직(전환 시트)·언어·리더기·로그아웃 | 탭바 | 조직 행→settings-org | 이름 수정은 온보딩 1회만(예외) |
| settings-manager.html | 매니저 설정 | +연결 요청 N건 | 매니저 역할 변형 | →approvals | 역할=메타 텍스트 |
| approvals.html | 연결 요청+멤버 | 승인(항상 작업자·근무 위치 선택 — 시트는 기존 위치 시트 문법 재사용)·거절 + 연결된 멤버 접근 끊기(✅A 확정). 비매니저에게는 연결 요청 행 자체가 미노출(권한 없음 화면 불필요) | ← settings-manager | 끊기→approvals-revoke | 브랜드명 상시(✅⑧) |
| approvals-revoke.html | 접근 끊기 확인 | 1단계 확인 — 기기 로그아웃 고지 | ← approvals | 취소/끊기→approvals | 대상=작업자만 |
| demo-home-sheet.html | 데모 · 홈 시트 | 갤러리 데모(흐름 미배선) | gallery | — | 데모 |
| settings-org.html | 조직 전환 시트 | 겸직 전환 · 역할 1단어 · 업로드 대기 시 차단 | ← settings 조직 행 | 행→settings, 다른 브랜드→login-brand | 조직 1개면 시트 없음 |


---

## 부록 A. AI-FIRST MACHINE BLOCK

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

```json
{
  "meta": {"product": "SuperFID worker app", "screens": 77, "viewport": "360x800", "updated": "2026-08-11",
           "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": {
    "policy_authority": "tony-knowledge business/rfid-saas/_context/2026-08-11_request-policy-kanban-draft.md (policy v6 (tab restoration + planned-date retirement + 5 worker labels); v5 = superseded base, Tony-confirmed + crew iteration 2026-08-11; overrides stale wiki request docs)",
    "nature": "kanban memo card OUTSIDE the inventory ledger; never moves stock; no request<->ledger linkage or metrics; card status = what the admin SAYS, not proof; physical truth lives only in 입고 records",
    "item": "one SKU + qty + target location + requester + submitted time; TARGET LOCATION = the location selected in the request tab (⑭), frozen at submit; NO source-location field; display names snapshotted at submit; submission_group_id = selection convenience only",
    "states": {"internal": ["pending","reviewing","planned","sent","not_sent","cancelled"],
               "workerDisplay": {"pending":"요청됨","reviewing":"검토 중","planned":"발송 예정","sent":"발송 예정","not_sent":"발송 불가","cancelled":"취소됨"},
               "activeTab": ["pending","reviewing","planned","sent"], "closedTab": ["not_sent","cancelled"],
               "plannedDate": "RETIRED 2026-08-12 — no date field, no overdue amber, no elapsed filter",
               "rules": ["from pending the admin may move to ANY of the 4 admin states (= the first admin touch, irreversible; worker qty-edit right ends there)",
                          "admin states are freely interchangeable INCLUDING reopening not_sent back to active (sent stays in the ACTIVE tab — the system cannot see arrival, so it may not declare completion) (old 'closed is final' is RETIRED); 취소 is the ONLY absolute terminal; admins can NEVER transition to 취소 (proxy withdrawal = 발송 불가 + memo)",
                          "closed->closed moves refresh closedAt; reopen clears closedAt (active closedAt = NULL invariant)",
                          "no time-based auto transition; planned date RETIRED (2026-08-12) — the system never promises a date; 'when' is an admin memo line or a phone call",
                          "status history: append actor(id+name snapshot)+time ONLY on real status change (worker cancel included; same-state reassign = 200 no-op, no revision bump, no history row); card shows last transition actor+time in DETAIL SHEET only; 요청됨 shows requester+time; full history = admin web",
                          "concurrency: every status change & qty edit sends seen revision; mismatch = 409 stock_request_version_conflict + currentVersion; client re-renders silently WITHOUT closing the sheet",
                          "memo = separate single command, no revision check/bump, atomic status-condition execution (memo WHERE active); transition commands may carry optional memo (any transition) (planned-date RETIRED 2026-08-12); memo = last-write-wins from any path",
                          "worker: create / qty edit(요청됨+mine) / cancel(currently-active+mine, ONE confirm step: '취소하면 되살릴 수 없어요. 필요하면 새로 요청하세요'); standard cancel-window copy = '관리자가 처리를 끝내기 전까지 취소할 수 있어요' ('발송 전까지' is BANNED copy)",
                          "admin permission = inventory.stock-requests.manage, org scope, minimum MANAGER access level; no per-location admin scoping",
                          "30-day rules: active fold = 30d since last movement (= last revision-bumping event: submit/status change/qty edit; memo & planned-date do NOT count); closed window = 30d since current closedAt; computed SERVER-side in org timezone (IANA org field, fallback Asia/Seoul, local-midnight day boundaries)",
                          "duplicate hint: scope = compose target location, judged on ALL currently-active cards from DATA (folded included), includes relative submit time, non-blocking; duplicate cards are normal (no server dedup limit)",
                          "offline (⑮): ALL request writes blocked; last-synced ACTIVE list read-only; cached-card detail sheets open READ-ONLY (write controls disabled); aged/closed expanders online-only; switching to uncached location blocked; refresh current location on reconnect"]},
    "tab": "TWO-LEVEL TABS (policy v6, supersedes retired policy 12): 진행 중|종료됨 x 내 요청|선택 위치|모든 위치; default 진행 중+내 요청; NEWEST-FIRST; 내 요청 is location-agnostic; 종료됨 = all locations, 30d, no scope row; retired 이전 요청 expander -> 종료됨 tab",
    "compose": "search SKU + stepper -> '〈location〉로 요청 보내기 N건' (target location named through the submit button); qty min 1; same-SKU re-add merges; double-submit guard; in-memory staging (row delete free; search back returns to compose keeping staging)",
    "bans": ["stock quantities anywhere in request screens", "request<->ledger matching or fulfillment metrics", "structured source-location/ETA/tracking (admin memo free-text only)", "computing shipments/counts from history", "app notifications & badges (admin-web inline counts/badges allowed as lookup feedback; external push/email banned on both)", "event codes in UI", "admin board as picking list", "admin proxy card creation (v1)"]},
  "flows": {
    "onboarding": ["login(Google one-tap, first screen; branch: memberships>=1 -> work-home / pending -> login-pending / 0 -> login-brand / all revoked -> login-ended)", "login-brand(brand code, membership-0 only; org creation = web only)", "login-confirm(brand card + name one-time edit + role line)", "login-pending(approval wait; rejected -> login-rejected, re-request allowed)"],
    "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": "개발 착수 시 확정"}
  ]
}
```
