다크모드
콘솔개요 기간(차수) 정보 스토어 — 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.jsseam으로 이관(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_PERIODclone으로 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. 불변식
- line+edition 독립:
service-charge:actual의 갱신은service-charge:provisional에 영향을 주지 않는다(역도 동일). - DEFAULT 불변:
DEFAULT_PERIOD는Object.freeze되어 있고 절대 직접 변이되지 않는다 — 모든 변경은 clone-replace. - date-pure: 스토어 자체는 "오늘"을 알지 못한다.
editedDate갱신은 항상 호출자가 명시 공급한 값에 의해서만 일어난다. - 부분 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를 공유하므로 자동으로 동일해짐).