다크모드
콘솔개요 기간(차수) 정보 편집 — FE 메인테이너 참고
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 차수 체인 진화(본 문서를 무효화하지 않음 — "현재 차수 표시/편집" 소비는 아래 그대로 유효):
useBillingPeriod가 체인(entries+currentIndex)으로 진화하며periods(computed, 전체 엔트리)가 반환값에 추가됐다(period는 무수정 — 여전히 현재 open 엔트리). 이 트랙에서 신설된 마감 UI(ActionBarOrg/DialogClosePeriodArt/PeriodHistoryOrg/SheetClosedPeriodArt)와billingAnchor.js(REFERENCE_DATE 하드코딩 → 체인 파생 앵커, 8+4 소비처 재배선)는 신설 정본docs/handoff/frontend/billing-period-chain.md에 기록했다. 아래 §2~§5(순수 API·소비처·i18n·테스트)는 이 문서가 다루는 "표시+편집" 범위 안에서 그대로 유효.
관리비 상용화 펀치리스트 Critical C-5 해소(파이프라인 최상단 진입점). 설계 정본:
docs/superpowers/plans/2026-07-10-billing-period-edit.md.
1. 배경
콘솔개요(일반 관리비 탭) "기본정보" 카드는 표시(DynamicTableOrg.vue)가 리터럴 하드코딩이었고, 편집 시트(FormSheetArt.vue)는 <script setup>이 아예 없어 v-model·취소·확인이 전부 dead-control이었다. 이번 슬라이스가 useBillingPeriod 스토어를 신설하고, actual/provisional 두 라인의 표시+편집 UI를 이 스토어 하나로 배선했다.
2. useBillingPeriod — 소비 방법
src/composables/useBillingPeriod.js
2-1. 순수 API vs 라우트 바인딩 래퍼
1차 계약은 순수 (line, edition) 인자 함수 — Vue 컴포넌트/라우터 없이 단위 테스트 가능:
js
import {
periodOf,
updatePeriod,
DEFAULT_PERIOD,
PERIOD_EDITABLE_FIELDS,
} from "@/composables/useBillingPeriod";
periodOf("service-charge", "actual"); // 현재 값(없으면 DEFAULT clone)
updatePeriod("service-charge", "actual", { noticeName: "..." }, "2026-07-10"); // patch 병합 + editedDate 명시 갱신컴포넌트에서는 useBillingPeriod() 래퍼(현재 라우트의 useBillingEdition().billing에서 line/edition 파생)를 쓴다:
js
import { useBillingPeriod } from "@/composables/useBillingPeriod";
const { period, update } = useBillingPeriod(); // period: computed, update(patch, editedDate)2-2. 반응성
state는 모듈 레벨 ref 싱글톤(useConsoleStage, useCharges와 동형). watch(state, ..., { deep: true })가 변경마다 localStorage에 직렬화. 표(read)와 편집 시트(write)가 같은 싱글톤을 구독하므로, 다른 컴포넌트 인스턴스에서 커밋해도 즉시 반영된다.
3. 소비처 — "두 UI, 한 소스" (actual + provisional)
| 콘솔 | 표시(read) | 편집(write) |
|---|---|---|
| actual | src/components/service-charge/actual/console/general/blocks/DynamicTableOrg.vue | src/components/service-charge/actual/console/general/overlays/FormSheetArt.vue |
| provisional | src/components/service-charge/provisional/console/general/blocks/DynamicTableOrg.vue | src/components/service-charge/provisional/console/general/overlays/FormSheetArt.vue |
3-1. 표시 — DynamicTableOrg.vue
리터럴을 전부 useBillingPeriod().period 파생으로 교체:
vue
<script setup>
import { useBillingPeriod } from "@/composables/useBillingPeriod";
const { period } = useBillingPeriod();
</script>
<template>
<td>{{ period.noticeName }}</td>
...
</template>- actual은
<table>단일 카드(disclosure)로 세로 나열. - provisional은 구조가 다르다 — 원본이
table-dynamic형(단일 행v-for="billing in billings", 컬럼 나열)이었고, 이번 슬라이스는 그 형태를 유지한 채 값만 store로 교체했다(회귀 최소화 우선, table 구조 자체를 actual과 통일하는 리팩터링은 스코프 밖).billings는period에서 파생한 단일 행 배열(computed). - provisional 필드명 매핑: 원본 mock 필드명(
overdueDate/lastEditDate/lastEditorId)이 store 필드명(overdueBaseDate/editedDate/operator)과 달라DynamicTableOrg.vue의billingscomputed 안에서 명시 매핑한다. 템플릿의 표시 라벨(i18n 키)은 그대로 두고 값만 교체했다. - noticeName 단일화: provisional 원본 리터럴이
'3월고지서'였는데, 공유 DEFAULT('2025년 3월 고지서')로 통일됐다 — actual/provisional이 같은 DEFAULT 엔트리 값을 초깃값으로 공유하기 때문(라인별 독립 엔트리 구조 자체는 유지, 최초 표시값만 동일).
3-2. 편집 — FormSheetArt.vue (draft → dirty → commit 관용)
두 라인의 FormSheetArt.vue는 동형 배선(<script setup> 신설, 이전엔 없었음):
js
import { reactive, computed, watch } from "vue";
import { useBillingPeriod, PERIOD_EDITABLE_FIELDS } from "@/composables/useBillingPeriod";
const { period, update } = useBillingPeriod();
const toDraft = () => {
/* PERIOD_EDITABLE_FIELDS만 복사 (메타 4필드는 draft에 없음) */
};
const draft = reactive(toDraft());
const resetDraft = () => {
/* draft를 현재 period로 재동기화 */
};
const dirty = computed(() => PERIOD_EDITABLE_FIELDS.some((k) => draft[k] !== period.value[k]));
// 외부에서 스토어가 바뀌면(다른 UI 커밋 등) 미변경 상태일 때만 재동기화
watch(
period,
() => {
if (!dirty.value) resetDraft();
},
{ deep: true },
);
const commit = () => {
if (!dirty.value) return; // dirty 게이트 — 무변경 시 update 호출 자체를 안 함
update({ ...draft }, new Date().toISOString().slice(0, 10)); // 오늘 날짜는 FE가 공급(스토어는 date-pure)
document.getElementById("form-sheet-art")?.close();
};
const cancel = () => {
resetDraft();
document.getElementById("form-sheet-art")?.close();
};
defineExpose({ draft, dirty, commit, cancel, resetDraft }); // 테스트가 sheet.vm.draft/dirty/commit/cancel로 직접 구동- dirty 게이트: 확인 버튼은
:disabled="!dirty".commit()내부에도 같은 가드가 있어 방어 이중화(버튼이 비활성이어도 어떤 경로로든commit()이 호출되면 무변경 시 no-op). - 취소는 확인창 없이 즉시 폐기 — draft를 store 값으로 되돌리고 시트를 닫는다. follow-up(§6).
- 읽기전용 메타(관리코드·생성일·수정일·담당자)는 이 폼에 없다 —
PERIOD_EDITABLE_FIELDS(8개)만 draft 대상.editedDate는 오직commit()이 넘기는 3번째 인자로만 갱신된다. - 두 라인의
FormSheetArt.vue는 거의 byte-identical(provisional은 일부 필드에 대해 전용 formSheet i18n 키가 없어 기존table.cycle/table.overdueDate키를 재사용 — §4 참고).
4. i18n 키
actual/provisional 각각 자체 네임스페이스(billing.serviceCharge.{actual,provisional}.console.general.general.*)를 쓴다 — 새 키 추가는 없었다(기존 표시 라벨 키를 그대로 재사용, 값만 스토어로 교체). provisional FormSheetArt.vue는 cycle/overdueBaseDate 입력이 원본 폼에 없어 신설한 필드인데, 전용 formSheet.* i18n 키가 없어 기존 table.cycle/table.overdueDate 키를 재사용했다(신규 키 미추가 — 라벨 텍스트는 표 헤더와 동일하게 노출됨, 폼 컨텍스트에 맞는 전용 라벨이 필요해지면 후속 작업).
5. 테스트 위치
| 파일 | 대상 |
|---|---|
src/composables/__tests__/useBillingPeriod.spec.js | 스토어 순수 함수 — DEFAULT 동결·필드 상수·키잉·periodOf/updatePeriod 병합·editedDate date-pure 계약·line/edition 독립·localStorage 라운드트립·sanitize(부분/전체 손상) |
src/components/service-charge/actual/console/general/__tests__/PeriodWiring.spec.js | actual 표시+편집 실배선 — DEFAULT 렌더·반응형 갱신·편집 커밋→표 갱신·취소 무변경·dirty 게이트·dirty=false 시 commit no-op |
src/components/service-charge/provisional/console/general/__tests__/PeriodWiring.spec.js | provisional 동형 검증(actual과 독립) |
컴포넌트 테스트는 defineExpose된 draft/dirty/commit/cancel을 wrapper.vm.*으로 직접 구동한다(dialog showModal/close는 jsdom 미지원이라 시트 자체를 열고 닫는 브라우저 API는 커버리지 밖 — document.getElementById(...)?.close() 호출은 옵셔널 체이닝으로 존재하지 않아도 안전).
6. 확장 포인트 (follow-up)
- 헤더 periodLabel 연동: 콘솔 최상단 헤더가 표시하는 기간 라벨은 여전히
billingLines.js정적 config에서 온다(useBillingPeriod와 무연동). 연동하려면 헤더 컴포넌트에서useBillingPeriod().period를 구독해periodLabel을 파생시키면 된다 — 스토어 API는 이미 준비돼 있다. - 새 차수 생성/이력: 현재는 "현재 차수" 단일 엔트리 편집만 가능. 새 차수를 만들거나(예: 다음 달로 롤오버) 과거 차수 이력을 조회하려면
useBillingPeriod와 별도로line:edition:cycle3축 키 또는 이력 배열 모델이 필요 — 현재 스키마는 이를 고려하지 않았다. - 취소 확인 다이얼로그:
cancel()호출 지점에<dialog>확인 팝오버를 얹으면 된다(다른 폼들의 관용과 동일 패턴) — dirty 상태일 때만 확인창을 띄우도록dirty값을 그대로 재사용 가능. - provisional 표 구조 통일: provisional
DynamicTableOrg.vue는 여전히 원본table-dynamic(단일 행) 구조를 유지한다 — actual과 동일한 세로 나열<table class="table-static">카드로 통일하는 리팩터링은 스코프 밖으로 남겨두었다(회귀 최소화 우선). - 필드 타입 검증:
sanitizeEntry는 존재 여부만 검사한다.year/cycle이 숫자인지, 날짜 필드가YYYY-MM-DD포맷인지 등 엄격 검증이 필요해지면useBillingPeriod.js의sanitizeEntry에 필드별 검증기를 추가.