Skip to content

Commit 3823462

Browse files
committed
공통화 및 코드 리팩토링
1 parent 10011cf commit 3823462

File tree

7 files changed

+151
-113
lines changed

7 files changed

+151
-113
lines changed

apps/mobile/app/(tabs)/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// PRD FR-101~FR-109: 지도 + 여행 리스트 + 자동 전환
33

44
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
5-
import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity } from 'react-native';
5+
import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Alert } from 'react-native';
66
import { router } from 'expo-router';
77
import { MaterialIcons } from '@expo/vector-icons';
88
import { useTheme } from '../../lib/theme';
@@ -220,7 +220,7 @@ export default function GlobalHomeScreen() {
220220
{categorizedTrips.past.length > 5 && (
221221
<TouchableOpacity
222222
style={[styles.showMoreButton, { borderColor: colors.border }]}
223-
onPress={() => {/* TODO: 전체 여행 목록 화면 */}}
223+
onPress={() => Alert.alert('준비 중', '전체 여행 목록 기능은 곧 추가될 예정입니다.')}
224224
accessibilityLabel={`과거 여행 ${categorizedTrips.past.length - 5}개 더보기`}
225225
accessibilityRole="button"
226226
>

apps/mobile/app/trip/[id]/main.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default function TripMainScreen() {
3333
const insets = useSafeAreaInsets();
3434
const { colors, spacing, typography, borderRadius, shadows } = useTheme();
3535
const { trips, destinations, loadDestinations, setActiveTrip } = useTripStore();
36-
const { expenses, loadExpenses, getStats } = useExpenseStore();
36+
const { expenses, loadExpenses, getStats, getTotalLocal } = useExpenseStore();
3737
const { hapticEnabled, currencyDisplayMode } = useSettingsStore();
3838
const showInKRW = currencyDisplayMode === 'krw';
3939

@@ -68,17 +68,10 @@ export default function TripMainScreen() {
6868
return expenses.filter((e) => e.date === selectedDate);
6969
}, [expenses, selectedDate]);
7070

71-
// 현지통화별 총 지출 합계 (BudgetSummaryCard용)
71+
// 현지통화별 총 지출 합계 (BudgetSummaryCard용) - 스토어 메서드 활용
7272
const totalSpentLocal = useMemo(() => {
73-
const localAmounts: Record<string, number> = {};
74-
for (const expense of expenses) {
75-
localAmounts[expense.currency] = (localAmounts[expense.currency] || 0) + expense.amount;
76-
}
77-
// 금액이 큰 순서대로 정렬
78-
return Object.entries(localAmounts)
79-
.map(([currency, amount]) => ({ currency, amount }))
80-
.sort((a, b) => b.amount - a.amount);
81-
}, [expenses]);
73+
return id ? getTotalLocal(id) : [];
74+
}, [id, getTotalLocal, expenses]);
8275

8376
// Current destination
8477
const currentDestination = useMemo(() => {

apps/mobile/lib/constants/layout.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ export const FAB_CONSTANTS = {
2020
BOTTOM_POSITION: 24,
2121
RIGHT_POSITION: 16,
2222
} as const;
23+
24+
// 헤더 버튼 스타일 (공통)
25+
export const HEADER_BUTTON = {
26+
SIZE: 44,
27+
HIT_SLOP: { top: 10, bottom: 10, left: 10, right: 10 },
28+
} as const;
29+
30+
// 애니메이션 상수
31+
export const ANIMATION = {
32+
SPRING_TENSION: 100,
33+
SPRING_FRICTION: 10,
34+
} as const;

apps/mobile/lib/stores/expenseStore.ts

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
import { create } from 'zustand';
44
import { Expense, ExpenseStats, Destination, DayExpenseGroup } from '../types';
55
import { 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

815
interface 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+
3463
export 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(

apps/mobile/lib/stores/tripStore.ts

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { create } from 'zustand';
44
import { getCountryCode } from '../utils/constants';
55
import { Trip, Destination, CurrentLocation } from '../types';
66
import { tripApi, CreateTripDto, TripResponse } from '../api/trip';
7+
import { formatDate, parseServerDate } from '../utils/date';
78

89
interface TripState {
910
trips: Trip[];
1011
activeTrip: Trip | null;
1112
activeTrips: Trip[];
1213
isLoading: boolean;
13-
isInitialized: boolean; // 초기 데이터 로딩 완료 여부
14+
isInitialized: boolean;
1415
error: string | null;
1516
hasAutoNavigatedToTrip: boolean;
1617

@@ -43,44 +44,50 @@ interface TripState {
4344
setHasAutoNavigatedToTrip: (value: boolean) => void;
4445
}
4546

46-
// Helper: Convert date to YYYY-MM-DD format (로컬 타임존 기준)
47-
function toDateString(date: string | Date): string {
48-
if (!date) return '';
49-
const d = typeof date === 'string' ? new Date(date) : date;
50-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
47+
// 서버 방문지 응답 타입
48+
interface DestinationResponse {
49+
id: string;
50+
country: string;
51+
countryName?: string;
52+
city?: string;
53+
currency: string;
54+
startDate?: string;
55+
endDate?: string;
56+
orderIndex: number;
57+
createdAt: string;
5158
}
5259

5360
// Helper: Convert API response to local Trip type
5461
function toTrip(response: TripResponse): Trip {
5562
return {
5663
id: response.id,
5764
name: response.name,
58-
startDate: toDateString(response.startDate),
59-
endDate: toDateString(response.endDate),
65+
startDate: parseServerDate(response.startDate, response.startDate),
66+
endDate: parseServerDate(response.endDate, response.endDate),
6067
budget: response.budget,
6168
createdAt: response.createdAt,
6269
};
6370
}
6471

6572
// Helper: Convert API response destinations to local type
66-
function toDestination(dest: any, tripId: string): Destination {
73+
function toDestination(dest: DestinationResponse, tripId: string): Destination {
6774
return {
6875
id: dest.id,
6976
tripId,
7077
country: dest.country,
7178
countryName: dest.countryName,
7279
city: dest.city,
7380
currency: dest.currency,
74-
startDate: dest.startDate ? toDateString(dest.startDate) : undefined,
75-
endDate: dest.endDate ? toDateString(dest.endDate) : undefined,
81+
startDate: dest.startDate ? parseServerDate(dest.startDate) : undefined,
82+
endDate: dest.endDate ? parseServerDate(dest.endDate) : undefined,
7683
orderIndex: dest.orderIndex,
7784
createdAt: dest.createdAt,
7885
};
7986
}
8087

8188
// Helper: Get active trips from trips array
8289
function filterActiveTrips(trips: Trip[]): Trip[] {
83-
const today = toDateString(new Date());
90+
const today = formatDate(new Date());
8491
return trips.filter((t) => t.startDate <= today && t.endDate >= today);
8592
}
8693

@@ -128,7 +135,7 @@ export const useTripStore = create<TripState>((set, get) => ({
128135
// Set current destination if active trip exists
129136
if (activeTrip) {
130137
const tripDestinations = allDestinations.filter(d => d.tripId === activeTrip.id);
131-
const today = toDateString(new Date());
138+
const today = formatDate(new Date());
132139
const currentDest = tripDestinations.find(
133140
d => d.startDate && d.endDate && d.startDate <= today && d.endDate >= today
134141
) || tripDestinations[0] || null;
@@ -137,17 +144,18 @@ export const useTripStore = create<TripState>((set, get) => ({
137144
} catch (error) {
138145
const message = error instanceof Error ? error.message : '여행 목록을 불러오는데 실패했습니다';
139146
set({ isLoading: false, isInitialized: true, error: message });
147+
throw error;
140148
}
141149
},
142150

143151
loadDestinations: async (tripId) => {
144152
try {
145153
const { data } = await tripApi.getById(tripId);
146154
if (data.destinations) {
147-
const destinations = data.destinations.map((d: any) => toDestination(d, tripId));
148-
const today = toDateString(new Date());
155+
const destinations = data.destinations.map((d) => toDestination(d as DestinationResponse, tripId));
156+
const today = formatDate(new Date());
149157
const currentDest = destinations.find(
150-
(d: Destination) => d.startDate && d.endDate && d.startDate <= today && d.endDate >= today
158+
(d) => d.startDate && d.endDate && d.startDate <= today && d.endDate >= today
151159
) || destinations[0] || null;
152160

153161
set((state) => ({
@@ -159,7 +167,9 @@ export const useTripStore = create<TripState>((set, get) => ({
159167
}));
160168
}
161169
} catch (error) {
162-
console.error('Failed to load destinations:', error);
170+
const message = error instanceof Error ? error.message : '방문지 정보를 불러오는데 실패했습니다';
171+
set({ error: message });
172+
throw error;
163173
}
164174
},
165175

@@ -202,7 +212,7 @@ export const useTripStore = create<TripState>((set, get) => ({
202212
}));
203213

204214
// Update active trips if applicable
205-
const today = toDateString(new Date());
215+
const today = formatDate(new Date());
206216
if (trip.startDate <= today && trip.endDate >= today) {
207217
set((state) => ({
208218
activeTrip: trip,
@@ -257,7 +267,7 @@ export const useTripStore = create<TripState>((set, get) => ({
257267
if (trip) {
258268
// Set current destination from existing destinations
259269
const tripDestinations = get().destinations.filter(d => d.tripId === trip.id);
260-
const today = toDateString(new Date());
270+
const today = formatDate(new Date());
261271
const currentDest = tripDestinations.find(
262272
d => d.startDate && d.endDate && d.startDate <= today && d.endDate >= today
263273
) || tripDestinations[0] || null;
@@ -318,7 +328,7 @@ export const useTripStore = create<TripState>((set, get) => ({
318328

319329
getCurrentLocation: (tripId) => {
320330
const destinations = get().destinations.filter(d => d.tripId === tripId);
321-
const today = toDateString(new Date());
331+
const today = formatDate(new Date());
322332
const currentDest = destinations.find(
323333
d => d.startDate && d.endDate && d.startDate <= today && d.endDate >= today
324334
) || destinations[0] || null;

0 commit comments

Comments
 (0)