33import { create } from 'zustand' ;
44import { Expense , ExpenseStats , Destination , DayExpenseGroup } from '../types' ;
55import { Category } from '../utils/constants' ;
6- import { expenseApi , CreateExpenseDto } from '../api/expense' ;
6+ import { expenseApi , CreateExpenseDto , ExpenseResponse } from '../api/expense' ;
7+ import { parseServerDate , parseServerTime , formatDate } from '../utils/date' ;
8+
9+ // 현지통화별 합계 타입
10+ export interface LocalAmountItem {
11+ currency : string ;
12+ amount : number ;
13+ }
714
815interface ExpenseState {
916 expenses : Expense [ ] ;
1017 isLoading : boolean ;
18+ isInitialized : boolean ;
1119 error : string | null ;
1220
1321 // 액션들
@@ -22,6 +30,7 @@ interface ExpenseState {
2230 getStats : ( tripId : string ) => ExpenseStats ;
2331 getTotalByTrip : ( tripId : string ) => number ;
2432 getTodayTotal : ( tripId : string ) => { totalKRW : number ; byCurrency : Record < string , number > } ;
33+ getTotalLocal : ( tripId : string ) => LocalAmountItem [ ] ;
2534
2635 // 다중 국가 레이어 (FR-008)
2736 getExpensesByDateGrouped : (
@@ -31,53 +40,42 @@ interface ExpenseState {
3140 ) => DayExpenseGroup [ ] ;
3241}
3342
43+ /**
44+ * 서버 응답을 클라이언트 Expense 타입으로 변환
45+ */
46+ function toExpense ( e : ExpenseResponse , fallbackDate ?: string , fallbackTime ?: string ) : Expense {
47+ return {
48+ id : e . id ,
49+ tripId : e . tripId ,
50+ destinationId : e . destinationId ,
51+ amount : Number ( e . amount ) ,
52+ currency : e . currency ,
53+ amountKRW : Number ( e . amountKRW ) ,
54+ exchangeRate : Number ( e . exchangeRate ) ,
55+ category : e . category ,
56+ memo : e . memo ,
57+ date : parseServerDate ( e . expenseDate , fallbackDate ) ,
58+ time : parseServerTime ( e . expenseTime , fallbackTime ) ,
59+ createdAt : e . createdAt ,
60+ } ;
61+ }
62+
3463export const useExpenseStore = create < ExpenseState > ( ( set , get ) => ( {
3564 expenses : [ ] ,
3665 isLoading : false ,
66+ isInitialized : false ,
3767 error : null ,
3868
3969 loadExpenses : async ( tripId ) => {
4070 set ( { isLoading : true , error : null } ) ;
4171 try {
4272 const { data } = await expenseApi . getByTrip ( tripId ) ;
43-
44- // Convert server response to local type
45- const expenses : Expense [ ] = data . map ( ( e : any ) => {
46- // 날짜 파싱 - 로컬 타임존 기준
47- let dateStr = e . date ;
48- if ( e . expenseDate ) {
49- const d = new Date ( e . expenseDate ) ;
50- dateStr = `${ d . getFullYear ( ) } -${ String ( d . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( d . getDate ( ) ) . padStart ( 2 , '0' ) } ` ;
51- }
52-
53- // 시간 파싱
54- let timeStr = e . time ;
55- if ( e . expenseTime ) {
56- const t = new Date ( e . expenseTime ) ;
57- timeStr = `${ String ( t . getHours ( ) ) . padStart ( 2 , '0' ) } :${ String ( t . getMinutes ( ) ) . padStart ( 2 , '0' ) } ` ;
58- }
59-
60- return {
61- id : e . id ,
62- tripId : e . tripId ,
63- destinationId : e . destinationId ,
64- amount : Number ( e . amount ) ,
65- currency : e . currency ,
66- amountKRW : Number ( e . amountKRW ) ,
67- exchangeRate : Number ( e . exchangeRate ) ,
68- category : e . category ,
69- memo : e . memo ,
70- date : dateStr ,
71- time : timeStr ,
72- createdAt : e . createdAt ,
73- } ;
74- } ) ;
75-
76- set ( { expenses, isLoading : false } ) ;
73+ const expenses : Expense [ ] = data . map ( ( e ) => toExpense ( e ) ) ;
74+ set ( { expenses, isLoading : false , isInitialized : true } ) ;
7775 } catch ( error ) {
7876 const message = error instanceof Error ? error . message : '지출 내역을 불러오는데 실패했습니다' ;
79- console . error ( 'Failed to load expenses:' , error ) ;
80- set ( { isLoading : false , error : message } ) ;
77+ set ( { isLoading : false , isInitialized : true , error : message } ) ;
78+ throw error ;
8179 }
8280 } ,
8381
@@ -95,34 +93,7 @@ export const useExpenseStore = create<ExpenseState>((set, get) => ({
9593 } ;
9694
9795 const { data } = await expenseApi . create ( expenseData . tripId , dto ) ;
98-
99- // 날짜/시간 파싱
100- let dateStr = expenseData . date ;
101- if ( data . expenseDate ) {
102- const d = new Date ( data . expenseDate ) ;
103- dateStr = `${ d . getFullYear ( ) } -${ String ( d . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( d . getDate ( ) ) . padStart ( 2 , '0' ) } ` ;
104- }
105-
106- let timeStr = expenseData . time ;
107- if ( data . expenseTime ) {
108- const t = new Date ( data . expenseTime ) ;
109- timeStr = `${ String ( t . getHours ( ) ) . padStart ( 2 , '0' ) } :${ String ( t . getMinutes ( ) ) . padStart ( 2 , '0' ) } ` ;
110- }
111-
112- const expense : Expense = {
113- id : data . id ,
114- tripId : expenseData . tripId ,
115- destinationId : expenseData . destinationId ,
116- amount : Number ( data . amount ) ,
117- currency : data . currency ,
118- amountKRW : Number ( data . amountKRW ) ,
119- exchangeRate : Number ( data . exchangeRate ) ,
120- category : data . category ,
121- memo : data . memo ,
122- date : dateStr ,
123- time : timeStr ,
124- createdAt : data . createdAt ,
125- } ;
96+ const expense = toExpense ( data , expenseData . date , expenseData . time ) ;
12697
12798 set ( ( state ) => ( { expenses : [ expense , ...state . expenses ] } ) ) ;
12899 return expense ;
@@ -209,9 +180,7 @@ export const useExpenseStore = create<ExpenseState>((set, get) => ({
209180 } ,
210181
211182 getTodayTotal : ( tripId ) => {
212- const today = new Date ( ) ;
213- const todayStr = `${ today . getFullYear ( ) } -${ String ( today . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } -${ String ( today . getDate ( ) ) . padStart ( 2 , '0' ) } ` ;
214-
183+ const todayStr = formatDate ( new Date ( ) ) ;
215184 const todayExpenses = get ( ) . expenses . filter (
216185 ( e ) => e . tripId === tripId && e . date === todayStr
217186 ) ;
@@ -227,6 +196,19 @@ export const useExpenseStore = create<ExpenseState>((set, get) => ({
227196 return { totalKRW, byCurrency } ;
228197 } ,
229198
199+ getTotalLocal : ( tripId ) => {
200+ const tripExpenses = get ( ) . expenses . filter ( ( e ) => e . tripId === tripId ) ;
201+ const localAmounts : Record < string , number > = { } ;
202+
203+ for ( const expense of tripExpenses ) {
204+ localAmounts [ expense . currency ] = ( localAmounts [ expense . currency ] || 0 ) + expense . amount ;
205+ }
206+
207+ return Object . entries ( localAmounts )
208+ . map ( ( [ currency , amount ] ) => ( { currency, amount } ) )
209+ . sort ( ( a , b ) => b . amount - a . amount ) ;
210+ } ,
211+
230212 // PRD FR-008: 특정 날짜의 지출을 국가별로 그룹화
231213 getExpensesByDateGrouped : ( tripId , date , destinations ) => {
232214 const dayExpenses = get ( ) . expenses . filter (
0 commit comments