@@ -7,7 +7,6 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
77import {
88 ArrowDown ,
99 ArrowUp ,
10- BarChart2 ,
1110 Building2 ,
1211 Check ,
1312 ChevronLeft ,
@@ -131,6 +130,22 @@ type CashFlow = {
131130 total : number
132131}
133132
133+ type InstallmentProgress = {
134+ processedMonths : number
135+ totalMonths : number
136+ progressRate : number
137+ }
138+
139+ type InstallmentSummary = {
140+ id : number
141+ accountId : number
142+ totalAmount : number
143+ totalMonths : number
144+ monthlyAmount : number
145+ memo : string | null
146+ progress : InstallmentProgress
147+ }
148+
134149type BalanceReport = {
135150 summary : {
136151 totalAssets : number
@@ -311,7 +326,15 @@ function NetAssetCard({ summary }: { summary: BalanceReport['summary'] }) {
311326 )
312327}
313328
314- function AccountGroupSection ( { group, type } : { group : AccountGroup ; type : 'asset' | 'liability' } ) {
329+ function AccountGroupSection ( {
330+ group,
331+ type,
332+ installmentsByAccount,
333+ } : {
334+ group : AccountGroup
335+ type : 'asset' | 'liability'
336+ installmentsByAccount ?: Map < number , InstallmentSummary [ ] >
337+ } ) {
315338 const [ expanded , setExpanded ] = useState ( true )
316339 const isAsset = type === 'asset'
317340 const panelId = `group-panel-${ type } -${ group . major } `
@@ -331,14 +354,33 @@ function AccountGroupSection({ group, type }: { group: AccountGroup; type: 'asse
331354 </ span >
332355 </ button >
333356 < div id = { panelId } hidden = { ! expanded } className = "divide-y" >
334- { group . accounts . map ( ( account ) => (
335- < div key = { account . id } className = "flex items-center justify-between px-4 py-2.5" >
336- < span className = "text-sm text-foreground" > { account . name } </ span >
337- < span className = { cn ( 'text-sm font-medium' , isAsset ? 'text-foreground' : 'text-red-500' ) } >
338- { formatAmount ( account . balance ) }
339- </ span >
340- </ div >
341- ) ) }
357+ { group . accounts . map ( ( account ) => {
358+ const linkedInstallments = isAsset ? [ ] : ( installmentsByAccount ?. get ( account . id ) ?? [ ] )
359+
360+ return (
361+ < div key = { account . id } className = "flex items-start justify-between gap-3 px-4 py-2.5" >
362+ < div className = "flex min-w-0 flex-col gap-1" >
363+ < span className = "text-sm text-foreground" > { account . name } </ span >
364+ { linkedInstallments . map ( ( installment ) => {
365+ const remainingMonths = Math . max (
366+ installment . totalMonths - installment . progress . processedMonths ,
367+ 0 ,
368+ )
369+ const installmentName = installment . memo ?. trim ( ) || `할부 #${ installment . id } `
370+
371+ return (
372+ < span key = { installment . id } className = "text-xs text-muted-foreground" >
373+ { installmentName } · 잔여 { remainingMonths } 개월 · 월 { formatAmount ( installment . monthlyAmount ) }
374+ </ span >
375+ )
376+ } ) }
377+ </ div >
378+ < span className = { cn ( 'text-sm font-medium' , isAsset ? 'text-foreground' : 'text-red-500' ) } >
379+ { formatAmount ( account . balance ) }
380+ </ span >
381+ </ div >
382+ )
383+ } ) }
342384 </ div >
343385 </ div >
344386 )
@@ -389,6 +431,7 @@ function CashFlowCard({ cashFlow }: { cashFlow: CashFlow }) {
389431
390432function BalanceTab ( { ledgerId } : { ledgerId : number } ) {
391433 const [ report , setReport ] = useState < BalanceReport | null > ( null )
434+ const [ installmentsByAccount , setInstallmentsByAccount ] = useState < Map < number , InstallmentSummary [ ] > > ( new Map ( ) )
392435 const [ loading , setLoading ] = useState ( true )
393436 const [ error , setError ] = useState < string | null > ( null )
394437
@@ -398,15 +441,15 @@ function BalanceTab({ ledgerId }: { ledgerId: number }) {
398441 setError ( null )
399442
400443 apiFetch ( `/api/ledgers/${ ledgerId } /reports/balance` )
401- . then ( async ( res ) => {
444+ . then ( async ( balanceRes ) => {
402445 let json : unknown = null
403446 try {
404- json = await res . json ( )
447+ json = await balanceRes . json ( )
405448 } catch {
406449 // 응답 본문이 JSON이 아닌 경우 json은 null로 유지
407450 }
408451
409- if ( ! res . ok ) {
452+ if ( ! balanceRes . ok ) {
410453 let message = '잔고 데이터를 불러오지 못했습니다.'
411454 if ( json && typeof json === 'object' ) {
412455 const anyJson = json as { error ?: { message ?: unknown } ; message ?: unknown }
@@ -423,7 +466,55 @@ function BalanceTab({ ledgerId }: { ledgerId: number }) {
423466 json && typeof json === 'object' && 'data' in json
424467 ? ( json as { data : BalanceReport } ) . data
425468 : ( json as BalanceReport )
426- if ( ! cancelled ) setReport ( data )
469+ if ( ! cancelled ) {
470+ setReport ( data )
471+ }
472+
473+ let installmentsRes : Response | null = null
474+ try {
475+ installmentsRes = await apiFetch ( `/api/ledgers/${ ledgerId } /installments` )
476+ } catch {
477+ installmentsRes = null
478+ }
479+
480+ if ( ! installmentsRes || ! installmentsRes . ok ) {
481+ if ( ! cancelled ) {
482+ setInstallmentsByAccount ( new Map ( ) )
483+ }
484+ return
485+ }
486+
487+ let installmentsPayload : { data ?: InstallmentSummary [ ] } | InstallmentSummary [ ] = [ ]
488+ try {
489+ installmentsPayload = ( await installmentsRes . json ( ) ) as
490+ | { data ?: InstallmentSummary [ ] }
491+ | InstallmentSummary [ ]
492+ } catch {
493+ installmentsPayload = [ ]
494+ }
495+ const installments = Array . isArray ( installmentsPayload )
496+ ? installmentsPayload
497+ : ( installmentsPayload . data ?? [ ] )
498+ const nextInstallmentsByAccount = new Map < number , InstallmentSummary [ ] > ( )
499+
500+ for ( const installment of installments ) {
501+ const remainingMonths = Math . max (
502+ installment . totalMonths - installment . progress . processedMonths ,
503+ 0 ,
504+ )
505+ if ( remainingMonths <= 0 ) continue
506+
507+ const existing = nextInstallmentsByAccount . get ( installment . accountId )
508+ if ( existing ) {
509+ existing . push ( installment )
510+ } else {
511+ nextInstallmentsByAccount . set ( installment . accountId , [ installment ] )
512+ }
513+ }
514+
515+ if ( ! cancelled ) {
516+ setInstallmentsByAccount ( nextInstallmentsByAccount )
517+ }
427518 } )
428519 . catch ( ( err : Error ) => {
429520 if ( ! cancelled ) setError ( err . message )
@@ -497,7 +588,12 @@ function BalanceTab({ ledgerId }: { ledgerId: number }) {
497588 < h2 className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2 px-0.5" > 부채</ h2 >
498589 < div className = "space-y-2" >
499590 { report . liabilityGroups . map ( ( group ) => (
500- < AccountGroupSection key = { group . major } group = { group } type = "liability" />
591+ < AccountGroupSection
592+ key = { group . major }
593+ group = { group }
594+ type = "liability"
595+ installmentsByAccount = { installmentsByAccount }
596+ />
501597 ) ) }
502598 </ div >
503599 </ section >
0 commit comments