@@ -27,6 +27,12 @@ import {
2727import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
2828import type { TouchEvent as ReactTouchEvent } from 'react'
2929import { apiFetch , ensureAuthenticated } from '../lib/auth'
30+ import {
31+ formatSignedAmount ,
32+ formatTransactionType ,
33+ getAmountColorClass ,
34+ getTransactionBadgeClass ,
35+ } from '../lib/format'
3036import { getSelectedLedgerId } from '../lib/ledger'
3137import {
3238 consumeOpenCreateTransactionOnce ,
@@ -132,11 +138,7 @@ function resolveScheduleDate(year: number, month: number, dayOfMonth: number, on
132138}
133139
134140function getTransactionTypeLabel ( type : TxType | null ) {
135- if ( type === 'INCOME' ) return '수입'
136- if ( type === 'EXPENSE' ) return '지출'
137- if ( type === 'DEBT_PAYMENT' ) return '부채 상환'
138- if ( type === 'SAVING' ) return '저축'
139- if ( type === 'TRANSFER' ) return '이체'
141+ if ( type ) return formatTransactionType ( type )
140142 return '템플릿 없음'
141143}
142144
@@ -620,6 +622,49 @@ function TransactionsPage() {
620622
621623 const isTransferType = ( type : TxType ) => type === 'TRANSFER' || type === 'DEBT_PAYMENT' || type === 'SAVING'
622624
625+ const getAmountToneClass = ( type : TxType ) => getAmountColorClass ( type , 'semantic' )
626+
627+ const getTransactionPreview = ( tx : Transaction ) => {
628+ const isTransfer = isTransferType ( tx . type )
629+ const typeLabel = getTransactionTypeLabel ( tx . type )
630+ const memoText = tx . memo ?. trim ( ) ?? ''
631+
632+ const categoryLabel = tx . categoryId
633+ ? ( categoryNameById . get ( tx . categoryId ) ?? '알 수 없는 카테고리' )
634+ : '카테고리 없음'
635+
636+ const accountOutName = tx . accountOutId ? ( accountNameById . get ( tx . accountOutId ) ?? '알 수 없는 계좌' ) : null
637+ const accountInName = tx . accountInId ? ( accountNameById . get ( tx . accountInId ) ?? '알 수 없는 계좌' ) : null
638+
639+ const transferAccountLabel = isTransfer
640+ ? [ accountOutName , accountInName ] . filter ( Boolean ) . join ( ' → ' ) || null
641+ : null
642+
643+ const primaryLabel = isTransfer
644+ ? ( transferAccountLabel ?? ( memoText || '계좌 정보 없음' ) )
645+ : ( memoText || categoryLabel )
646+
647+ const secondaryParts = [ typeLabel ]
648+ if ( isTransfer ) {
649+ if ( memoText && primaryLabel !== memoText ) secondaryParts . push ( `메모 ${ memoText } ` )
650+ } else {
651+ if ( primaryLabel !== categoryLabel ) secondaryParts . push ( categoryLabel )
652+ if ( accountOutName ) secondaryParts . push ( `출금 ${ accountOutName } ` )
653+ if ( accountInName ) secondaryParts . push ( `입금 ${ accountInName } ` )
654+ }
655+ if ( tx . isFixed ) secondaryParts . push ( '고정' )
656+
657+ const secondaryMeta = secondaryParts . join ( ' · ' )
658+ const amountLabel = formatSignedAmount ( tx . type , Number ( tx . amount ) )
659+
660+ return {
661+ primaryLabel,
662+ secondaryMeta,
663+ amountLabel,
664+ amountToneClass : getAmountToneClass ( tx . type ) ,
665+ }
666+ }
667+
623668 const filteredTransactions = useMemo ( ( ) => {
624669 let result = transactions
625670
@@ -1217,46 +1262,7 @@ function TransactionsPage() {
12171262 < div key = { date } className = "space-y-2" >
12181263 < p className = "text-xs font-semibold text-muted-foreground" > { date } </ p >
12191264 { items . map ( ( tx ) => {
1220- const isTransfer = isTransferType ( tx . type )
1221- const memoText = tx . memo ?. trim ( )
1222-
1223- const transferAccountLabel = isTransfer
1224- ? [
1225- tx . accountOutId ? accountNameById . get ( tx . accountOutId ) ?? '알 수 없는 계좌' : null ,
1226- tx . accountInId ? accountNameById . get ( tx . accountInId ) ?? '알 수 없는 계좌' : null ,
1227- ]
1228- . filter ( Boolean )
1229- . join ( ' → ' ) || null
1230- : null
1231-
1232- const detailText = [
1233- transferAccountLabel ,
1234- ! transferAccountLabel
1235- ? tx . categoryId
1236- ? ( categoryNameById . get ( tx . categoryId ) ?? '알 수 없는 카테고리' )
1237- : '카테고리 없음'
1238- : null ,
1239- ! isTransfer && tx . accountOutId
1240- ? `출금 ${ accountNameById . get ( tx . accountOutId ) ?? '알 수 없는 계좌' } `
1241- : null ,
1242- ! isTransfer && tx . accountInId
1243- ? `입금 ${ accountNameById . get ( tx . accountInId ) ?? '알 수 없는 계좌' } `
1244- : null ,
1245- memoText ? `메모 ${ memoText } ` : null ,
1246- ]
1247- . filter ( Boolean )
1248- . join ( ' · ' )
1249-
1250- const typeLabel =
1251- tx . type === 'INCOME'
1252- ? '수입'
1253- : tx . type === 'EXPENSE'
1254- ? '지출'
1255- : tx . type === 'DEBT_PAYMENT'
1256- ? '부채 상환'
1257- : tx . type === 'SAVING'
1258- ? '저축'
1259- : '이체'
1265+ const preview = getTransactionPreview ( tx )
12601266
12611267 return (
12621268 < div
@@ -1279,19 +1285,17 @@ function TransactionsPage() {
12791285 >
12801286 < button
12811287 type = "button"
1282- className = "flex flex-1 flex-col items-start gap-1 text-left focus-visible:outline-none"
1288+ className = "flex min-w-0 flex-1 flex-col items-start gap-0.5 text-left focus-visible:outline-none"
12831289 onClick = { ( ) => { setSwipedTxId ( null ) ; setDetailTransaction ( tx ) } }
12841290 >
1285- < p className = "text-sm font-medium" > { typeLabel } </ p >
1286- < p className = "text-xs text-muted-foreground" > { detailText } </ p >
1291+ < p className = "w-full truncate text-sm font-medium text-foreground " > { preview . primaryLabel } </ p >
1292+ < p className = "w-full truncate text-xs text-muted-foreground" > { preview . secondaryMeta } </ p >
12871293 </ button >
12881294 < div className = "flex items-center gap-1" >
12891295 < span
1290- className = { `text-sm font-semibold ${
1291- isTransfer ? 'text-foreground' : tx . type === 'INCOME' ? 'text-blue-600' : 'text-foreground'
1292- } `}
1296+ className = { `shrink-0 text-sm font-semibold tabular-nums ${ preview . amountToneClass } ` }
12931297 >
1294- { isTransfer ? '' : tx . type === 'INCOME' ? '+' : '-' } { Number ( tx . amount ) . toLocaleString ( ) } 원
1298+ { preview . amountLabel }
12951299 </ span >
12961300 { /* 데스크톱 호버 삭제 아이콘 */ }
12971301 < button
@@ -1351,35 +1355,7 @@ function TransactionsPage() {
13511355 ) : (
13521356 < div className = "mb-4 max-h-[45dvh] space-y-2 overflow-y-auto pr-1" >
13531357 { selectedDateTransactions . map ( ( tx ) => {
1354- const isTransfer = isTransferType ( tx . type )
1355- const transferAccountLabel = isTransfer
1356- ? [
1357- tx . accountOutId ? accountNameById . get ( tx . accountOutId ) ?? '알 수 없는 계좌' : null ,
1358- tx . accountInId ? accountNameById . get ( tx . accountInId ) ?? '알 수 없는 계좌' : null ,
1359- ]
1360- . filter ( Boolean )
1361- . join ( ' → ' ) || null
1362- : null
1363- const memoText = tx . memo ?. trim ( )
1364- const detailText = [
1365- transferAccountLabel ,
1366- ! transferAccountLabel
1367- ? tx . categoryId
1368- ? ( categoryNameById . get ( tx . categoryId ) ?? '알 수 없는 카테고리' )
1369- : '카테고리 없음'
1370- : null ,
1371- memoText ? `메모 ${ memoText } ` : null ,
1372- ]
1373- . filter ( Boolean )
1374- . join ( ' · ' )
1375- const amountLabel = isTransfer
1376- ? `${ Number ( tx . amount ) . toLocaleString ( ) } 원`
1377- : `${ tx . type === 'INCOME' ? '+' : '-' } ${ Number ( tx . amount ) . toLocaleString ( ) } 원`
1378- const amountClass = isTransfer
1379- ? 'text-foreground'
1380- : tx . type === 'INCOME'
1381- ? 'text-blue-600'
1382- : 'text-foreground'
1358+ const preview = getTransactionPreview ( tx )
13831359
13841360 return (
13851361 < button
@@ -1391,21 +1367,13 @@ function TransactionsPage() {
13911367 } }
13921368 className = "flex w-full items-center justify-between rounded-md border px-3 py-2 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
13931369 >
1394- < div className = "space-y-1" >
1395- < p className = "text-sm font-medium" >
1396- { tx . type === 'INCOME'
1397- ? '수입'
1398- : tx . type === 'EXPENSE'
1399- ? '지출'
1400- : tx . type === 'DEBT_PAYMENT'
1401- ? '부채 상환'
1402- : tx . type === 'SAVING'
1403- ? '저축'
1404- : '이체' }
1405- </ p >
1406- < p className = "text-xs text-muted-foreground" > { detailText } </ p >
1370+ < div className = "min-w-0 space-y-0.5" >
1371+ < p className = "truncate text-sm font-medium text-foreground" > { preview . primaryLabel } </ p >
1372+ < p className = "truncate text-xs text-muted-foreground" > { preview . secondaryMeta } </ p >
14071373 </ div >
1408- < span className = { `text-sm font-semibold ${ amountClass } ` } > { amountLabel } </ span >
1374+ < span className = { `shrink-0 text-sm font-semibold tabular-nums ${ preview . amountToneClass } ` } >
1375+ { preview . amountLabel }
1376+ </ span >
14091377 </ button >
14101378 )
14111379 } ) }
@@ -1669,19 +1637,8 @@ function TransactionsPage() {
16691637
16701638 { detailTransaction ? ( ( ) => {
16711639 const tx = detailTransaction
1672- const isTransfer = isTransferType ( tx . type )
1673- const typeLabel =
1674- tx . type === 'INCOME' ? '수입'
1675- : tx . type === 'EXPENSE' ? '지출'
1676- : tx . type === 'DEBT_PAYMENT' ? '부채 상환'
1677- : tx . type === 'SAVING' ? '저축'
1678- : '이체'
1679- const typeBadgeClass =
1680- tx . type === 'INCOME'
1681- ? 'bg-blue-100 text-blue-800'
1682- : tx . type === 'EXPENSE'
1683- ? 'bg-rose-100 text-rose-800'
1684- : 'bg-blue-100 text-blue-800'
1640+ const typeLabel = getTransactionTypeLabel ( tx . type )
1641+ const typeBadgeClass = getTransactionBadgeClass ( tx . type )
16851642
16861643 const hasCategoriesLoaded = categoryNameById . size > 0
16871644 const categoryName = tx . categoryId
@@ -1702,27 +1659,27 @@ function TransactionsPage() {
17021659 >
17031660 < SheetDragHandle />
17041661 < SheetHeader className = "mb-4 p-0" >
1705- < SheetTitle className = "text-left" > 거래 상세</ SheetTitle >
1706- </ SheetHeader >
1707-
1708- < div className = "space-y-3" >
1709- < div className = "flex items-center justify-between" >
1662+ < div className = "flex items-center justify-between gap-2" >
1663+ < SheetTitle className = "text-left" > 거래 상세</ SheetTitle >
17101664 < span className = { `inline-block rounded-full px-2.5 py-1 text-xs font-medium ${ typeBadgeClass } ` } >
17111665 { typeLabel }
17121666 </ span >
1713- { tx . isFixed ? (
1667+ </ div >
1668+ </ SheetHeader >
1669+
1670+ < div className = "space-y-3" >
1671+ { tx . isFixed ? (
1672+ < div className = "flex justify-end" >
17141673 < span className = "flex items-center gap-1 text-xs text-muted-foreground" >
17151674 < Pin className = "size-3" /> 고정
17161675 </ span >
1717- ) : null }
1718- </ div >
1676+ </ div >
1677+ ) : null }
17191678
17201679 < div className = "flex items-baseline justify-between" >
17211680 < span className = "text-sm text-muted-foreground" > 금액</ span >
1722- < span className = { `text-lg font-bold ${
1723- isTransfer ? 'text-foreground' : tx . type === 'INCOME' ? 'text-blue-600' : 'text-foreground'
1724- } `} >
1725- { isTransfer ? '' : tx . type === 'INCOME' ? '+' : '-' } { Number ( tx . amount ) . toLocaleString ( ) } 원
1681+ < span className = { `text-lg font-bold ${ getAmountToneClass ( tx . type ) } ` } >
1682+ { formatSignedAmount ( tx . type , Number ( tx . amount ) ) }
17261683 </ span >
17271684 </ div >
17281685
0 commit comments