Skip to content

기초데이터 — 개시 미수 마스터 (FE 메인테이너 매뉴얼)

관리비 단독 상용화 프로그램 #3 (P0) — 2026-07-01 완료. 정본 설계 spec: docs/superpowers/specs/2026-07-01-initial-data-onboarding-design.md BE 참고 매뉴얼: 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.contractJSON.parse(JSON.stringify(...)) deep clone으로 초기화(contractPeriods ref).
  • 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.vueselectedContract = ref(null) 을 소유하고 provide('initialDataSelected', selectedContract)로 하위에 공급한다.

하위 컴포넌트(DynamicTableOrg, SheetCuOpeningArt, SheetReadPrincipleArt 등)는 inject('initialDataSelected', ref(null))로 수신한다.

진입 흐름:

  1. DynamicTableOrg에서 행 유닛 클릭 → selectedContract.value = contractCode 설정 후 commandfor="sheet-cu-opening-art" 트리거.
  2. SheetCuOpeningArtinject로 받은 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> 하드코딩. targetLabel computed 추가 — 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) AND dirty.
  • save() 로직:
    1. upsertContract({ contractCode }) — 계약 진입 보장.
    2. draft에 없는 기존 차수 removePeriod() + purgeOverlay('contract', cc, label) 동반 호출(W3b FB-4 — 안 하면 동일 label 재등록 시 고아 collected/adjusted가 유령 수납으로 부활). useForwarding을 이 컴포넌트가 직접 import해 호출(순환 회피 — useInitialReceivablesuseForwarding을 모른다).
    3. 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')text ref.
  • textarea 직접 붙여넣기도 지원.
  • [가져오기 실행]importCsv(text.value)result.value = { added, errors }.
  • 결과: i18n 'billing.serviceCharge.initialDataPage.import.result' 메시지 + 실패행 테이블.
  • @close → text/result 초기화.

8. 읽기 시트 상세 (SheetReadPrincipleArt 기준)

  • inject('initialDataSelected')selected.value contractCode.
  • 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.jsonbilling.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 맵 직접 미러 편집 기능 추가 필요
백엔드 연동클라이언트 모의 singletonuseInitialReceivables의 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.jsforwarding 재배선 후 출력 회귀. W3b: addPeriod stale 반영(FB-13)·removeContract 3축 소멸·파생==contract 직접대조·회귀0 베이스라인 동치·purgeOverlay·overpaidWarnings
src/components/service-charge/setting/initial-data/general/overlays/__tests__/SheetCuOpeningArt.spec.jsW3b 신규(첫 UI 컴포넌트 테스트) — 정상 상태 무경고 / billed 하향 시 초과수납 배지+툴팁 노출(mount)
src/composables/__tests__/receivableAging.spec.js연령분석 downstream 회귀
src/composables/__tests__/useFinancialStatements.spec.jsE1 개시분개 회귀

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에는 아무 신호 없이 해당 행만 저장되지 않는다 — 새 진입점을 만들 때 반환값 처리를 추가하는 편이 안전하다.