날짜 필터에 숨은 타임존 버그 잡고, 대출 상환을 복식부기로 제대로 구현하고, 소프트 삭제가 남긴 유령 데이터를 청소한 기록이다.
1. “오늘 거래가 안 보여요” — KST 타임존 함정
거래내역에 날짜 필터가 없어서 미래 대출 상환 거래가 최근 거래와 뒤섞여 표시됐다. 기본 조회를 “1개월 전 ~ 오늘”로 세팅했다.
그런데 날짜 필터를 넣자마자 새 버그: 당일 오전 거래가 누락됨.
new Date("2026-03-12")
// → 2026-03-12T00:00:00Z (UTC 자정)
// → KST로 환산하면 2026-03-12 오전 9시
JavaScript new Date()가 날짜만 있는 문자열을 UTC 자정으로 해석한다. KST는 UTC+9니까, “3월 12일의 끝”이 KST 기준 오전 9시가 되어버린다. 당일 00:00~08:59 거래가 전부 빠진다.
해결: 타임존 오프셋을 명시적으로 붙인다.
const dateToParam = dateTo + 'T23:59+09:00'
// KST 기준 자정 직전
원칙: 날짜 문자열을 API에 넘길 때는 반드시 타임존 오프셋을 포함하자. “2026-03-12″처럼 타임존 없이 보내면 서버와 클라이언트가 다른 시간대로 해석할 수 있다.
date-fns 주의사항도 하나 있었다. parseISO() + format() 조합이 Next.js SSR에서 타임존 이슈로 빈 값을 반환하는 경우가 있다. 가장 확실한 건 Date.getFullYear(), getMonth(), getDate()로 직접 문자열 조립. 라이브러리 없이 확실히 동작한다.
2. 복식부기 — 대출 상환 양쪽에 기록
대출 상환 거래가 대출 계좌에만 기록되고, 실제 돈이 빠지는 은행 계좌에는 아무 기록이 없었다. 대출 잔액은 줄어드는데 은행 잔액은 그대로 — 자산 현황이 맞지 않는다.
스키마에 paymentAccountId 필드가 이미 있었는데 UI와 API가 연결 안 된 상태였다.
대출 상환의 복식부기:
– 대출 계좌(부채): 잔액 감소 (빚이 줄어듦)
– 납부 계좌(자산): 잔액 감소 (돈이 나감)
기존에는 한쪽만 기록하고 있었으니 절반만 구현된 셈이었다.
납부 계좌 변경 시 세 작업이 전부 성공하거나 전부 실패해야 해서 Prisma $transaction()으로 묶었다:
await prisma.$transaction([
prisma.loan.update({ where: { id }, data: { paymentAccountId } }),
prisma.transaction.deleteMany({ where: { /* 기존 미래 거래 */ } }),
prisma.transaction.createMany({ data: [ /* 새 미래 거래 */ ] }),
])
셋 중 하나라도 실패하면 전부 롤백된다.
3. 소프트 삭제의 유령 데이터
계좌를 삭제했는데 연결된 대출이 목록에 계속 보인다.
원인: 계좌 삭제 API가 실제 DELETE가 아니라 isActive = false로 플래그만 바꾸는 소프트 삭제였다. Prisma의 onDelete: Cascade는 실제 DELETE에만 반응하고, 소프트 삭제(UPDATE)에는 발동 안 한다.
결과: 계좌는 “삭제”됐지만 대출, 상환 일정, 거래 기록은 그대로 — 유령 데이터.
DB 직접 확인해보니 Loan 2건, LoanSchedule 240건, Transaction 202건이 고아 상태로 남아있었다.
해결: 계좌 “삭제” API에 연관 데이터 처리를 추가했다:
await prisma.$transaction([
prisma.loan.updateMany({
where: { accountId: id },
data: { status: 'CANCELLED' }
}),
prisma.transaction.deleteMany({
where: { accountId: id, transactedAt: { gt: new Date() } }
}),
prisma.account.update({
where: { id },
data: { isActive: false }
}),
])
대출 목록 조회에도 account: { isActive: true } 필터를 추가.
소프트 삭제 쓸 때 주의: 해당 테이블을 참조하는 모든 쿼리에 isActive: true 조건이 있는지 확인해야 한다. 하나라도 빠지면 유령이 나타난다.
4. 갭 분석 — 54%만 구현
설계 문서(schema.md, api-spec.md)와 실제 코드를 비교했더니 Match Rate 54%. 설계의 절반만 구현된 상태.
미구현: 신용카드, 투자, 보험, 연금 등 6개 도메인의 API 27개 엔드포인트. 스키마 이름 변경도 미반영.
54%가 낮아 보이지만 의도된 우선순위 결정이다. 핵심(계좌, 거래, 대출)을 먼저 완성하고 나머지는 다음 단계로 미뤘다.
이번 세션 정리:
1. 날짜 API에는 반드시 타임존 오프셋을 포함하자. T23:59+09:00을 붙이면 KST 기준 정확.
2. 금융 데이터 여러 테이블 동시 수정은 $transaction()으로 묶자.
3. 소프트 삭제 쓰면 모든 쿼리에 isActive: true 확인.
4. 갭 분석으로 “남은 일”을 숫자로 관리하자.


