Skip to content

콘솔개요 기간(차수) 정보 스토어 — BE 참고

2026-07-11 정책 전환(본 문서 §편집 서술을 대체): 편집 시트 FormSheetArt.vue(×2)와 기본정보 전용 DataToolBarOrg.vue(×2)는 폐기됐다 — 상주 단일 카드의 편집은 카드 제자리 인라인 전환이 기본(정책 정본 docs/decisions/PATTERN-INLINE-EDIT-VS-DIALOG-2026-07-11.md). draft/dirty/commit 로직은 src/composables/usePeriodDraft.js로 이관됐고, 두 라인의 DynamicTableOrg.vue가 read↔edit를 소유한다(disclosure-footer [편집]↔[취소][저장] 토글·provisional 가로 1행 테이블도 세로 카드로 통일). 스토어(useBillingPeriod) 계약·키잉·불변식 서술은 그대로 유효하다.

2026-07-11 차수 체인 진화(본 문서 §2 데이터 모델을 확장, 무효화하지 않음): useBillingPeriod단일 엔트리 → 체인(state[key] = { entries: [entry, ...], currentIndex })으로 진화했다. 아래 §2의 필드 구조(12필드)·sanitize·date-pure 불변식은 각 엔트리 단위로 그대로 유효 — 체인은 엔트리를 감싸는 바깥 껍질일 뿐이다. periodOf/updatePeriod는 여전히 "현재(open) 엔트리"만 접근하는 종전 시그니처를 유지한다(무수정). 신규: periodsOf(line, edition)(전체 엔트리 조회) · closeCurrentPeriod(line, edition, {closedSummary, nextEntry})(원자 전이) · 엔트리에 status/closedSummary 필드 추가. 영속도 billingRepo.js seam으로 이관(mock 활성, Supabase 어댑터 구현 완료·미접속). 마감 상태머신·순서도·vatIncluded seam·DDL 전체는 신설 정본 docs/handoff/backend/billing-period-chain.md 참고 — 본 문서는 "단일 엔트리 스토어 계약"의 원형 기록으로 유지한다.

관리비 상용화 펀치리스트 Critical C-5 해소(파이프라인 최상단 진입점 — 새 차수/기간 설정). 설계 정본: docs/superpowers/plans/2026-07-10-billing-period-edit.md.


1. 개요

콘솔개요(일반 관리비 탭) "기본정보" 카드 — 고지서명·차수·년/월·기간·납기일·연체기산일·메타(관리코드·생성일·수정일·담당자) — 는 이전에 service-charge/actual/console/general/blocks/DynamicTableOrg.vue에 리터럴로 하드코딩돼 있었고, 편집 시트(overlays/FormSheetArt.vue)는 <script setup>이 아예 없어 모든 입력·취소·확인이 dead였다(눌러도 아무 일도 일어나지 않음). 본 문서는 신설 useBillingPeriod 스토어의 필드·키잉·기본값·불변식·영속을 기술한다.


2. 데이터 모델

2-1. 진실원

src/composables/useBillingPeriod.js 모듈 레벨 reactive 싱글톤 — useConsoleStage/useCharges와 동형(line+edition 키잉·clone-replace·DEFAULT 불변·localStorage).

state = {
  'service-charge:actual':      { noticeName, cycle, year, month, startDate, endDate, dueDate, overdueBaseDate,
                                   managementCode, createdDate, editedDate, operator },
  'service-charge:provisional': { ... 독립 엔트리 ... }
}
  • key = buildPeriodKey(line, edition) = \${line}😒{edition ?? 'default'}`` — line+edition 완전 독립.
  • 편집 가능 필드(8개): PERIOD_EDITABLE_FIELDS = ['noticeName', 'cycle', 'year', 'month', 'startDate', 'endDate', 'dueDate', 'overdueBaseDate'].
  • 읽기전용 메타(4개): PERIOD_READONLY_FIELDS = ['managementCode', 'createdDate', 'editedDate', 'operator'] — 시스템 관리, 편집 시트에 노출되지 않는다. 유일한 예외는 editedDate로, 저장(커밋) 시 UI가 명시 공급하는 값으로 갱신된다(§2-3).
  • 기본값 DEFAULT_PERIOD(Object.freeze)는 콘솔개요의 기존 하드코딩 리터럴을 그대로 이관했다(회귀 시각적 동일):
js
{
  noticeName: '2025년 3월 고지서', cycle: 3, year: 2025, month: '03',
  startDate: '2025-03-01', endDate: '2025-03-31', dueDate: '2025-04-15', overdueBaseDate: '2025-04-15',
  managementCode: 'a3b3c3', createdDate: '2025-02-01', editedDate: '2025-02-01', operator: 'username1234'
}
  • 미착수 line+edition은 저장소에 엔트리가 없다 — 조회 시 DEFAULT_PERIOD clone으로 lazy 폴백(첫 mutation 전까지 영속하지 않음).

2-2. periodOf(line, edition)

현재 엔트리(없으면 DEFAULT clone)를 반환. 항상 새 객체를 반환하므로 호출자가 반환값을 변이해도 스토어는 오염되지 않는다.

2-3. updatePeriod(line, edition, patch = {}, editedDate) — 이 스토어의 핵심 불변식: date-pure

