구조와 경계
현장 태블릿 앱을 고치거나 다른 시스템과 붙일 때 알아야 할 구조입니다. 소스는 plantpulse-platform/plantpulse-frontline-web 저장소입니다.
모양
태블릿 브라우저 (PWA) frontline 컨테이너 플랫폼
┌──────────────────────┐ HTTPS ┌──────────────────────────────┐ V5 ┌──────────────┐
│ React 웹 앱 │ ──────▶ │ BFF (/api/*) │ ────▶ │ server-web │
│ 서비스워커 · 입력 큐 │ 9701 │ · 기기 토큰 · 작업자 세션 │ 토큰 │ /api/v5/... │
└──────────────────────┘ │ · 범위 검사 · 감사 로그 │ 하나 └──────────────┘
└──────────────┬───────────────┘
│ JDBC
플랫폼 DB pp · 스키마 pf
| 부분 | 무엇 | 저장소 안 위치 |
|---|---|---|
| 웹 | React 19 + TypeScript + Vite PWA. 서비스워커, 오프라인 입력 큐(IndexedDB), 화면별 마지막 값(localStorage). 그래프는 ECharts 6 — 그래프가 있는 화면을 열 때만 받는 별도 청크다 | web/ |
| BFF | Spring Boot 4.1 WAR, 플랫폼 공용 Tomcat 10.1(runtime-base) 위에서 돈다. 디스패처는 /api/* 에만 걸린다. 범위 검사(scope/), 정비(maintenance/), 불량(defect/), 리포트(floor/ReportController)가 여기 있다 | src/plantpulse/frontline/ |
| DB | 플랫폼 데이터베이스 pp 의 pf 스키마(2026-10-29 전에는 frontline), 플랫폼의 기존 DB 계정. 표는 fl_ 로 시작한다. 스키마는 Flyway 로 올린다(V1~V14) | src/db/migration/ |
| 이미지 | docker.kopens.io/pp/plantpulse-frontline-web. 포트 9701(TLS) · 9700(상태 확인만) | Dockerfile |
스키마 이력
| 판 | 무엇 |
|---|---|
| V1~V4 | 작업자 · PIN · 기기 · 설정 · 이유 목록 · 교대 메모 · 감사 로그, 화면 언어 · 테마 |
| V5 | 언어 칸의 CHECK IN ('ko','en') 을 지우고 fl_operator.lang · fl_operator_revision.lang · fl_device.default_lang 을 VARCHAR(35)(BCP 47)로 늘렸다. 허용 언어는 스키마가 아니라 서버의 목록 한 곳이 정한다 |
| V6 | fl_setting 의 kiosk.micro_stop_seconds 행을 지웠다. 짧은 정지 기준은 플랫폼 설정이다(옛 값은 옮기지 않는다) |
| V7 | 맡은 범위. 아래 |
| V8 | 감사 로그를 사람별로 쪽 단위로 읽는 색인 |
| V9 | 작업자 로그인과 플랫폼 직원(pp.mm_employee)의 연결 칸 |
| V10 | 작업자 계정을 플랫폼으로 옮김. 로그인 캐시 fl_account_cache 와 동기화 상태 fl_account_sync 를 만들고, 세션이 플랫폼 직원 ID 를 가리키게 바꿨다(기존 세션은 지운다 — 올린 뒤 모든 태블릿이 다시 로그인한다). 옛 fl_operator · fl_operator_scope 는 플랫폼으로 옮긴 것을 확인한 뒤 V11 에서 지운다 |
| V11 | 옛 작업자 표(fl_operator · fl_operator_scope 와 각 이력 표)를 지운다. 플랫폼으로 옮기기가 끝난 것으로 표시된 설치(fl_account_sync.imported_at)에서만 지우고, 아직 못 옮긴 설치는 남겨 둔다 |
| V12 | 설비 «주요 값» fl_key_value — 설비마다 개요에 먼저 보일 태그 6개까지(태그 id 와 순서만, 값은 V5 에서 읽는다) |
| V13 | 고객 로고 fl_brand_logo — 밝은 판 · 어두운 판 PNG 한 장씩 |
| V14 | 기기 기본 범위 fl_device.default_scope_type 의 허용 단계를 SITE · AREA · LINE · ASSET 넷으로 넓혔다(전에는 LINE · ASSET). 있던 행은 그대로 |
스키마 이름 바꾸기. 앱은 Flyway 보다 먼저 frontline 스키마가 있고 pf 가 없을 때만 ALTER SCHEMA frontline RENAME TO pf 를 한 문장으로 한 번 한다. 표 · 데이터 · Flyway 이력 · 설정(ai.url · ai.api_key)이 그대로 옮겨지고 복사는 없다. 다시 기동하면 pf 가 있으니 아무것도 하지 않는다(멱등). 새 설치는 Flyway 가 pf 를 만든다. 둘 다 있으면 추측하지 않고 경고만 남기고 pf 를 쓴다. DB 가 답하지 않으면 기동하지 않는다 — 건너뛰면 Flyway 가 빈 pf 를 만들어 데이터가 사라진 것처럼 보이기 때문이다.
작업자 계정 옮기기. 기동 뒤 첫 동기화 전에 fl_operator · fl_operator_scope 를 V5 POST /app-accounts/frontline/import 로 한 번 보낸다. PIN 해시는 그대로 옮긴다(다시 해시하지 않는다). 직원에 연결되지 않은 로그인은 플랫폼이 직원 EMP_FL_<로그인 번호> 를 만들어 붙인다. 플랫폼에 이미 있는 로그인 번호는 플랫폼 쪽을 둔다(EXISTS). 모든 행이 CREATED · EXISTS 로 돌아와야 끝난 것으로 표시하고, 아니면 다음 회차에 다시 보낸다(멱등).
플랫폼과의 경계
OEE · 가동 · 성능 · 품질 · 정지 시간 · 손실 · MTBF · MTTR 은 플랫폼 V5 가 준 값만 보인다. 플랫폼이 주지 않는 값은 빈칸이나 «데이터 없음» 으로 두거나 숨기고, 0 이나 추정값으로 채우지 않는다. 화면에서 계산하는 것은 표시용 진행률(양품 ÷ 계획) 하나뿐이고, 그래프는 V5 값을 그대로 그리며 축 단위만 바꾼다. 그래서 태블릿 · 플랫폼 웹 · 리포트 · AI 가 같은 숫자를 본다.
- 플랫폼 데이터는 V5 로만 읽고 쓴다. 같은
pp데이터베이스에 있어도 플랫폼 표를 직접 읽거나 쓰지 않고,pf스키마에 플랫폼 데이터 사본을 두지 않는다. 정비 호출과 불량 기록도 앱에 사본이 없다 — 정본은 V5 의 정비 오더 · 불량 기록이다. - 예외는 하나 — 로그인 캐시
fl_account_cache. 플랫폼이 재시작하는 동안에도 현장이 로그인할 수 있어야 해서 둔다. 아래 작업자 계정. 다른 표에는 PIN 해시 · 직원 이름 · 재직 상태를 두지 않는다(시험이 막는다). - MES 기능을 넣지 않는다. 작업자 계정은 로그인 수단(로그인 번호 · 역할 · PIN)과 맡은 범위(무엇을 볼 수 있나)뿐이다. 맡은 범위는 MES 의 인력 배정이 아니다 — 자격 · 교대 편성 · 개인 실적 · 인원 집계는 없다. 오더는 고르고 시작 · 일시정지 · 재개 · 종료 · 중단(사유 코드)하고, 시간 기준 오더의 완성률을 보고한다 — 발행 · 편집은 플랫폼에서 한다.
pf스키마에 두는 것은 기기(기본 언어 · 화면 테마 포함) · 설비별 이유 목록 · 키오스크 설정 · 교대 메모 · 감사 로그 · 세션, 그리고 로그인 캐시(fl_account_cache·fl_account_sync)다. 작업자 계정(로그인 번호 · PIN 해시 · 역할 · 맡은 범위 · 화면 언어 · 테마)의 정본은 플랫폼pp.mm_app_account·pp.mm_app_account_scope다.- V5 를 부를 때 보는 사람의 언어를
Accept-Language로 싣는다. 정지 이유 · 고장 모드 · 불량 원인 이름은 V5 가 보낸 그대로 보인다.
판정도 플랫폼이 한다
화면에 쓰는 판정은 앱이 하지 않고 V5 가 준 값을 그대로 쓴다. 앱 쪽에 있던 판정 코드(계획 · 출처 · 길이 비교, 교대 창 고르기와 8시간 대체창)는 지웠다.
| 판정 | V5 의 출처 |
|---|---|
| 이번 교대의 창 | GET /calendar/shift-schedule/current. 근무표가 없을 때의 «최근 9시간» 도 V5 가 준다(사람이 12시간을 고르면 그만큼 넓힌다). V5 가 창을 주지 못하면 카드가 그 거절을 그대로 말하고, 앱이 창을 지어내지 않는다 |
| «지금 계획» 오더 | 같은 응답의 planned_order_id |
| 이유가 빠진 정지 | 정지마다의 needs_reason |
| 짧은 정지 | 정지마다의 short. 기준은 GET /config/PP_STOP_MICRO_STOP_SECONDS(기본 60초, 0~1800, 0 이면 모두 묻는다) |
앱이 부르는 V5
| 읽기 | 쓰임 |
|---|---|
GET /site · /asset/lines/by-site/… · /asset/equipments/by-line/… · /path/asset/… | 범위 고르기(사이트 → 라인 → 설비) |
GET /asset/… · GET /site/… · GET /asset/equipments/by-line/… | 맡은 범위 노드 해석(부모 사슬). 라인의 계획 대비는 설비 목록으로 묻는다 |
GET /asset/…/status | 상태와 지속 시간 |
GET /kpi/oee/status/current · /kpi/period | 교대 OEE, 대시보드 기간 지표 |
GET /kpi/daily | 대시보드 · 리포트의 일별 추이 |
GET /ram/breakdown?by=CAUSE · /failure-dictionary/alarm-cause | 고장 원인 파레토와 원인 이름 |
GET /asset/…/stops · /asset/…/stop-loss | 타임라인, 이유가 빠진 정지, 짧은 정지, 정지 이유별 손실 |
GET /asset/…/timeline/alarms | 알람(min_priority, limit=500). BFF 가 진행 중(최근 7일 안에 켜진 것)과 오늘 끝난 것만 고른다 |
GET /stop-reasons | 원인 사전(이유 버튼, 설정의 정지 이유 목록) |
GET /calendar/shift-schedule/current | 이번 교대 창(asset_id). 응답 칸은 from_ts · to_ts · shift_code · shift_date · from_plan · planned_order_id |
GET /config/PP_STOP_MICRO_STOP_SECONDS | 짧은 정지 기준(1분 캐시) |
GET /order/query · /order/current-asset · /order/… · /order/…/quantity | 오더 목록 · 진행 오더 · 상세 · 수량 |
GET /order/…/revisions · /order/…/progress | 변경 이력, 완성률 보고 이력. V5 는 사번만 주므로 BFF 가 로그인 캐시에서 찾은 이름을 names 로 붙인다(없는 사번은 사번 그대로) |
GET /maintenance-orders · /maintenance-orders/options · /maintenance-orders/… | 정비 보드(open=true&order=asc&limit=100, 범위 · 담당자 거르기), 증상 · 코드 목록, 취소 전 확인 |
GET /asset/…/defects/options · /asset/…/defects · /order/…/defects · /defects/… | 불량 원인 목록, 설비 · 오더의 불량 기록 |
GET /asset/qr/{token} | 찍은 QR 라벨의 토큰 → 설비. BFF 만 부르고, 토큰은 로그에 남기지 않는다 |
GET /order/deviation-summary · /defects/summary · /maintenance-orders/summary | 리포트의 계획 대비 · 불량 원인 · 정비 요약(느린 읽기) |
GET /order/query 는 워크오더 화면을 위해 line_id · site_id · status=END,ABORTED · end_since · end_before · include=oee · sort · limit=500 · order_id 거르기를 쓴다. 정비 보드 읽기는 BFF 가 5초, 정비 · 불량 options 는 60초 공유 캐시를 두고, 쓰면 비운다.
| 쓰기 | 쓰임 | 헤더 |
|---|---|---|
PUT /asset/{asset}/stops/{started_at}/reason | 정지 이유 기록 | Idempotency-Key |
PUT /order/{order}/quantity | 수량 정정(양품 · 불량 합계 + 사유) | Idempotency-Key |
POST /order/{order}/start · pause · resume · end | 오더 전이. 본문에 누른 시각 occurred_at(epoch ms) | If-Match (오더 version) |
POST /order/{order}/abort | 오더 중단. abort_code · abort_notes · occurred_at | If-Match · Idempotency-Key |
POST /order/{order}/progress | 완성률 보고. percent_complete(0~100 정수) · note · reported_at | If-Match · Idempotency-Key |
POST /asset/{asset}/maintenance-orders | 정비 호출(maintenance_kind_code: CORRECTIVE, failure_mode_code · 설명 · 부른 사람) | Idempotency-Key · X-PP-Channel: FRONTLINE |
POST /maintenance-orders/{id}/transition | 받음 · 수리 시작 · 완료(status_code + 조치 · 부품 · 고장 코드), 취소(status_code: CANCELLED + cancel_reason_code). version 은 본문에 싣는다 | Idempotency-Key · X-PP-Channel |
POST /asset/{asset}/defects | 불량 기록 | Idempotency-Key · X-PP-Channel |
POST /defects/{id}/cancel | 불량 기록 취소 | If-Match · X-PP-Channel |
- 모든 쓰기에
X-PP-Performed-By: 로그인한 사번을 싣는다. V5 는 그것을performed_by로 남기고changed_by는 앱 계정으로 둔다. 오프라인 큐가 나중에 보낼 때도 누른 사람의 사번이 실린다. - 쓰기에는
Idempotency-Key를 붙인다. 오프라인 큐가 다시 보내도 V5 가 같은 쓰기를 두 번 받지 않는다. - 오더 전이는
If-Match로 version 을 맞춘다. 그 사이 다른 사람이 바꿨으면 409 가 돌아오고 화면은 «다른 사람이 먼저 바꿨습니다» 를 보인다. - 오더 전이 · 중단 · 불량 기록은 누른 시각을 싣는다. 큐에서 나중에 보내도 실적 시각은 누른 시각이다.
- «이전 오더 끝내고 시작» 은 BFF 가
end와start를 차례로 부른다. 반쪽마다 따로 멱등 키를 쓰고 두 반쪽에 같은 누른 시각을 싣는다. 둘째가 실패하면 첫째는 되돌리지 않고 그대로 알린다.
V5 자체는 API 문서를 본다.
QR 라벨로 들어올 때
플랫폼의 공개 페이지 «frontline 에서 열기» 는 <PP_UI_FRONTLINE_URL>/#/qr/<토큰> 으로 온다. 주소에는 설비 ID 가 없다.
- 앱은 토큰을 주소에서 바로 지우고 그 탭의 sessionStorage 에 둔다(
fl.qr_pending). 로그인 전에는 서버로 보내지 않는다. - 로그인하면 토큰을 꺼내 BFF
POST /api/v1/qr/resolve의 본문으로 보낸다 — 경로에 넣으면 지나는 접근 로그마다 남기 때문이다. - BFF 는 토큰 꼴(base64url 43자)을 먼저 보고, V5
GET /asset/qr/{token}으로 설비를 찾고, 그 계정 · 태블릿이 그 설비를 고를 수 있는지 범위 칩과 같은 규칙으로 본다. - 통과하면 앱이 범위를 그 설비로 옮겨 «지금» 을 연다.
| 응답 | 코드 | 화면 |
|---|---|---|
| 400 | qr_token | 라벨 토큰이 아니다 |
| 404 | qr_unknown | 재발급됐거나 설비가 없어졌다 |
| 403 | scope_denied | 맡은 범위 밖이다 |
작업자 계정 V5
작업자 계정은 플랫폼 V5 /api/v5/app-accounts/frontline 에 있다. ADMIN 토큰 또는 플랫폼 마스터 키만 부를 수 있다(변경 피드에는 PIN 해시가 실리기 때문이다).
| 호출 | 쓰임 |
|---|---|
GET /app-accounts/frontline | 계정 목록(직원 이름 · 재직 상태 포함). 앱의 읽기 전용 «작업자» 탭 |
GET /app-accounts/frontline/{employee_id} | 한 건(ETag = version) |
PUT /app-accounts/frontline/{employee_id} | 계정 만들기 · 고치기(로그인 번호 · 역할 · 허용 · 임시 PIN) |
DELETE /app-accounts/frontline/{employee_id} | 로그인 삭제(직원은 남는다). 마지막 관리자는 지울 수 없다 |
PUT /app-accounts/frontline/{employee_id}/scope | 맡은 범위 — {"all": true} 또는 {"nodes": [...]} |
POST /app-accounts/frontline/pin/verify | 키패드 PIN 확인. 언제나 200 과 결과(OK · INVALID · LOCKED · DISABLED · PIN_RESET_REQUIRED · EMPLOYEE_INACTIVE · EMPLOYEE_ON_LEAVE) |
POST /app-accounts/frontline/{employee_id}/pin | 본인 PIN 변경(지금 PIN · 새 PIN) |
POST /app-accounts/frontline/{employee_id}/pin/reset | 관리자 PIN 초기화(임시 PIN, 잠금도 푼다) |
POST /app-accounts/frontline/{employee_id}/unlock | 잠금 해제 |
GET /app-accounts/frontline/changes | 변경 피드(커서). 로그인 캐시가 이것을 읽는다 |
POST /app-accounts/frontline/import | 앱의 옛 로그인을 옮기기(멱등) |
- 로그인. 플랫폼에 닿으면 BFF 는
pin/verify만 부른다. 실패 횟수 · 잠금(5번 틀리면 5분) · 퇴직 · 휴직 거절은 플랫폼이 한다. - 로그인 캐시.
AccountSync가 60초마다 변경 피드를 커서부터 끝까지 읽어fl_account_cache를 맞춘다(스냅샷에 PIN 해시 · 재직 상태 · 범위가 실린다). 캐시는 플랫폼에 닿지 않을 때만 읽고, 마지막 성공 동기화가accounts.offline_days(관리자 «키오스크» 화면, 기본 7일, 1~30일)보다 오래면 오프라인 로그인을 거절한다(fail-closed,503 offline_login_expired). - 오프라인 실패. 캐시로 확인할 때 틀린 PIN 은 앱이 세고(사람별 5번이면 5분, 기기별 20번이면 15분) 플랫폼으로 보내지 않는다.
- 오프라인에서 안 되는 것. PIN 변경 · 화면 언어 · 테마 바꾸기는 플랫폼에 닿을 때만 된다. 계정 편집 · PIN 초기화 · 잠금 해제는 앱에 없다(콘솔 «직원»).
- 본인이 바꾸는 것은 PIN(BFF
PUT /v1/session/pin) · 화면 언어 · 테마(PUT /v1/me/preferences)와, «내 프로필» 의 연락처 · 사진(PUT /v1/me/profile·PUT /v1/me/photo, 플랫폼 직원 정보)이다.
이중화(HA)에서
플랫폼 이중화의 PG 메타 동기화가 frontline 것도 대기 클러스터로 옮긴다.
| 따라가는 것 | 따라가지 않는 것(노드마다) |
|---|---|
작업자 계정과 맡은 범위 pp.mm_app_account · pp.mm_app_account_scope(PIN 해시 포함) | 세션 · 기기 등록 코드 · 감사 로그 · 교대 메모 · 멱등 기록 |
키오스크 설정 pf.fl_setting · 설비별 정지 이유 목록 pf.fl_asset_reason_list · 등록한 태블릿 pf.fl_device | 로그인 캐시 fl_account_cache · fl_account_sync — 노드마다 자기 플랫폼의 변경 피드로 채운다 |
AI 키(fl_setting 의 ai.api_key)는 metasync.include.tokens=true 일 때만 |
플랫폼이 재시작될 때
플랫폼 재시작은 보통 1~3분, 길어야 10분쯤이다. 그동안 앱은 멈추지 않는다.
- BFF 는 V5 나 자기 DB 에 닿지 못하면
503과X-FL-Offline헤더로 «지금은 안 됨» 을 알린다. 거절(4xx)과 구분된다. - 웹은 그 답을 받으면 연결 끊김으로 표시하고, 화면은 마지막 값을 «3분 전 기준» 처럼 받은 때와 함께 보인다. 지어낸 값은 없다 — BFF 가 마지막으로 준 값뿐이다.
- 큐에 넣는 것은 정지 이유 · 오더 전이(중단 포함) · 완성률 보고 · 정비 호출 · 불량 기록이다. 같은 오더 · 정지를 건드리는 입력은 큐 순서를 지키고, 앞의 것이 거절되면 뒤의 것은 보내지 않고 사람의 판단을 기다린다.
- 복귀는
/api/health를 2 · 4 · 8 · 15초(이후 15초 반복) 간격으로 물어 DB 와 플랫폼이 모두UP이면 연결로 되돌리고, 큐를 보내고, 화면을 다시 읽는다. - 수량 정정 · 정비 단계 이동 · 정비 호출 취소 · 불량 기록 취소는 큐에 넣지 않는다 — 그 순간의 값과 version 을 기준으로 판단하는 쓰기라서 연결된 상태에서만 받는다. 아직 큐에 있는 정비 호출의 취소는 큐에서 지우는 것으로 끝난다.
- V5 읽기의 시간 제한은 연결 2초, 응답 5초다. 리포트의 무거운 요약(계획 대비 · 불량 · 정비)만 20초까지 기다리고, 넘어도 회로를 열지 않는다 — 그 카드만
504 platform_slow로 실패하고 연결 끊김으로 넘어가지 않는다.
PlantPulse AI 채팅
태블릿의 «AI 에게 묻기» 는 BFF 가 PlantPulse AI 의 현장 채팅 API 로 중계한다. 플랫폼 V5 와는 별개 연결이라, AI 가 느리거나 꺼져도 플랫폼 연결 끊김으로 넘어가지 않는다.
| BFF | AI 쪽 | 쓰임 |
|---|---|---|
GET /api/v1/chat/capabilities | GET /api/v1/chat/capabilities | 버튼을 보일지(60초 기억). 범위가 없는 사람은 AI 에 묻지 않고 «없음» |
POST /api/v1/chat/stream | POST /api/v1/chat/stream | 답을 SSE 로 받아 그대로 흘려 보낸다 |
POST /api/v1/chat/cancel | POST /api/v1/chat/cancel | 멈춤. 같은 사람의 대화만 |
- 주소와 키는 앱 DB
fl_setting의ai.url·ai.api_key에 둔다(설정 → AI). 키는X-API-Key로만 나가고 로그 · 응답 · 예외 문장에 싣지 않는다. - 요청에는 «대신 묻는 사람» 으로 로그인한 사번(
X-PP-Performed-By와 같은 값)과 그 사람의 범위(범위 검사와 같은 규칙)를 싣는다. 범위가 없으면 AI 를 부르지 않고 403 이다. - 화면 언어는
Accept-Language로 싣는다.
인증 — 세 층
| 층 | 무엇 | 어디에 |
|---|---|---|
| 플랫폼 키 | 플랫폼 마스터 API 키. BFF 만 갖는다 | 컨테이너 환경 변수 PP_MASTER_API_KEY |
| 기기 토큰 | 태블릿을 등록할 때 발급하는 난수. DB 에는 해시만 둔다. 만료는 없고 관리자가 해제하면 끝난다. Authorization: Bearer 로 실린다 | 그 브라우저의 localStorage(fl.device_token). 다른 브라우저 · 시크릿 창 · 사이트 데이터를 지운 브라우저에는 없으므로 미등록이다 |
| 작업자 세션 | 로그인 번호 + PIN 으로 연다. PIN 은 플랫폼이 확인한다(닿지 않으면 로그인 캐시). 세션은 플랫폼 직원 ID 를 가리키고, 자동 로그아웃 시간까지 유효하며 기기 토큰에 묶인다. X-FL-Session 으로 실린다 | 그 탭의 sessionStorage — 새로고침해도 남고 탭을 닫으면 끝난다(저장소를 못 쓰면 메모리) |
- 플랫폼 V5 는 USER 역할 토큰을 받지 않는다(ADMIN · API 만). 그래서 사람마다 플랫폼 토큰을 줄 수 없고, 마스터 키 하나로 부른다. 누가 했는지는 V5 기록의
performed_by(위X-PP-Performed-By)와 앱의 감사 로그(fl_audit— 사번 · 기기 · V5 경로 · 응답 코드 · Idempotency-Key) 둘에 남는다. - 플랫폼 감사 로그에서 이 앱의 호출은 행위자
master-api-key로, 프록시 로그에서는 User-AgentPlantPulse-frontline-web/<버전>으로 가른다. - 모든 BFF 핸들러는 접근 수준을 명시한다 — 표시가 없으면 거부가 기본이다.
- 토큰 · PIN 은 로그 · 응답 · 예외 문장에 싣지 않는다.
역할 검사
핸들러마다 @Access 로 허용 역할을 적는다. 화면에서 버튼을 숨기는 것과 별개로 서버가 다시 막는다.
| 동작 | 허용 역할 |
|---|---|
| 정비 받음 · 수리 시작 · 완료 | 정비 · 관리자 |
| 오더 중단 | 반장 · 관리자 |
| 정비 호출 · 정비 호출 취소 · 불량 기록 · 불량 기록 취소 | 네 역할 모두. 취소는 핸들러가 다시 본다 — 본인이거나 반장 · 관리자 |
설정(/v1/admin/) | 관리자 |
범위 검사
역할 다음에 맡은 범위를 검사한다. 모든 현장 요청은 scope/ScopeInterceptor 한 곳을 지난다(인증 인터셉터 다음).
- 대상은 경로 · 쿼리에 실린 설비 · 라인 · 오더(V5 로 설비를 확인) · 정비 오더 · 불량 기록이다. 읽기는 범위 안이어야 하고, 쓰기는
RW여야 한다. - 범위 밖이면
403 scope_denied, 보기 전용에 쓰려 하면403 scope_read_only다. @OpenToReadOnly(정비 호출, 정비 호출 취소)는 읽기 수준으로만 검사한다 — 설비를 «보기» 로만 받은 사람도 정비를 부를 수 있어야 하기 때문이다. 남의 호출 취소는 핸들러가 다시 막는다.- 로그인 범위 = 계정 범위 ∩ 기기 기본 범위(
ScopeGuard.within). 계정 노드가 기기 노드와 같거나 그 아래면 그대로 두고, 기기 노드보다 위면 기기 노드를 그 계정 노드의 access 로 준다. 겹치지 않는 노드는 버린다. 계정이scope_all이면 기기 노드RW다. 기기가 넓혀 주거나RO를RW로 올리는 일은 없다. 관리자 역할과 기본 범위가 없는 기기는 좁히지 않는다. 기기 기본 범위는 사이트 · 구역 · 라인 · 설비 어느 단계든 된다(DeviceScope). - 노드는 아래로 상속된다. 한 설비가
RW와RO를 함께 받으면RW가 이긴다. 노드는 V5 로 부모 사슬을 해석하고 5분 보관한다. V5 에서 지워진 노드는 접근 0 이다. - 대시보드 · 리포트는 그 노드 자체가 범위 안이어야 한다. 설비 하나를 받았다고 그 라인의 대시보드가 열리지 않는다.
- 목록은 핸들러가 좁힌다 — 범위 트리, 라인 카드 · 알람 수, 워크오더(행마다 access 동봉), 정비 보드.
- fail-closed 다.
scope_all도 아니고 노드도 없으면 아무것도 읽지 못한다. 범위는 플랫폼 계정(pp.mm_app_account.scope_all·pp.mm_app_account_scope)에서 오고, 앱에는 범위를 고치는 API 가 없다. V5 는 빈 노드 목록을 «아무것도 없음» 으로 받고, 노드는 200개까지다. - 관리자 역할은 범위 검사 없이 전체다. 관리 API(
/v1/admin/)는 범위 검사를 건너뛰고 관리자 역할만 본다. - 로그인하지 않은 태블릿(«PIN 없이 «지금» 보기»)은 기기의 기본 범위만
RO로 읽는다.
로그인 응답에는 범위 칸이 넷 실린다.
| 칸 | 뜻 |
|---|---|
scope | {"all": true} 또는 노드 목록(id · kind · access) |
scope_none | 이 태블릿에서 볼 수 있는 것이 전혀 없음 — 웹은 메뉴 없이 안내와 로그아웃만 보인다 |
scope_none_reason | scope_none 일 때 까닭. ACCOUNT 는 계정에 범위가 없음(«범위 미지정»), TABLET 은 계정 범위가 이 태블릿 범위와 겹치지 않음(«이 태블릿에서 볼 수 있는 범위가 없습니다») |
home_scope | 화면이 시작할 범위(마지막에 고른 것, 없으면 기기 기본 범위)가 그 사람 범위 밖일 때 대신 열 첫 설비나 라인 |
환경 변수
| 변수 | 무엇 |
|---|---|
PP_TZ | 표시 시간대(필수). 저장은 UTC epoch, 화면은 이 시간대로 보인다. 날짜 넘기기 · «오늘» · 자정을 넘는 창의 날짜 묶음도 이 시간대다 |
PP_LANG | 설치 언어(필수). 지원 언어 목록의 태그(지금 ko · en)여야 하고, 대소문자와 지역 변형(en-US)도 받는다. 목록에 없으면 기동하지 않는다 |
PP_SERVER_URL | 플랫폼 V5 주소 |
PP_MASTER_API_KEY | 플랫폼 마스터 API 키. server-web · batch-web · warehouse-web 과 같은 값이다 — 바꾸면 넷을 함께 재시작한다 |
PP_POSTGRES_HOST · PP_POSTGRES_PORT | 데이터레이크 PostgreSQL |
PP_DB_NAME · PP_PG_USER · PP_PG_PASSWORD | 플랫폼 데이터베이스(pp)와 계정. server-web · batch-web · warehouse-web 과 같은 이름 · 같은 값이다. frontline 전용 계정 · 비밀번호는 없다 |
PP_MASTER_API_KEY · PP_PG_PASSWORD · PP_POSTGRES_HOST 가 없으면 컨테이너가 뜨지 않는다. JVM 힙은 512 MB(direct 128 MB)로 고정이다. 맡은 범위 · 정비 · 불량 · 리포트 때문에 새로 생긴 변수는 없다.
새 화면 언어를 더하려면 웹 번들에 web/src/i18n/<태그>.json 을 넣고 서버의 지원 언어 목록에 한 줄을 더한다. 스키마는 바꾸지 않는다(V5).
다음 단계에 열릴 칸
아래 칸은 아직 앱에 없다. 화면에 자리를 두지 않았고, 플랫폼에 일반 V5 API 가 생기면 BFF 에 호출을 붙여 연다.
| 칸 | 지금 | 플랫폼에 필요한 것 |
|---|---|---|
| 주별 추이 · 지난 기간 비교 | 없다(일별 추이는 /kpi/daily 로 열렸다) | 주 단위 기간 계열 |
| 에너지(EMS) | 없다 — 대시보드 탭도 리포트 절도 없다 | EMS API |
| 라인 단위 정지 이유별 손실 | 없다 — 설비에서만 그린다 | 라인 범위 정지 손실 집계 |
| 시간별 실적 / 목표 | 없다 | 시간별 목표 · 실적 API |
| 교대 손실 폭포 | 없다 — 교대 OEE 창에는 OEE · 가동 · 성능 · 품질과 정지 이유 상위만 있다 | 손실 분해 |
| 정지 수동 등록 · 시간대별 수량 | 없다 | 정지 쓰기 경로, 시간대별 수량 쓰기 |
이전 판에서 이 표에 있던 칸 가운데 아래는 열렸다.
| 칸 | 연 방법 |
|---|---|
| 누가 했나(플랫폼 기록에 사번) | 모든 쓰기의 X-PP-Performed-By → V5 performed_by |
| 정비 호출 → 받음 → 수리 시작 → 완료 | V5 /maintenance-orders |
| 불량 수량 + 사유 | V5 /defects |
| 일별 추이 | V5 /kpi/daily |
| 교대 · 일 · 주간 리포트 | BFF GET /v1/report 가 V5 의 KPI · 오더 · 요약 API 를 모은다 |