“입력은 더 빠르게, 피드백은 더 직관적으로.”

핵심 요약

  • 트랜잭션 입력 UX 개선: 계정/카테고리 드롭다운, 수입/지출 토글, 날짜 선택기, 숫자 키패드 적용
  • 대시보드 시각화: /api/stats/monthly의 byCategory로 원형 차트 추가
  • API 연동 확장: /api/accounts, /api/categories?flow=, /api/transactions 사용
  • 안드로이드 에뮬레이터는 API 주소 10.0.2.2:8080 (물리 기기는 PC LAN IP:8080)

변경 사항 (요약)

  • src/api/client.ts
    • 타입 추가: Account, Category
    • API 추가: AccountsAPI.list(), CategoriesAPI.list(flow?)
  • src/screens/TransactionFormScreen.tsx
    • INCOME/EXPENSE 토글, Picker 기반 드롭다운, DateTimePicker, 숫자 키패드
  • src/screens/HomeScreen.tsx
    • VictoryPie로 카테고리별 지출 원형 차트 추가

설치 & 업데이트

# 차트 & SVG (Expo가 자동 링크)
npx expo install react-native-svg
npm i victory-native@^36.6.11

# 드롭다운 & 날짜 선택기 (Expo SDK에 맞춰 설치 권장)
npx expo install @react-native-picker/picker @react-native-community/datetimepicker

npm i로 설치하다 충돌나면 expo install을 사용하거나, node_modules/package-lock.json 삭제 후 다시 설치.


실행 방법

# 안드로이드 에뮬레이터
EXPO_PUBLIC_API_URL=http://10.0.2.2:8080 npx expo start -c

# 물리 기기(같은 Wi-Fi): PC IP 사용
EXPO_PUBLIC_API_URL=http://<PC_LAN_IP>:8080 npx expo start -c
  • app.config.js에 extra.apiUrl을 넣어두면, 환경변수가 없을 때 해당 값 사용.

  • 실제 사용 중인 baseURL은 개발 콘솔에서 한 번 확인해두면 디버깅이 편함:

    console.log('[API baseURL]', api.defaults.baseURL);

스니펫 (발췌)

1) API 확장 (client.ts)

export type Account = { id: number; type: 'CASH'|'CARD'|'BANK'; name: string };
export type Category = { id: number; flow: 'INCOME'|'EXPENSE'; name: string; color?: string };

export const AccountsAPI = {
  async list() { const { data } = await api.get<Account[]>('/api/accounts'); return data; }
};

export const CategoriesAPI = {
  async list(flow?: 'INCOME'|'EXPENSE') {
    const url = flow ? `/api/categories?flow=${flow}` : '/api/categories';
    const { data } = await api.get<Category[]>(url); return data;
  }
};

2) 트랜잭션 폼: 토글·드롭다운·날짜

const [flow, setFlow] = useState<'INCOME'|'EXPENSE'>('EXPENSE');
const [date, setDate] = useState(ymd(new Date()));
// ...
<View style={{ flexDirection:'row', gap:8 }}>
  {(['INCOME','EXPENSE'] as const).map(v => (
    <Pressable key={v} onPress={()=>setFlow(v)} /* 선택 스타일 */>
      <Text>{v==='INCOME'?'INCOME(수입)':'EXPENSE(지출)'}</Text>
    </Pressable>
  ))}
</View>

<View> {/* 계정 드롭다운 */} <Picker ...>{accounts.map(a=>... )}</Picker></View>
<View> {/* 카테고리 드롭다운 */} <Picker ...>{categories.map(c=>... )}</Picker></View>

<Pressable onPress={()=>setShowDate(true)}><Text>{date}</Text></Pressable>
{showDate && <DateTimePicker value={new Date(date)} mode="date" onChange={(_,d)=>{ setShowDate(false); d && setDate(ymd(d)); }} />}

3) 대시보드 차트 (HomeScreen)

import { VictoryPie } from 'victory-native';
const INNER = 60;
const pieData = (stats?.byCategory||[])
  .map((c:any)=>({ x:c.name, y:Number(c.total) }))
  .filter((d:any)=>d.y>0);

<VictoryPie
  data={pieData}
  height={220}
  innerRadius={INNER}
  padAngle={2}
  labelRadius={INNER + 30}
  style={{ labels: { fontSize: 10 } }}
/>

스크린샷 가이드



문제/해결 메모

1) Android에서 API 연결 실패

  • 에뮬레이터: 10.0.2.2:8080
  • 실기기: http://<PC_LAN_IP>:8080
  • 에뮬 브라우저로 http://10.0.2.2:8080/ping 열리면 네트워크 OK.

2) Expo 패키지 버전 경고/충돌

  • npx expo install <패키지>로 SDK에 맞춰 설치
  • 충돌 지속 시 node_modules/package-lock.json 제거 후 재설치

3) Victory/TS 타입 에러

labelRadius={({ innerRadius }) => (innerRadius ?? 60) + 30}에서
“NumberOrCallback + number 연산 불가” 경고 → 상수로 고정:

const INNER = 60;
<VictoryPie innerRadius={INNER} labelRadius={INNER + 30} ... />

(또는 typeof innerRadius==='function' 분기 후 숫자로 변환)

4) 엔트리 파일 오류

ConfigError: main entry ... → package.json의 "main": "index.js"로 설정하고 루트에 index.js 생성:

import { registerRootComponent } from 'expo';
import App from './App';
registerRootComponent(App);

마무리 & 다음

  • 이번 편으로 입력 → 통계 피드백 루프가 완성됨.
  • 다음은 선택:
    • 프론트 다듬기: 입력 검증/에러 메시지, 차트 스타일, 다크모드
    • 백엔드 보강: 요청 검증(Bean Validation), 에러 응답 일원화, Refresh Token
    • 배포: EAS Build, 환경 분리(dev/prod), 스토어 제출 체크리스트

✅ 참고

profile
Java 개발자 | 사이드 프로젝트 마니아 | GPT 기반 자동화 툴 연구 중 기술 리뷰, 개발일지

0개의 댓글