계좌 상세보기 - 2단계(기능, 동적 쿼리 구현) + Format 처리 + 페이징 처리
사용자 요청 list.jsp 에서 해당 계좌 번호 선택 - (list.jsp 링크 수정 해야 함)
1. detail.jsp 만들기
2. account/list.jsp 파일에 링크 추가 하기
3. 계좌 상세 보기 기능 구현
4. AccountController 주소 설계 및 코드 추가
5. 거래 내역 쿼리 확인 후 DTO 설계 - HistoryAccountDTO
6. AccountService 상세 보기 기능 구현
6-1. 단일 계좌 검색 기능 추가
6-2. 거래 내역 확인 기능 추가 (동적 쿼리 생성)
7. utils/ValueFormatter 클래스 추가 - 시간 포맷 기능
8. #,### 금액 단위 포맷 기능 추가 - HistoryAccount 클래스, Account 클래스에 기능 추가
코드상에서 사용할 쿼리
-- 출금에 대한 쿼리 출력
-- 출금에는 AMT 출금, 1111 ---> 2222 이체
select h.id, h.amount, h.w_balance AS balance, h.created_at,
coalesce(cast(da.number as CHAR(10)), 'ATM') as receiver,
wa.number as sender
from history_tb as h
left join account_tb as wa on wa.id = h.w_account_id
left join account_tb as da on da.id = h.d_account_id
where h.w_account_id = 1;
-- 입금에 대한 쿼리 출력 ( AMT 입금, 다른계좌에서 --> 1111계좌로 받거나)
select h.id, h.amount, h.d_balance as balance, h.created_at,
coalesce(CAST(wa.number as CHAR(10)) , 'ATM') as sender,
da.number as receiver
from history_tb as h
left join account_tb as wa on wa.id = h.w_account_id
left join account_tb as da on da.id = h.d_account_id
where h.d_account_id = 1;
-- 입,출금 전체 쿼리
select h.id, h.amount,
case
when h.w_account_id = 1 then (h.w_balance)
when h.d_account_id = 1 then (h.d_balance)
end as balance,
coalesce(cast(wa.number as char(10)), 'ATM') as sender,
coalesce(cast(da.number as char(10)), 'ATM') as receiver,
h.created_at
from history_tb as h
left join account_tb as wa on h.w_account_id = wa.id
left join account_tb as da on h.d_account_id = da.id
where h.w_account_id = 1 OR h.d_account_id = 1;
select * from history_tb
1. detail.jsp 만들기
계좌 상세 보기 화면은 계좌 목록 페이지에서 존재하는 하나의 계좌 번호를 선택했을 때 DB에서 데이터를 조회하고 결과를 화면에 출력해야 합니다. 한 번에 작업을 하면 어려움이 있을 수 있으니 기본 화면부터 만들고 기능을 추가하도록 합시다.
계좌 상세 보기 화면은 계좌 목록 페이지에서 존재하는 하나의 계좌 번호를 선택했을 때 DB에서 데이터를 조회하고 결과를 화면에 출력해야 합니다. 한 번에 작업을 하면 어려움이 있을 수 있으니 기본 화면부터 만들고 기능을 추가하도록 합시다.
list.jsp - a 태그 추가
<tbody>
<c:forEach var="account" items="${accountList}">
<tr>
<td><a href="/account/detail/${account.id}?type=all">${account.number}</a></td>
<td>${account.balance}</td>
</tr>
</c:forEach>
</tbody>
AccountController - detail GetMapping 추가
/**
* 계좌 상세 보기 페이지
* 주소 설계 : http://localhost:8080/account/detail/1?type=all, deposit, withdraw
* @return
*/
@GetMapping("/detail/{accountId}")
public String detail(@PathVariable(name = "accountId") Integer accountId, @RequestParam(required = false, name ="type") String type) {
// 인증검사 추후 추가
System.out.println("@PathVariable : " + accountId);
System.out.println("@RequestParam : " + type);
return "account/detail";
}
required = false
=> ?type=~을 생략해도 오류 발생 안 하고 null 값을 받아옴
detail.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!-- header.jsp -->
<%@ include file="/WEB-INF/view/layout/header.jsp"%>
<!-- start of content.jsp(xxx.jsp) -->
<div class="col-sm-8">
<h2>계좌 상세 보기(인증)</h2>
<h5>Bank App에 오신걸 환영합니다</h5>
<div class="bg-light p-md-5">
<div class="user--box">
길동님 계좌<br> 계좌번호 : xxxxxxx<br> 잔액 : xxxxx 원
</div>
<br>
<div>
<a href="/account/detail/${account.id}?type=all" class="btn btn-outline-primary" >전체</a>
<a href="/account/detail/${account.id}?type=deposit" class="btn btn-outline-primary" >입금</a>
<a href="/account/detail/${account.id}?type=withdrawal" class="btn btn-outline-primary" >출금</a>
</div>
<br>
<table class="table table-striped">
<thead>
<tr>
<th>날짜</th>
<th>보낸이</th>
<th>받은이</th>
<th>입출금 금액</th>
<th>계좌잔액</th>
</tr>
</thead>
<tbody>
<tr>
<th>yyyy-mm-dd 11:20:11</th>
<th>ATM</th>
<th>1111</th>
<th>10,000</th>
<th>5,000,000</th>
</tr>
</tbody>
</table>
</div>
</div>
<!-- end of col-sm-8 -->
</div>
</div>
<!-- end of content.jsp(xxx.jsp) -->
<!-- footer.jsp -->
<%@ include file="/WEB-INF/view/layout/footer.jsp"%>
위 코드와 그림 상세보기에서 전체(all), 입금(deposit), 출금(withdrawal)은 같은 화면에서 사용자 선택에 따라 다른 결과 화면이 출력이 되어야 합니다. (동적 쿼리를 사용해서 구현 할 예정 입니다.)
model 패키지에 HistoryAccount class를 생성합니다.
HistoryAccount
package com.tenco.bank.repository.model;
import java.sql.Timestamp;
import com.tenco.bank.utils.ValueFormatter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class HistoryAccount extends ValueFormatter {
private Integer id;
private Long amount;
private Long balance;
private String sender;
private String receiver;
private Timestamp createdAt;
}
history.xml에 쿼리문을 추가합니다.
type을 전체, 입금, 출금 으로 나누어서 조회할 수 있도록 합니다.
history.xml
<select id="findByAccountIdAndTypeOfHistory" resultType="com.tenco.bank.repository.model.HistoryAccount">
<if test="type == 'all'">
select h.id, h.amount,
case
when h.w_account_id = #{accountId} then (h.w_balance)
when h.d_account_id = #{accountId} then (h.d_balance)
end as balance,
coalesce(cast(wa.number as char(10)), 'ATM') as sender,
coalesce(cast(da.number as char(10)), 'ATM') as receiver,
h.created_at
from history_tb as h
left join account_tb as wa on h.w_account_id = wa.id
left join account_tb as da on h.d_account_id = da.id
where h.w_account_id = #{accountId} OR h.d_account_id = #{accountId}
order by h.created_at DESC
</if>
<if test="type == 'deposit'">
select h.id, h.amount, h.d_balance as balance, h.created_at,
coalesce(CAST(wa.number as CHAR(10)) , 'ATM') as sender,
da.number as receiver
from history_tb as h
left join account_tb as wa on wa.id = h.w_account_id
left join account_tb as da on da.id = h.d_account_id
where h.d_account_id = #{accountId}
order by h.created_at DESC
</if>
<if test="type == 'withdrawal'">
select h.id, h.amount, h.w_balance AS balance, h.created_at,
coalesce(cast(da.number as CHAR(10)), 'ATM') as receiver,
wa.number as sender
from history_tb as h
left join account_tb as wa on wa.id = h.w_account_id
left join account_tb as da on da.id = h.d_account_id
where h.w_account_id = #{accountId}
order by h.created_at DESC
</if>
</select>
HistoryRepository 코드 추가
public List<HistoryAccount> findByAccountIdAndTypeOfHistory(@Param("type") String type, @Param("accountId") Integer accountId)
AccountService 코드 추가
public List<HistoryAccount> readHostoryByAccountId(String type, Integer accountId, int page, int size) {
List<HistoryAccount> list = new ArrayList<>();
list = historyRepository.findByAccountIdAndTypeOfHistory(type, accountId);
return list;
}
AccountController 코드 추가
/**
* 계좌 상세 보기 페이지
* 주소 설계: http://localhost:8080/account/detail/1?type=all, deposit, withdraw
* @return
*/
@GetMapping("/detail/{accountId}")
public String detail(@PathVariable (name = "accountId")Integer accountId,
@RequestParam(required = false, name = "type") String type,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "2") int size,
Model model) {
// 1. 인증 검사
User principal = (User) session.getAttribute(Define.PRINCIPAL); // 다운 캐스팅
if (principal == null) {
throw new UnAuthorizedException(Define.ENTER_YOUR_LOGIN, HttpStatus.UNAUTHORIZED);
}
// 2. 유효성 검사
// 선언 동시에 arrayList 생성
List<String> validTypes = Arrays.asList("all", "deposit", "withdrawal");
if (!validTypes.contains(type)) {
throw new DataDeliveryException("유효하지 않은 접근입니다", HttpStatus.BAD_REQUEST);
}
Account account = accountService.readAccountById(accountId); // 이거 던져서 응답 받기
List<HistoryAccount> historyList = accountService.readHostoryByAccountId(type, accountId, page, size);
// 데이터 2개 내리기
model.addAttribute("account", account);
model.addAttribute("historyList", historyList);
return "account/detail";
}
실행 결과
전체 list 조회
입금 list 조회
출금 list 조회
2. Fomat 처리
위 처럼 금액에 ,를 붙이고 시간을 yyyy:MM:ss 처럼 표현하기 위해서는 Format 처리를 해야 합니다.
먼저 utils 패키지에 ValueFormatter class를 생성합니다.
ValueFormatter
package com.tenco.bank.utils;
import java.sql.Timestamp;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
public abstract class ValueFormatter {
// 시간 포맷
public String timestampToString(Timestamp timestamp) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(timestamp);
}
public String formatKoreanWon(Long amount) {
DecimalFormat df = new DecimalFormat("#,###");
String formatNumber = df.format(amount);
return formatNumber + "원";
}
}
이 메서드를 Account class 와 HistoryAccount 메서드가 상속 받도록 합니다.
상속을 받은 클래스를 활용하여 부모 클래스의 메서드를 jsp에서 사용합니다.
detail.jsp 코드 수정
<div class="user--box">
${principal.username}님 계좌
<br>
계좌번호 : ${account.number}
<br>
잔액 : ${account.formatKoreanWon(account.balance)}
</div>
<c:forEach var="historyAccount" items="${historyList}">
<tr>
<th>${historyAccount.timestampToString(historyAccount.createdAt)}</th>
<th>${historyAccount.sender}</th>
<th>${historyAccount.receiver}</th>
<th>${historyAccount.formatKoreanWon(historyAccount.amount)}</th>
<th>${historyAccount.formatKoreanWon(historyAccount.balance)}</th>
</tr>
</c:forEach>
list.jsp 코드 수정
<c:forEach var="account" items="${accountList}">
<tr>
<td><a href="/account/detail/${account.id}?type=all">${account.number}</a></td>
<td>${account.formatKoreanWon(account.balance)}</td>
</tr>
</c:forEach>
3. 페이징 처리
detail.jsp 코드 추가
<!-- Pagination -->
<div class="d-flex justify-content-center">
<ul class="pagination">
<!-- Previous Page Link -->
<li class="page-item <c:if test='${currentPage == 1}'> disabled </c:if>">
<a class="page-link" href="?type=${type}&page=${currentPage - 1}&size=${size}">Previous</a>
</li>
<!-- Page Numbers -->
<!-- [Previous] 1 2 3 4 5 [Next] -->
<c:forEach begin="1" end="${totalPages}" var="page">
<li class="page-item <c:if test='${page == currentPage}'> active </c:if>">
<a class="page-link" href="?type=${type}&page=${page}&size=${size}">${page}</a>
</li>
</c:forEach>
<!-- Next Page Link -->
<li class="page-item <c:if test='${currentPage == totalPages}'> disabled </c:if>">
<a class="page-link" a href="?type=${type}&page=${currentPage + 1}&size=${size}">Next</a>
</li>
</ul>
</div>
</div>
</div>
<!-- end of col-sm-8 -->
</div>
</div>
<!-- end of content.jsp(xxx.jsp) -->
이 jsp의 class 명은
https://www.w3schools.com/bootstrap4/bootstrap_pagination.asp
W3Schools.com
W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.
www.w3schools.com
여기서 참고하였습니다.
history.xml 코드 추가
페이지의 내역 개수를 파악하기 위해 아래와 같은 쿼리문을 추가해야 합니다.
<select id="countByAccountIdAndType" resultType="int">
<if test="type == 'all'">
select count(*)
from history_tb as h
where h.w_account_id = #{accountId} OR h.d_account_id = #{accountId}
</if>
<if test="type == 'deposit'">
select count(*)
from history_tb as h
where h.d_account_id = #{accountId}
</if>
<if test="type == 'withdrawal'">
select count(*)
from history_tb as h
where h.w_account_id = #{accountId}
</if>
</select>
history.xml 코드 수정
페이징 처리를 위해 limit #{limit} offset #{offset} 이 코드를 추가합니다.
<select id="findByAccountIdAndTypeOfHistory" resultType="com.tenco.bank.repository.model.HistoryAccount">
<if test="type == 'all'">
select h.id, h.amount,
case
when h.w_account_id = #{accountId} then (h.w_balance)
when h.d_account_id = #{accountId} then (h.d_balance)
end as balance,
coalesce(cast(wa.number as char(10)), 'ATM') as sender,
coalesce(cast(da.number as char(10)), 'ATM') as receiver,
h.created_at
from history_tb as h
left join account_tb as wa on h.w_account_id = wa.id
left join account_tb as da on h.d_account_id = da.id
where h.w_account_id = #{accountId} OR h.d_account_id = #{accountId}
order by h.created_at DESC
limit #{limit} offset #{offset}
</if>
<if test="type == 'deposit'">
select h.id, h.amount, h.d_balance as balance, h.created_at,
coalesce(CAST(wa.number as CHAR(10)) , 'ATM') as sender,
da.number as receiver
from history_tb as h
left join account_tb as wa on wa.id = h.w_account_id
left join account_tb as da on da.id = h.d_account_id
where h.d_account_id = #{accountId}
order by h.created_at DESC
limit #{limit} offset #{offset}
</if>
<if test="type == 'withdrawal'">
select h.id, h.amount, h.w_balance AS balance, h.created_at,
coalesce(cast(da.number as CHAR(10)), 'ATM') as receiver,
wa.number as sender
from history_tb as h
left join account_tb as wa on wa.id = h.w_account_id
left join account_tb as da on da.id = h.d_account_id
where h.w_account_id = #{accountId}
order by h.created_at DESC
limit #{limit} offset #{offset}
</if>
</select>
HistoryRepository 코드 수정 및 추가
이 메서드명은 history.xml의 id와 동일하게 맞춥니다.
또한, 페이징 처리를 위해 @Param("limit") Integer limit, @Param("offset") Integer offset 이 부분을 추가해줍니다.
// 페이징 처리
public List<HistoryAccount> findByAccountIdAndTypeOfHistory(@Param("type") String type, @Param("accountId") Integer accountId, @Param("limit") Integer limit, @Param("offset") Integer offset);
// 페이지 개수
public int countByAccountIdAndType(@Param("type")String type, @Param("accountId")Integer accountId);
AccountService에 코드 수정 및 추가
계좌목록을 조회할 때 페이징처리가 가능하도록 코드를 수정해줍니다.
/**
* 단일 계좌 거래 내역 조회
* @param type = [all, deposit, withdrawal]
* @param accountId (PK)
* @return 전체, 입금, 출금 거래내역(3가지 타입) 반환
*/
public List<HistoryAccount> readHostoryByAccountId(String type, Integer accountId, int page, int size) {
List<HistoryAccount> list = new ArrayList<>();
int limit = size;
int offset = (page - 1) * size;
list = historyRepository.findByAccountIdAndTypeOfHistory(type, accountId, limit, offset);
return list;
}
//해당 계좌와 거래 유형에 따른 전체 레코드 수를 반환하는 메서드
public int countHistoryByAccountIdAndType(String type, Integer accountId) {
return historyRepository.countByAccountIdAndType(type, accountId);
}
AccountController 코드 수정
/**
* 계좌 상세 보기 페이지
* 주소 설계: http://localhost:8080/account/detail/1?type=all, deposit, withdraw
* @return
*/
@GetMapping("/detail/{accountId}")
public String detail(@PathVariable (name = "accountId")Integer accountId,
@RequestParam(required = false, name = "type") String type,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "2") int size,
Model model) {
// 1. 인증 검사
User principal = (User) session.getAttribute(Define.PRINCIPAL); // 다운 캐스팅
if (principal == null) {
throw new UnAuthorizedException(Define.ENTER_YOUR_LOGIN, HttpStatus.UNAUTHORIZED);
}
// 2. 유효성 검사
// 선언 동시에 arrayList 생성
List<String> validTypes = Arrays.asList("all", "deposit", "withdrawal");
if (!validTypes.contains(type)) {
throw new DataDeliveryException("유효하지 않은 접근입니다", HttpStatus.BAD_REQUEST);
}
// 페이지 개수를 계산하기 위해서 총 페이지의 수를 계산해주어야한다.
int totalRecords = accountService.countHistoryByAccountIdAndType(type, accountId);
int totalPages = (int) Math.ceil((double)totalRecords / size);
// 계좌 내역을 조회하도록 한다.
Account account = accountService.readAccountById(accountId);
List<HistoryAccount> historyList = accountService.readHostoryByAccountId(type, accountId, page, size);
// 계좌 내역을 조회하기 위해 데이터 내리기
model.addAttribute("account", account);
model.addAttribute("historyList", historyList);
// 페이징 처리를 위해 데이터 내리기
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", totalPages);
model.addAttribute("type", type);
model.addAttribute("size", size);
return "account/detail";
}
@GetMapping("/detail/{accountId}")
public String detail(@PathVariable (name = "accountId")Integer accountId,
@RequestParam(required = false, name = "type") String type,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "2") int size,
Model model) {
위 코드를 보시면
맵핑을 할 때 /detail/계좌ID를 받아오기 위해 @PathVariable 어노테이션을 사용하고 name값을 줘야 합니다.
그 외 @RequestParam 어노테이션을 사용하여 type, page, size 값을 받아오도록 합니다.
실행 결과
여기서 보시면 jsp에서 Currentpage가 1일 때 Previous 를 비활성화 시켰습니다.
type은 전체, 입금, 출금 으로 나뉘고 size는 내역 개수를 의미합니다.
여기도 마찬가지로 jsp에서 Currentpage가 TotalPages와 같을 때 Next를 비활성화 시켰습니다.
이때 page가 2로 동적으로 변환되는 것을 볼 수 있습니다.
입금 내역
type이 deposit으로 변경된 것을 볼 수 있습니다.
출금 내역
type이 deposit으로 변경된 것을 볼 수 있습니다.