다크모드
콘솔 stage 모델 — BE 참고
관리비 상용화 펀치리스트 Critical C-1 + Important I-7 해소. 설계 정본:
docs/superpowers/specs/2026-07-09-console-stage-model-design.md.
1. 개요
부과 → 청구 → 수납 → 이월 파이프라인에서 "확정" 상태를 담는 mutable 저장소가 이전에 없었다(billingLines.js의 assessmentStatus는 탭 배지용 정적 per-edition config일 뿐). 본 문서는 신설 상태머신 useConsoleStage의 상태·게이트 규칙·키잉·불변식·영속을 기술한다.
오너 결정(2026-07-09): 순차 게이트 + 재오픈 모델. 저장(draft)은 스코프 아웃 — 편집 draft 개념이 코드베이스에 없어 숨김 처리.
2. 데이터 모델
2-1. 진실원
src/composables/useConsoleStage.js 모듈 레벨 reactive 싱글톤이 전 라인·에디션의 stage 상태를 소유한다.
키 구조: state[key][stage]
state = {
'service-charge@office-uuid-1:actual': { charging: 'confirmed', invoicing: 'progress', collecting: 'pending', forwarding: 'pending' },
'service-charge@office-uuid-2:actual': { charging: 'pending', invoicing: 'pending', collecting: 'pending', forwarding: 'pending' }
}key = buildStageKey(scopedLine, edition) = \${scopedLine}😒{edition ?? 'default'}`. 운영 UUID Office의scopedLine은line@officeId, 로컬 데모 ID는 기존line`이다. Office+line+edition이 완전히 독립한다.- 단계 순서:
STAGES = ['charging', 'invoicing', 'collecting', 'forwarding'](헤더 4카드와 일치). - 상태값:
STAGE_STATUSES = ['pending', 'progress', 'confirmed']. - 기본값은 전부
pending(DEFAULT_STAGE_STATE,Object.freeze). 미착수 line+edition은 저장소에 엔트리가 없고, 조회 시 DEFAULT clone으로 lazy 폴백(첫 mutation 전까지 영속하지 않음). - 이전에는 charging=확정·invoicing=진행중이 하드코딩돼 있었는데, 이는 가짜 상태였다 — 정직한 기본값은 전부 대기이며 사용자가 시작/확정으로 구동해야 진행된다.
2-2. 상태 전이
pending --start--> progress --confirm--> confirmed
|
reopen (게이트)
v
progressprogress --(stepBack, 게이트 없음)--> pending 도 가능(하류에 영향 없어 게이트 불필요).
3. 게이트 규칙 (as-built)
순수 함수 시그니처는 전부 (line, edition, stage) — Vue 컴포넌트/라우터 의존 없음.
3-1. canStart(line, edition, stage)
js
statusOf(stage) === 'pending' AND (첫 단계이거나 직전 단계가 'confirmed')- 첫 단계(
charging,stageIndexOf === 0)는 직전 단계 조건이 없어 항상 시작 가능(pending일 때). - 순차 게이트 — 건너뛰기 불가. 부과가 confirmed가 아니면 청구는
canStartfalse.
3-2. canConfirm(line, edition, stage)
js
statusOf(stage) === "progress";3-3. canReopen(line, edition, stage)
js
statusOf(stage) === 'confirmed' AND (마지막 단계이거나 다음 단계가 'pending')- cascade 방지: 다음 단계가 이미
progress/confirmed로 착수됐으면 현재 단계를 되돌릴 수 없다. 하류가 상류의 확정 데이터를 전제로 진행 중일 수 있기 때문.
3-4. setter — 게이트 위반 시 no-op (throw 없음)
| 함수 | 게이트 | 전이 | 위반 시 반환 |
|---|---|---|---|
start(line, edition, stage) | canStart | pending → progress | { ok: false, reason } |
confirm(line, edition, stage) | canConfirm | progress → confirmed | { ok: false, reason } |
reopen(line, edition, stage) | canReopen | confirmed → progress | { ok: false, reason } |
성공 시 { ok: true }. 상태는 절대 예외 없이 게이트로만 보호된다 — 호출자가 게이트를 미리 체크하지 않고 호출해도 불변식이 깨지지 않는다.
3-5. 편의 함수 (헤더 prev/next 전용)
stepForward(line, edition, stage):canStart면start, 아니면canConfirm이면confirm. 둘 다 아니면 no-op.stepBack(line, edition, stage):confirmed면reopen(게이트 적용).progress면 게이트 없이 바로pending으로 되돌림(착수 취소 — 하류에 영향 없어 게이트 불필요). 그 외 no-op.
이 두 함수는 게이트를 재정의하지 않는다 — 내부적으로 위 canStart/canConfirm/canReopen + start/confirm/reopen을 호출할 뿐이다. 게이트 로직은 §3-1~3-3 한 곳에만 존재.
4. 불변식
- 순서 불변: 이전 단계가
confirmed가 아니면 다음 단계는progress/confirmed로 전이 불가(canStart가 막음). - 하류 우선 불변: 다음 단계가 착수(
progress이상)된 이후에는 상류 단계를 되돌릴 수 없다(canReopen이 막음) — cascade 방지. - no-op 불변: 모든 setter는 게이트 위반 시 상태를 변경하지 않고
{ ok: false, reason }만 반환한다. 예외를 던지지 않는다. - Office+line+edition 독립: 한 Office의
service-charge:actual전이는 다른 Office나service-charge:provisional에 영향을 주지 않는다 — 키가 다르면 완전히 분리된 엔트리. - DEFAULT 불변:
DEFAULT_STAGE_STATE는Object.freeze되어 있고 절대 직접 변이되지 않는다. 모든 변경은 clone-replace(state.value = { ...state.value, [key]: nextEntry }).
5. 영속
localStorage키:'leyve.billing.consoleStage'.useBillingGeneralSettings/useLateFeePolicy와 동일한 clone-replace 관용.watch(state, ..., { deep: true })가 변경 시마다 전체 상태를 JSON 직렬화해 저장.- 로드 시
sanitizeState()가 손상/부분 형태를 방어 — 알 수 없는 stage 값은 무시하고 DEFAULT로 폴백, entry 자체가 객체가 아니면 스킵.
6. 확정의 현재 의미 — 상태 + 게이트만, 실 잠금 아님
confirm(stage)가 하는 일은 오직:
state[key][stage]를'confirmed'로 바꾼다.- 이로 인해 다음 단계의
canStart가 true가 된다(게이트 해제). - 헤더/액션바 UI가 이 상태를 읽어 배지·버튼 활성화를 파생한다.
확정은 다음을 하지 않는다 (follow-up, 스코프 아웃):
- 검침·부과 입력 폼을 실제로 disable하지 않는다 — 확정 후에도 사용자가 값을 편집할 수 있다.
- 회계 전기(C-2,
useGeneralLedger.post)와 상호 게이팅하지 않는다 — 확정 여부와 무관하게 전기 버튼은 기존 로직(unpostedCount)대로 동작한다. - 저장(draft) 개념이 없으므로 "확정 전 임시저장" 흐름이 없다.
- 확정/재오픈 시 확인 다이얼로그가 없다 — 클릭 즉시 전이된다.
BE가 실제 서버측 상태머신·감사로그·잠금을 설계할 때 이 문서의 게이트 규칙(§3)을 1차 계약으로 참조할 수 있으나, 현재 구현은 클라이언트 로컬 UI 상태(localStorage)이며 서버 영속·트랜잭션 보장은 없다.
7. API 요약
| 함수 | 설명 |
|---|---|
statusOf(line, edition, stage) | 현재 상태 문자열 |
canStart/canConfirm/canReopen(line, edition, stage) | 게이트 판정 |
start/confirm/reopen(line, edition, stage) | 전이 setter (게이트 위반 시 no-op) |
stepForward/stepBack(line, edition, stage) | 헤더 prev/next 편의(내부적으로 위 게이트 재사용) |
buildStageKey(line, edition) | 저장 키 조립 |
useConsoleStage() | 현재 라우트(useBillingEdition) 바인딩 래퍼 — line/edition 인자 생략, 나머지 동일 |
8. 스코프 경계
모바일 단계 헤더의 가로 스크롤바 숨김은 CSS 표현 계층 변경이며 API·상태머신·DB 계약에는 영향이 없다. 터치·휠 가로 이동 동작과 단계 상태 데이터는 그대로 유지된다.
포함: stage 상태머신·순차 게이트·재오픈·line+edition 독립·localStorage 영속·부과/청구/수납 ActionBar 배선·헤더 4카드 실화(2026-07-10 갱신: StageWidgetOrg 공유 컴포넌트로 추출되어 관리비 콘솔 헤더 22개 전체에 동일 실 상태가 반영된다 — 상세: docs/handoff/frontend/billing-console-stage.md §3-2).
제외 (YAGNI/follow-up):
- 저장(draft) 모델.
- 확정 시 입력 폼 실 잠금(검침·부과 입력 disable 등).
- 이월(forwarding) 전용 시작/확정 버튼(헤더 prev/next로만 구동 가능).
- 확정/재오픈 확인 다이얼로그.
- 확정과 회계 전기(C-2)·기타 액션 간 상호 게이팅.
- 서버측 영속·감사로그(현재 클라이언트 localStorage 뿐).
9. 부과상세 데이터 계약
부과상세 시트는 별도 API 응답이나 정적 예시값을 사용하지 않고 목록과 같은 charge 레코드를 chargeCode로 조회한다. 현재 변경은 프론트엔드 선택·표시 배선이며 API·DB 스키마에는 영향이 없다. 서버 연동 시에도 목록과 상세가 같은 canonical charge ID와 금액 필드를 사용해야 한다.
10. 조정 planned surface — 서버 계약 없음
정기 관리비 콘솔 레일은 부과 → 조정 → 청구 순서에서 조정의 예정 위치를 보존한다. 현재 adjustment 탭은 availability='planned', actual 전용이며 disabled·저채도·미구현으로 렌더된다.
- 조정 route로 이동하지 않고 API를 호출하지 않는다.
- stage 상태머신의
STAGES에는 adjustment를 추가하지 않았다. - 현재 부과·청구 확정 사이에 새 전이·재오픈·감사 규칙이 생긴 것으로 해석하지 않는다.
- 서버 조정 모델을 구현할 때는 원전표/부과 Fact를 덮어쓰지 않는 별도 조정 Fact, 승인·감사 actor, 청구 반영 버전을 먼저 확정해야 한다.
따라서 FE의 미구현 탭은 서버 기능 존재의 근거가 아니다. 실제 route나 RPC를 연결하기 전에 별도 오너 결정과 BE 계약이 필요하다.