다크모드
기초데이터 — 개시 미수 마스터 (FE 메인테이너 매뉴얼)
관리비 단독 상용화 프로그램 #3 (P0) — 2026-07-01 완료. 정본 설계 spec:
docs/superpowers/specs/2026-07-01-initial-data-onboarding-design.mdBE 참고 매뉴얼:docs/handoff/backend/service-charge-initial-data.md⚠ 2026-07-06 W3b 갱신 — 축 미러 제거(FB-13)·오버레이 purge+당기 label 가드(FB-4·FB-19). 설계:docs/superpowers/specs/2026-07-06-initial-receivables-master-unify-design.md. §3-1·§6·§12·§13 갱신.
1. 라우트
/service-charge/setting/initial-data/general파일: src/views/service-charge/setting/initial-data/general/IndexView.vue
2. 컴포넌트 트리
IndexView
├── HeaderOrg (페이지 헤더 — 제목 "기초데이터 — 개시 미수" + 단지 메타)
└── MainOrg (selectedContract ref + provide('initialDataSelected') 소유)
├── SearchToolBarOrg (검색 도구 모음 — 현재 UI only)
├── CriteriaToolBarOrg (필터 도구 모음 — 현재 UI only)
├── DataToolBarOrg ([템플릿 다운로드] [가져오기])
├── DynamicTableOrg (세대 목록 테이블 — forwardingByContract 소비)
├── PaginationToolBarOrg (페이지 도구 모음 — 현재 UI only)
└── overlays
├── SheetCuOpeningArt (세대 개시미수 read↔edit 시트)
├── SheetImportOpeningArt (CSV 가져오기 시트)
├── SheetReadPrincipleArt (미수원금 상세 읽기 시트)
├── SheetReadInterestArt (미수연체료 상세 읽기 시트)
└── SheetReadTotalArt (미수합계 상세 읽기 시트)파일 경로 접두사: src/components/service-charge/setting/initial-data/general/
3. 소비 컴포저블
3-1. useInitialReceivables (신규 캐논 — 2026-07-06 W3b: 내부 구조 파생화)
src/composables/useInitialReceivables.js — 모듈 레벨 reactive singleton.
js
const {
priorPeriods, // ComputedRef<{ contract, unit, member }> — forwarding 소스
rowsByContract, // ComputedRef<Array<{ contractCode, periods }>>
addPeriod,
updatePeriod,
removePeriod,
upsertContract,
removeContract,
importCsv, // (text) → { added, errors: [{ line, reason }] }
resetToSeed,
} = useInitialReceivables();- 시드는
buildSeed(DEMO_CONFIG).priorPeriods.contract만JSON.parse(JSON.stringify(...))deep clone으로 초기화(contractPeriodsref). priorPeriods의 표면 표기는 무변경(여전히{ contract, unit, member })이지만 내부는 W3b로 바뀌었다:.contract= 저장소(contractPeriods.value) 그대로..unit/.member=computed파생(unitPeriods/memberPeriods) — 정적priorIdentity(contract→{unitCode,memberCode}, 시드 1회 도출)로 그룹핑 후mergePriorArrays(label 병합·성분별 합산)한 결과. 더 이상 시드 시점 deep-clone 스냅샷이 아니다 —.contract가 바뀌면 이 두 축도 매 접근마다 즉시 재계산된다.priorPeriods자체도ref가 아니라computed(() => ({ contract, unit: unitPeriods.value, member: memberPeriods.value })).
addPeriod는 같은label이면 upsert(splice로 교체).label === CURRENT_PERIOD이면 거부({ ok:false, reason }반환, W3b FB-19 가드) — 개시미수 마스터는 이월 전용.- 소비 코드(
SheetCuOpeningArt등)가.contract/.unit/.member를 구조분해(destructure)하지 않고priorPeriods.value.xxx로 접근하는 한 이 변경은 투명하다(§13 주의사항 참고).
3-2. useForwarding (재배선됨)
src/composables/useForwarding.js:
js
// 재배선 핵심 (기존 buildSeed 직접 참조 대체)
const { priorPeriods: _priorRef } = useInitialReceivables();
const PRIOR_PERIODS = {
get contract() {
return _priorRef.value.contract;
},
get unit() {
return _priorRef.value.unit;
},
get member() {
return _priorRef.value.member;
},
};PRIOR_PERIODS를 reactive getter 객체로 교체함으로써 computed 안에서 _priorRef.value를 추적 → 마스터 편집이 forwarding에 즉시 전파된다.
3-3. useAllocations
DynamicTableOrg에서 invoicingByContract(), lines, units를 소비해 unit name을 contractCode 기준으로 매핑한다. prior-only 계약은 contractCode를 fallback name으로 표시.
4. 선택 계약 provide/inject
MainOrg.vue가 selectedContract = ref(null) 을 소유하고 provide('initialDataSelected', selectedContract)로 하위에 공급한다.
하위 컴포넌트(DynamicTableOrg, SheetCuOpeningArt, SheetReadPrincipleArt 등)는 inject('initialDataSelected', ref(null))로 수신한다.
진입 흐름:
DynamicTableOrg에서 행 유닛 클릭 →selectedContract.value = contractCode설정 후commandfor="sheet-cu-opening-art"트리거.SheetCuOpeningArt가inject로 받은selected.value를 watch해 편집 상태 초기화.
5. DynamicTableOrg 동작 상세
- 행 소스:
forwardingByContract()+invoicingByContract()조인. - 컬럼: 유닛 · 소유자/점유자 · 미수원금 · 미수합계.
- 미수원금 =
r.forwarding(이월 미납 공급대가 합계, VAT 포함). - 미수합계 =
r.total(이월 + 당기 outstanding). - 연체료 컬럼은 현재 미표시 —
interestEngine통합 후 추가 예정. table-hover클래스로 행 hover 배경 표시.- 유닛 이름:
font-medium cursor-pointer hover:underline+commandfor="sheet-cu-opening-art"(CLAUDE.md §0-10 IA 패턴). - 금액 셀:
text-primary-bold클릭 링크로 읽기 시트 각각 진입.
6. SheetCuOpeningArt (세대 편집 시트)
패턴: CLAUDE.md §0 순수 속성 시트(컬렉션 없음) → sheet-footer 토글.
sheet-width-3xl집중 폭.- zone-top 대상명(FB-29, 2026-07-10): 이전엔
<span class="title-sm">—</span>하드코딩.targetLabelcomputed 추가 —useAllocations().{lines,units,contracts,members}를 조회해contractCode → line(현재기 라인) → unitCode → unitName+contracts.find(contractCode).memberCode → memberName을 파생,[unitName, memberName].filter(Boolean).join(' · ')(둘 다 없으면cc코드 폴백). prior-only 계약(현재기 라인 없음, 예:c-end)은line이 null이라 unitName은 없지만contracts배열은 계약별로 항상 존재하므로 memberCode는 resolve된다 — 그래서 "유닛명 없이 멤버명만" 표시로 정상 폴백한다. - READ 모드:
table-static차수 목록(label/dueDay/과세원금/면세원금/영세원금/소계). W3b 추가: label 셀에 초과수납 경고 배지(badge-warning-moderate, title 툴팁) —overpaidByLabel(computed,useForwarding().overpaidWarnings()를 선택 계약으로 필터)에 해당 label이 있으면 노출. - EDIT 모드: 인라인 input 행 테이블 + Data Tool Bar ([+ 차수 추가]).
- 저장 게이트:
valid(차수 1개 이상 + 모든 label이 YYYY-MM) ANDdirty. save()로직:upsertContract({ contractCode })— 계약 진입 보장.- draft에 없는 기존 차수
removePeriod()+purgeOverlay('contract', cc, label)동반 호출(W3b FB-4 — 안 하면 동일 label 재등록 시 고아 collected/adjusted가 유령 수납으로 부활).useForwarding을 이 컴포넌트가 직접 import해 호출(순환 회피 —useInitialReceivables는useForwarding을 모른다). - draft 각 행
addPeriod()(upsert) —label===CURRENT_PERIOD이면 내부적으로 거부되지만 이 시트는 UI 레벨 차단은 하지 않음(LABEL_RE만 검증) — CURRENT_PERIOD는YYYY-MM정규식을 통과하므로 addPeriod의 반환값({ok:false})을 무시하고 있다는 점 유의(향후 강화 지점).
watch(selected)→ 편집 상태/draft 초기화.@close핸들러:handleClose()→ editing/draft 리셋.
7. SheetImportOpeningArt (CSV 가져오기 시트)
sheet-width-3xl.- 파일 선택(
<input type="file" accept=".csv">) +FileReader.readAsText(f, 'utf-8')→textref. - textarea 직접 붙여넣기도 지원.
[가져오기 실행]→importCsv(text.value)→result.value = { added, errors }.- 결과:
i18n 'billing.serviceCharge.initialDataPage.import.result'메시지 + 실패행 테이블. @close→ text/result 초기화.
8. 읽기 시트 상세 (SheetReadPrincipleArt 기준)
inject('initialDataSelected')→selected.valuecontractCode.forwardingByContract().find(r => r.key === cc)→periods.filter(p => p.kind === 'forwarded').- TAX_DEFS =
[과세원금, 면세원금, 영세원금, 부가세(VAT)]. - 각 구분별 billed/collected/outstanding = forwarding
billedByComponent/byComponent집계. - totals.outstanding = 테이블의
principalTotal(=r.forwarding)과 정합해야 함(VAT-inclusive 공급대가 기준).
SheetReadInterestArt · SheetReadTotalArt는 동일 inject 패턴으로 연체료/합계 분해를 각각 표시한다.
9. i18n 키
src/i18n/locales/ko.json → billing.serviceCharge.initialDataPage.*
주요 키:
import.template— "템플릿 다운로드"import.open— "가져오기"import.paste— CSV 붙여넣기 라벨import.run— "가져오기 실행"import.result— 결과 메시지 ({ added, failed }보간)edit.title— 편집 시트 제목edit.period— "차수"edit.dueDay— "납기일"edit.taxable— "과세원금"edit.exempt— "면세원금"edit.zeroRated— "영세원금"edit.addPeriod— "차수 추가"
10. VAT 정합 불변식
| 계층 | 기준 | 표기 |
|---|---|---|
priorPeriods (마스터 저장) | 공급가액(excl. VAT) | 과세원금/면세원금/영세원금 |
forwarding r.forwarding | 공급대가(incl. VAT) | r.forwarding = withVat(원금 합계) |
| 테이블 미수원금 셀 | 공급대가 | formatAmount(r.forwarding) |
| 읽기 시트 미수잔액 합계 | 공급대가 | totals.outstanding (부가세 행 포함) |
테이블 셀과 읽기 시트 합계가 다르면 부가세 행이 누락되거나 VAT 파생 로직 오류.
11. 확장 포인트
| 항목 | 현재 | 확장 방향 |
|---|---|---|
| 연체료 표시 | 테이블 컬럼 미표시 | interestEngine 통합 후 forwarding row에서 연체료 합산 컬럼 추가 |
| 선수금 개시 온보딩 | 스코프 밖 | 별도 useInitialAdvances 캐논 + 화면 신설(미수와 분리) |
| prior-only 세대 신규 생성 | 화면 미지원 | upsertContract 후 unit/member 맵 직접 미러 편집 기능 추가 필요 |
| 백엔드 연동 | 클라이언트 모의 singleton | useInitialReceivables의 CRUD를 REST API 호출로 교체, priorPeriods를 서버 응답으로 초기화 |
| 검색·필터·페이지네이션 | UI only(no-op) | SearchToolBarOrg/CriteriaToolBarOrg/PaginationToolBarOrg 배선 |
12. 테스트 위치
| 파일 | 커버 |
|---|---|
src/composables/__tests__/useInitialReceivables.spec.js | 시드 초기화, CRUD, importCsv, forwarding 재배선 동등성, 전파 검증. W3b: 당기 label 거부 |
src/composables/__tests__/useForwarding.spec.js | forwarding 재배선 후 출력 회귀. W3b: addPeriod stale 반영(FB-13)·removeContract 3축 소멸·파생==contract 직접대조·회귀0 베이스라인 동치·purgeOverlay·overpaidWarnings |
src/components/service-charge/setting/initial-data/general/overlays/__tests__/SheetCuOpeningArt.spec.js | W3b 신규(첫 UI 컴포넌트 테스트) — 정상 상태 무경고 / billed 하향 시 초과수납 배지+툴팁 노출(mount) |
src/composables/__tests__/receivableAging.spec.js | 연령분석 downstream 회귀 |
src/composables/__tests__/useFinancialStatements.spec.js | E1 개시분개 회귀 |
UI 컴포넌트 테스트는 현재 없음 → W3b로 최초 1건 추가(SheetCuOpeningArt, mount 기반). 나머지 화면은 여전히 Playwright 라이브 검증으로 대체.
13. 주의사항
- reactive getter 패턴:
PRIOR_PERIODS는 일반 객체처럼PRIOR_PERIODS.contract[code]로 접근 가능하지만, Vue computed 추적은_priorRef.value레벨에서 이루어진다.PRIOR_PERIODS를 직접 destructure(const { contract } = PRIOR_PERIODS)하면 reactive 추적이 끊어진다. - deep clone 필수 — 단
.contract에만:resetToSeed()나 초기화 시 항상JSON.parse(JSON.stringify(...))수준의 deep clone을 사용해야 한다(참조 공유 시 시드 원본 오염 위험). W3b 이후 clone 대상은.contract뿐 —.unit/.member는 파생이라 clone할 저장소 자체가 없다. priorPeriods.value.contract의 내부 배열 직접 splice 가능: Vue 3 reactive는 배열 변이(splice/push)를 추적한다. addPeriod/removePeriod에서 직접 splice 사용 중.priorIdentity는 CRUD로 변하지 않는 정적 사실맵 (W3b): 새 unitCode/memberCode를 가진 계약을 만들어도priorIdentity에 없으면.unit/.member파생에 잡히지 않는다(§2-2/BE 핸드오프 "prior-only 세대 신규 생성 스코프 밖" 근거).priorIdentity확장이 필요하면demoSeed.js를 고쳐야 하며, 런타임 API로 노출되어 있지 않다.- removePeriod ↔ purgeOverlay는 반드시 짝 (W3b FB-4):
useInitialReceivables().removePeriod()만 호출하고useForwarding().purgeOverlay()를 빠뜨리면 오버레이 고아가 남는다. 이 계약은 타입 시스템이나 런타임 가드로 강제되지 않으므로(순환 import 회피 트레이드오프) 새 소비처를 만들 때마다 코드 리뷰로 확인해야 한다. - CURRENT_PERIOD 가드는 조용히 실패한다:
addPeriod가 당기 label을 거부해도{ ok:false, reason }을 반환할 뿐 예외를 던지지 않는다. 호출부가 반환값을 무시하면(현재SheetCuOpeningArt.save()가 그렇다) UI에는 아무 신호 없이 해당 행만 저장되지 않는다 — 새 진입점을 만들 때 반환값 처리를 추가하는 편이 안전하다.