다크모드
매입 P4(카드매입)/P5(정정·반품) — FE 메인테이너 참고
정본 설계:
docs/superpowers/specs/2026-07-07-purchasing-p4-p5-design.md. BE 계약은docs/handoff/backend/purchasing-p4-p5.md참고. P1/P2/P3/P6 기반 구조(라우트·컴포넌트 트리 기본형)는docs/handoff/frontend/purchasing-p1-p2.md·docs/handoff/frontend/purchasing-p3-p6.md를 먼저 읽어야 한다 — 이 문서는 그 위에 얹힌 확장(기존 3개 컴포넌트 수정 + 신규 다이얼로그 1개)만 다룬다. 신규 라우트 없음 — 전부/purchasing/console/transaction안에서 확장.
1. 컴포넌트 트리 확장
src/components/purchasing/console/transaction/
├── MainOrg.vue — (수정) reverseTargetId 상태·onReverse 핸들러·
│ DialogReversePurchaseArt 마운트·회계전기가
│ vouchersPurchase+vouchersPurchaseReversal 통합 소스로
├── blocks/
│ └── DynamicTableOrg.vue — (수정) 증빙 컬럼(카드매입 배지+카드사 subtitle)·
│ 상태 배지(정상/반품/정정) 컬럼·행 액션 "정정/반품" 아이콘버튼
└── overlays/
├── DialogCreatePurchaseArt.vue — (수정) 증빙유형 '카드' 옵션+카드사 조건부 필드·유효성 확장
├── DialogReversePurchaseArt.vue — (신규) 정정/반품 다이얼로그(구분 반품/에누리 분기)
└── SheetReadPurchaseTransactionArt.vue — (수정) 상태 배지·"정정/반품 내역" 카드·
"역분개 전표" 카드·sheet-footer "정정/반품" 버튼2. 소비 컴포저블 확장
2-1. usePurchases() — 신규 노출 없음, 기존 시그니처 확장만
js
const { purchases, recordPurchase, reversePurchase } = usePurchases();recordPurchase({..., documentType, cardIssuer}):documentType에'카드'추가,cardIssuer신규 파라미터(카드매입일 때만 전달).reversePurchase(id, { kind, amount?, reason }): 신규 노출 —DialogReversePurchaseArt가 직접 호출하는 유일한 소비처.purchases배열 원자에status/reversals/reversedSupply/reversedVat/reversedPayable/documentType/cardIssuer필드가 이제 항상 존재(기존 매입도 기본값으로 채워짐 —DynamicTableOrg/SheetReadPurchaseTransactionArt가 이 필드들을 직접 읽는다).
2-2. usePurchaseJournal() — 신규 노출
js
const { vouchersPurchase, vouchersPurchaseReversal } = usePurchaseJournal();MainOrg.vue의purchaseVouchers()(로컬 함수, payment 콘솔의paymentVouchers()4종 통합 선례 미러)가vouchersPurchase() + vouchersPurchaseReversal()을 합쳐 "회계 전기" 버튼의 소스로 쓴다 — ★종전엔vouchersPurchase만 전기 대상이라 P5 전표가 전기 불가능한 "미아 전표"가 될 뻔했다(payment 콘솔 T3 Minor와 동일 교훈).SheetReadPurchaseTransactionArt는vouchersPurchase().find(v => v.id === 'pj-pur-'+purchase.id)(P1 전표, 재계산 금지·단일 소스)와vouchersPurchaseReversal().filter(v => v.refPurchaseId === purchase.id)(그 건의 반품/에누리 전표들)를 각각 조회해 분개 미리보기 카드에 표시.
3. 데이터 흐름
3-1. 카드매입 등록 흐름
DialogCreatePurchaseArt: 증빙유형 Select에서 '카드' 선택 → isCard computed=true
→ 카드사 필드 v-if="isCard"로 노출·필수화(valid computed에 !form.cardIssuer.trim() 가드 추가)
→ [등록] → recordPurchase({..., documentType:'카드', cardIssuer: form.cardIssuer.trim()})
→ DynamicTableOrg가 즉시 반영(reactive purchases 배열) — 증빙 컬럼에 "카드매입" 배지+카드사 subtitle발생일 필드는 그대로 재사용(별도 "승인일" 입력 없음) — purchaseDate가 곧 승인일.
3-2. 카드값 결제(인출) 흐름
카드매입 전용 결제 UI는 없다 — 기존 payment 콘솔(/purchasing/console/payment)의 지급 등록을 그대로 사용. 카드사를 vendorCode처럼 선택(또는 카드매입 등록 시 입력한 카드사 문자열과 일치시켜 입력)하고 인출일에 지급을 등록하면 P2 FIFO 충당이 그대로 작동한다. 이 흐름을 위한 신규 코드는 0줄(BE 핸드오프 §2-1 참고).
3-3. 정정/반품 흐름
DynamicTableOrg: 행의 "정정/반품" 아이콘 버튼 클릭(:disabled="p.status === '반품'")
→ emit('reverse', p.id)
또는
SheetReadPurchaseTransactionArt: sheet-footer "정정/반품" 버튼(v-if="canReverse") 클릭
→ emit('reverse', view.id)
↓ (두 진입점 모두 같은 대상을 같은 dialog로 인계 — DisbursementQueueOrg 'identify' 패턴 미러)
MainOrg: onReverse(id) → reverseTargetId = id → dialog-reverse-purchase-art showModal()
→ DialogReversePurchaseArt(:purchase-id="reverseTargetId")
→ target computed(purchases.find(p => p.id === purchaseId))
→ 구분(반품/에누리) Select → 반품이면 amount 불필요, 에누리면 감액 공급가액 입력
→ [확인] → reversePurchase(target.id, {kind, amount, reason})
→ {ok:false}면 error.value = result.reason(그대로 alert-error-subtle에 노출, dialog 안 닫힘)
→ {ok:true}면 emit('reversed', result) → dialog close
→ MainOrg: reverseTargetId = null
→ DynamicTableOrg/SheetReadPurchaseTransactionArt 둘 다 reactive 재계산(status 배지·미지급 잔액 자동 갱신)두 흐름 모두 prop+emit 릴레이만 사용 — 별도 store 없음(P1/P2 §4 컨벤션 그대로).
4. 회계 게이팅
useModuleSubscription().accountingEnabled — P1/P2/P3/P6과 동일 원칙, 이번 확장이 추가한 P5 전표도 같은 게이트 아래 있다:
transaction/MainOrg.vue의 "회계 전기" 버튼(v-if="accountingEnabled")이purchaseVouchers()(vouchersPurchase+vouchersPurchaseReversal 통합) 기준unpostedCount를 계산.SheetReadPurchaseTransactionArt의 "분개 미리보기"·"역분개 전표" 카드 둘 다v-if="accountingEnabled"(또는accountingEnabled && reversalVouchers.length) — 회계 모듈 미신청 시 전표 관련 카드는 아예 렌더링되지 않는다. 상태 배지(정상/반품/정정)·정정/반품 내역 카드·[정정/반품] 액션 자체는 게이팅 없음 — 회계 모듈 미신청이어도 "업무"(정정/반품 처리)는 그대로 가능(P3/P6의 PrepaidStatusOrg/DisbursementQueueOrg 선례와 동형 판단).
5. DS 패턴
DialogReversePurchaseArt:dialog-md dialog-inset-edged dialog-divide-y dialog-filled— 헤더에badge-sm badge-warning-moderate"P5" 태그(billing 계열 배지 어휘 상속). 대상 요약은table table-sm table-static table-divide-y, 이미 반품된 매입은alert-warning-subtle로 안내하고 폼 자체를 숨김(v-else분기). 구분별 조건부 필드는v-if="form.kind === '에누리'". 확인 전 안내는alert-info-subtle("반품 = 전액 · 에누리 = 부분 감액"), 실패 사유는alert-error-subtle.DynamicTableOrg증빙 컬럼: 배지(documentBadge)+카드사 subtitle(body-xs text-neutral-subtle)을 세로 스택(flex flex-col gap-0.5)으로 배치 — 문서유형과 카드사 정보를 한 셀에 밀도 있게 표시.- 상태 배지(
statusBadge,DynamicTableOrg/SheetReadPurchaseTransactionArt양쪽에 동일 매핑): 정상=badge-success-moderate, 정정=badge-warning-moderate, 반품=badge-red-moderate. SheetReadPurchaseTransactionArt의 "정정/반품 내역" 카드:v-if="reversals.length"(이력이 없으면 카드 자체를 숨김 — CLAUDE.md 카드 규율 §4 "접을 이유가 없으면 카드 금지"의 변형 적용, 여기선 "내용이 없으면 카드 금지"). summary zone-right에{{ fmt(view.reversedPayable) }}원 역분개합계 노출(카드 규율 §8-2 summary 합계 의무).- "역분개 전표" 카드: 여러 건의 반품/에누리 전표가 있을 수 있어(
v-for="rv in reversalVouchers") 각 전표를 독립된 분개 미리보기 테이블로 반복 렌더링, 각각 전기 상태 배지를 개별 표시.
6. 회귀0 가드
seedDemoPurchases()가seed.purchases(카드매입 1건 포함) 시드 직후seed.purchaseReversals(에누리 1건)를 소비 —demoSeed.js의buildPurchaseReversals가 vendorCode+purchaseDate로 방금 기록된 원자를 찾아reversePurchase를 호출한다(원자 id를 미리 알 수 없으므로 매칭 조회 방식).resetPurchases()가 P5 상태(status/reversedSupply/reversedVat/reversedPayable/reversals)도 매입 배열 자체를splice(0)하므로 함께 초기화 — 신규 스펙 작성 시 별도 조치 불요.- 기존 P1/P2 스펙(status 필드 미언급)은
recordPurchase가 항상 기본값(정상)을 채우므로 무영향 — 회귀 테스트로 확인됨(usePurchaseJournal.spec.js"status 필터 활성화 — 정상 매입은 vouchersPurchase에서 그대로 방출").
7. 테스트 위치 (신규/수정)
src/composables/__tests__/usePurchases.spec.js — (+) P4 카드매입·P5 reversePurchase·에누리 드리프트 하드닝
src/composables/__tests__/usePurchaseJournal.spec.js — (+) P4 카드매입 event 분기·P5 vouchersPurchaseReversal·
status 필터·혼합 GL 대사 하드닝(반품/에누리 포함)
src/components/purchasing/console/transaction/__tests__/MainOrg.spec.js — (+)
src/components/purchasing/console/transaction/blocks/__tests__/DynamicTableOrg.spec.js — (+)
src/components/purchasing/console/transaction/overlays/__tests__/DialogCreatePurchaseArt.spec.js — (+)
src/components/purchasing/console/transaction/overlays/__tests__/DialogReversePurchaseArt.spec.js — (신규)
src/components/purchasing/console/transaction/overlays/__tests__/SheetReadPurchaseTransactionArt.spec.js — (+)8. 확장 포인트
- 카드 명세서 자동 대사: 현재는
cardIssuer(문자열)로 카드매입을 식별할 뿐, 카드사 명세서 파일을 업로드해 등록된 매입과 매칭하는 기능이 없다. 추가 시 은행 거래 자동 수집(P3/P6 후속 확장 포인트)과 유사한 매칭 로직(카드 승인번호 또는 승인일+금액 조합 키) 검토. - 정정/반품 취소(되돌리기) UI:
usePurchases.reversePurchase는 append-only라 "반품 자체를 취소"하는 역-역분개 액션이 컴포저블에도 없다(BE 핸드오프 §6). 추가하려면 먼저 컴포저블에cancelReversal(reversalId)류 액션이 필요. - 이미 지급된 매입 반품 시 환급/선급 전환 액션: 현재
DialogReversePurchaseArt는 헤드룸 초과 시 BE의 차단 사유를 그대로 노출할 뿐(error.value = result.reason), "선급금으로 전환" 또는 "환급 처리"로 넘어가는 후속 액션 버튼이 없다. BE에 해당 액션이 추가되면 이 다이얼로그의 에러 alert 옆에 CTA 버튼 추가 검토. - 카드매입 가맹점(사용처)별 비용 분석:
cardIssuer는 "채무 상대"(카드사)만 담고, 실제 가맹점/사용처 정보는memo자유 텍스트로만 남길 수 있다. 정식 분석이 필요해지면 별도merchantName필드와 집계 뷰 신설 검토. - 거래처명 마스터 연결: P1/P2/P3/P6와 동일 후속(정정/반품 다이얼로그의 vendorCode 표시도 이름 마스터 도입 시 함께 교체).