js
export function updatePeriod(line, edition, patch = {}, editedDate) {
  const current = periodOf(line, edition);
  const next = { ...current, ...patch }; // clone-replace
  if (editedDate !== undefined) next.editedDate = editedDate;
  state.value = { ...state.value, [key]: next };
  return { ok: true };
}
  • patch가 현재 엔트리 위에 병합된다(부분 갱신 가능 — patch에 없는 필드는 유지).
  • editedDate는 스토어가 스스로 "오늘 날짜"를 계산해 자동 갱신하지 않는다. 3번째 인자로 명시 공급했을 때만(또는 patch.editedDate로) 갱신된다 — 스토어를 date-pure(결정적, new Date() 미사용)로 유지해 단위 테스트가 시각에 무관하게 재현 가능하도록 하기 위함이다. 오늘 날짜를 공급하는 책임은 호출자(FE의 편집 시트 커밋 핸들러)에 있다update({ ...draft }, new Date().toISOString().slice(0, 10)).
  • 항상 { ok: true } 반환 — 게이트/거부 없음(useConsoleStage와 달리 이 스토어에는 상태 전이 게이트가 없다. 순수 값 저장소).

2-4. useBillingPeriod() — 라우트 바인딩 래퍼

js
export function useBillingPeriod() {
  const { billing } = useBillingEdition();
  const line = computed(() => billing.value.line);
  const edition = computed(() => billing.value.edition);
  const period = computed(() => periodOf(line.value, edition.value));
  return {
    period,
    update: (patch, editedDate) => updatePeriod(line.value, edition.value, patch, editedDate),
  };
}

현재 라우트(useBillingEdition)에서 line/edition을 파생 — 컴포넌트가 line/edition을 직접 넘길 필요가 없다. 순수 함수(periodOf/updatePeriod)가 1차 계약이며, 이 래퍼는 얇은 바인딩일 뿐 로직을 재구현하지 않는다.


3. 불변식

  1. line+edition 독립: service-charge:actual의 갱신은 service-charge:provisional에 영향을 주지 않는다(역도 동일).
  2. DEFAULT 불변: DEFAULT_PERIODObject.freeze되어 있고 절대 직접 변이되지 않는다 — 모든 변경은 clone-replace.
  3. date-pure: 스토어 자체는 "오늘"을 알지 못한다. editedDate 갱신은 항상 호출자가 명시 공급한 값에 의해서만 일어난다.
  4. 부분 sanitize: localStorage 로드 시 sanitizeEntry필드 단위로 방어한다 — undefined/null 필드는 DEFAULT 값으로 개별 폴백(엔트리 전체를 버리지 않음). 엔트리 자체가 객체가 아니면 해당 엔트리만 DEFAULT로 대체. JSON 파싱 자체가 실패하면 전체 state가 빈 객체({})로 폴백(모든 line/edition이 DEFAULT).

4. 영속

  • localStorage 키: 'leyve.billing.period'.
  • watch(state, ..., { deep: true })가 변경마다 전체 state를 JSON 직렬화해 저장.
  • 값 형태: { [line:edition]: { ...12필드 } }.

5. 스코프 경계

포함: 필드 스토어(편집 8 + 메타 4)·line/edition 독립·localStorage 영속·date-pure editedDate 갱신 계약·DEFAULT clone-replace·부분 sanitize.

제외 (YAGNI/follow-up, 오너 확인 필요):

  • 헤더 periodLabel 미연동: 콘솔 헤더 최상단(billingLines.js)이 표시하는 기간 라벨(예: "2025년 12월")은 여전히 정적 config이며 이 스토어와 연동되지 않는다 — 개요 표의 편집값과 헤더 표시가 불일치할 수 있다.
  • 새 차수 생성/이력: 현재는 단일 "현재 차수" 엔트리를 편집만 할 뿐, 새 차수를 만들거나 과거 차수를 이력으로 조회하는 기능이 없다.
  • 취소 확인 다이얼로그: FE 편집 시트의 취소는 확인창 없이 즉시 draft를 폐기한다(§ FE 문서 참고).
  • provisional 전용 기본값 없음: provisional 엔트리도 actual과 동일한 DEFAULT_PERIOD를 공유한다 — provisional 고유 초기값이 필요해지면 별도 DEFAULT 분리를 검토해야 한다(현재 두 라인의 noticeName 기본값이 '2025년 3월 고지서'로 단일화됨, 원래 provisional mock은 '3월고지서'였음 — 아래 §6 참고).
  • open-ended 필드 타입 검증 없음: sanitizeEntry는 존재/null 여부만 검사한다 — 예: year에 문자열이 저장돼도 막지 않는다. 엄격한 타입/포맷 검증(날짜 포맷, 정수 등)은 미구현.
  • 서버 영속 없음: 현재는 클라이언트 localStorage 뿐 — 서버 API·감사로그·동시편집 충돌 처리는 스코프 밖.

6. provisional 마이그레이션 메모 (BE가 알아야 할 데이터 정합 이슈)

provisional 콘솔의 원본 mock 필드명은 store와 달랐다 — overdueDate/lastEditDate/lastEditorId(원본) → overdueBaseDate/editedDate/operator(store). FE DynamicTableOrg.vue가 표시 시점에 명시 매핑한다(값 자체는 store가 유일 source). 또한 provisional 원본 noticeName 리터럴이 '3월고지서'였던 것이 이번 통합으로 공유 DEFAULT '2025년 3월 고지서'로 단일화됐다 — actual과 provisional이 같은 표기 규칙을 쓰는 게 맞는지는 오너 확인이 필요하다(현재는 두 라인이 같은 DEFAULT를 공유하므로 자동으로 동일해짐).