글이 상당히 기니까 혹시라도 읽으시는분이 계시면 8일차만 읽으셔도 됩니당
금요일에 그렇게 알차게 글을 쓰고 다음날 일어났더니 열이 39도 까지 오르고
토요일이라 응급실을 다녀온뒤 약을 먹으면서 프로젝트를 진행하였다... 다행스럽게도 코로나는 아니였다 근데 월요일에 학원에 가니 내 옆에 앉으신분도 감기에 걸렸네 누가 시초냐에 대해 이야기함
진짜 고통의 시간이었다 그로인해서 블로그는 뒷전이었고 프로젝트에만 남은 힘을 쏟아 부었다. 이제 기침만 하고 오늘은 그동안 있었던 이 글은 그동안 진행 상황에 대해 정리할까 한다
일차엔 우리가 앞으로 쓰게될 스프링부트,부트스트랩,타임리프를 주로 공부하였고 프로젝트는 거의 진행 하지 않았다 거의 공부와 erd 수정에 집중한거 같다
스프링부트를 공부할때 강의에서 알려준 쿼리에 logback를 적용 시키는 방법을 우리 프로젝트에 적용 하면 좋을거 같다 생각해서 적용 시키고 팀원들이 물어보는걸 답변해주느라 바빴다 타임리프 사용법이라던지 인텔리제이 사용법 gradle로 디펜던시 적용시키는 방법이라던지 우리 프로젝트에 필요한 디펜던시등 팀원알려주는거에 시간을 다 보냈었다
덕분에 나도 복습이 되었던거 같아 좋았다
// sql에서 로그 가져오기 위한 의존성 추가
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
#로그백 설정 - 최규하
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0
#테스트용이었음
#log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
#log4jdbc.dump.sql.maxlinelength=0
#
#log4jdbc.sqltiming.warn.threshold=5
#log4jdbc.dump.sql.select=true
#log4jdbc.dump.sql.insert=true
#log4jdbc.dump.sql.update=true
#log4jdbc.dump.sql.delete=true
#log4jdbc.dump.sql.create=true
#log4jdbc.auto.load.popular.drivers=true
#log4jdbc.drivers=oracle.jdbc.OracleDriver
#log4jdbc.sqltiming.sql.exectimeout=10
#log4jdbc.dump.booleanastruefalse=false
#log4jdbc.dump.fulldebugstacktrace=false
#log4jdbc.dump.sql.addsemicolon=true
#log4jdbc.dump.passwords=false
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative] [%thread] %-5level %logger{36} -
%msg%n
</pattern>
</encoder>
</appender>
<logger name="com.example.project_machimo" level="OFF"/>
<!-- sql문을 로그 찍기엔 불가능하여서 log back 사용 jpa는 이거 자동 지원으로 앎 mybtais는 안 됨 -->
<logger name="com.example.project_machimo" level="OFF"/>
<logger name="jdbc" level="OFF"/>
<logger name="jdbc.sqlonly" level="DEBUG"/>
<logger name="jdbc.resultset" level="OFF"/>
<logger name="jdbc.resultsettable" level="OFF"/>
<logger name="jdbc.connection" level="OFF"/>
<logger name="log4jdbc.log4j2" level="OFF"/>
<root level="off">
<appender-ref ref="console"/>
</root>
</configuration>
이렇게 설정하면 mybatis로 쿼리를 가져올때 어디서 잘못됐는지나 어떻게 가져왔는지를 좀더 알기 쉽게 되었다
이때는 코드작성에만 거의 몰두했었다 근데 이때 기억에 남는게 약이 되게 독하다는걸 느꼈다 약만 먹으면 계속 가라앉는 느낌이고 안 먹으면 열나고 몸이 아프니 참으면서 했는데 시간이 금방금방 갓던거 보면 코드 치는게 재밌었나보다
코드 사진이라도 캡쳐해둘껄 그랬다
구현한 코드로는 경매프로세스를 구현했었고
경매만료 기간과 입찰기록이 존재 하냐 안 하냐에 따라
제품의 상태를 업데이트하기 위해 여러 자료들을 찾아봤는데 그중에 하나가 스프링에 스케줄링이었다
@SpringBootApplication
@EnableScheduling
public class ProjectMachimoApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectMachimoApplication.class, args);
}
@EnableScheduling 어노테이션을 사용해서 스케줄링이 동작하게 해준다
@Service
public class AuctionExpirationHandler {
@Autowired
private SqlSession session;
@Scheduled(fixedDelay = 10000)
public void executeTask() {
System.out.println("동작하니??");
ProductsDAO productsDAO = session.getMapper(ProductsDAO.class);
BidsDAO bidsDAO = session.getMapper(BidsDAO.class);
LocalDateTime now = LocalDateTime.now();
AuctionDAO auctionDAO = session.getMapper(AuctionDAO.class);
List<AuctionVO> auctionVOS = auctionDAO.endList(Timestamp.valueOf(now));
for (AuctionVO auctionVO : auctionVOS) {
if(auctionVO.highestBid()==null){
productsDAO.failedSale(Timestamp.valueOf(now), auctionVO.productsId());
}else {
productsDAO.succeedsSale(Timestamp.valueOf(now), auctionVO.productsId());
}
}
}
}
@Scheduled(fixedDelay = 10000)를 사용해서 이 메소드가 실행 되고 10초 뒤
또 실행되도록 설정했다 이러니까 ㄹㅇ 로그창 정신없이 바뀜 logback설정해둔것도 있고 그러니까 메소드를 간단히 설명하자면
지금 시간을 where에 값으로 보내서 경매기간이 끝난 제품들의 상태를 가져온다
그리고 for문으로 highestBid가 null 즉 입찰에 최대값이 없으면 입찰 기록이 없었다는거니까 이 제품은 판매 성공이 아닌 판매 실패가 되어서 상태값을 변경했고
not null이면 입찰 기록이 있었다는 뜻이니까 판매에 성공했다라는 상태값을 바꿔 주었다
@Service 어노테이션을 달아준 이유로는 스케줄링은 스프링 컨테이너가 관리하는거라 필클래스를 빈으로 등록할 필요가 있다 왜 동작 안 하나 했다가 빈으로 등록시키니 됐다
그냥 서비스라 생각해서 서비스 어노테이션을 달았다
이때 내가 뭘 기록으론 경매기능 변경 및 ERD 추가라 했는데
뭘했는지 잘 기억이 안난다... 해야할 일로는 경매 페이지 프론트 구현이었네
드디어 오늘이다
우선 ajax와 부트스트랩,타임리프를 사용해서 프론트 기능들을 구현했다
원래 이랬던 해더를 검색기능 추가하기 위해
<form action="/search/test3" method="get" class="d-flex" role="search">
<label>
<select name="choice" style="height: 32.5px">
<option value="1">작성자</option>
<option value="2">글 제목</option>
</select>
</label>
<input type="search" placeholder="search">
<button type="submit" style="border: none" class="bg-body-tertiary" >
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>
</button>
</form>
이렇게 부트스트랩를 사용해서 다음과 같이 바꿔주었다
헤더와 푸터를 내가 만든게 아니라 몰랐는데 위에 저 돋보기 모양이 버튼이 아니라 이미지만 있는거라서 버튼 형식으로 바꾸고 스타일을 줘서 테두리도 없앴다 그리고 저 돋보기 모양만 살려서 동작하게함 나름 잘한듯
그리고 경매
부트스트랩에 modal과 제이쿼리,ajax를 이용해서 화면에서 서버의 결과를 출력하게 했다
<div class="me-auto py-4">
<button th:if="${pView.pSaleType()==0}" type="button" class="btn btn-success btn-lg mb w-50"
data-bs-toggle="modal"
data-bs-target="#staticBackdrop">
입찰하기
</button>
</div>
입찰하기를 누르면 모달이 작동한다
<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
<form action="amountCheck" method="post">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class=" modal-title fs-5 " id="staticBackdropLabel">입찰가 입력</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body ">
<div class="input-group mb-3">
<input type="number" name="bids" class="form-control"
aria-label="Won amount (with dot and two decimal places)">
<span class="input-group-text">₩</span>
<input name="productId" type="hidden" th:value="${pView.productsId()}">
<input name="bidsHistory" type="hidden" th:value="${pView.productsId()}">
<input name="firstPrice" type="hidden" th:value="${pView.pBPrice()}">
</div>
</div>
<div class="modal-footer">
<button type="button" id="test1" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
<button id="compare" type="button" class="btn btn-primary">입찰</button>
</div>
</div>
</div>
</form>
</div>
입찰 버튼을 누르면 제이쿼리가 동작하고 히든으로 숨긴 값들을 ajax를 이용해 서버로 전송한다
$(document).ready(function () {
$("#compare").click(function (event) {
event.preventDefault();
//input에서 받아온 값들
var bidsInput = $('input[name="bids"]').val();
var productIdInput = $('input[name="productId"]').val();
var bidsHistoryInput = $('input[name="bidsHistory"]').prop('checked');
var firstPriceInput = $('input[name="firstPrice"]').val();
$.ajax({
url: "/auction/action-list/amountCheck",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
bids: bidsInput,
productId: productIdInput,
bidsHistory: bidsHistoryInput,
firstPrice: firstPriceInput
}),
success: function (response) {
alert("입찰성공")
document.getElementById("test1").click()
location.reload()
},
error: function (xhr, status, error) {
var response = JSON.parse(xhr.responseText);
alert(response.message);
}
});
});
});
서버 컨트롤러
@RestController
@RequestMapping("/auction")
public class AuctionRestController {
@Autowired
private CheckResponseEntity response;
@PostMapping("/action-list/amountCheck")
public ResponseEntity<? extends Object> updateAmount(@RequestBody CheckDTO check) {
return response.getResponseEntityForCheck(check);
}
@Component
public class CheckResponseEntityImpl implements CheckResponseEntity{
@Autowired
private AuctionService auctionService;
@Autowired
private ProductService productService;
@Autowired
private BidsService bidsService;
@Override
public ResponseEntity<?> getResponseEntityForCheck(CheckDTO check) {
CustomResponse response = new CustomResponse();
if (check.getBids() == null) {
response.setMessage("금액을 입력해주세요");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
Long bids = 0L;
if (check.getBids() != null) bids = check.getBids();
int productId = check.getProductId();
boolean bidsHistory = check.getBidsHistory();
Long firstPrice = 0L;
if (check.getFirstPrice() != null) {
firstPrice = check.getFirstPrice();
}
Long amount = bidsService.maxAmount(productId);
System.out.println("firstPrice = " + firstPrice);
System.out.println("bids" + bids);
if (firstPrice != 0) {
if (firstPrice < bids) {
bidsService.write(bids, productId, firstPrice);
auctionService.highestBidUpdate(bids, productId);
amount = (long) bidsService.maxAmount(productId);
List<BidsVO> bidsVOS = bidsService.bList(productId);
return ResponseEntity.ok(bidsVOS);
} else if (bids > amount) {
bidsService.amountUpdate(bids, productId);
auctionService.highestBidUpdate(bids, productId);
amount = bidsService.maxAmount(productId);
List<BidsVO> bidsVOS = bidsService.bList(productId);
return ResponseEntity.ok(bidsVOS);
} else {
response.setMessage("입찰가가 기존 금액보다 낮습니다");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
} else {
response.setMessage("입찰금액을 입력해주세요");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
}
@Data
static class CustomResponse {
private String message;
}
}
이렇게 값을 검증하고 값이 null 아니면서 기존 가격보다 크다면 ok를 내려주고 그게 아니라면 bad_request를 내려준다 바디에는 임의로 만든 CustomResponse에 message를 담아서 기존 값보다 낮은지 아니면 값을 입력 안 했는지 내려줌
여기까지가 지금까지 한 것들이다 한 번에 쓸려니까 되게 길어졌다
나름 그래도 잘짰다 생각해고 gpt한테 코드를 가지고 갔더니 평가해달랬더니
CheckResponseEntityImpl이 SOLID원칙을 잘 지키지 않고 있다고 해서
기능을 분리 하라한다 김영한의 스프링부트 원리를 봤는데도 기억을 못했다...
기능을 분리할려면 익셉션 핸들러를 구현해야한다 그에 validation과 예외 핸들러를 구현해서 처리해야하넹 허허 하면 되지 않을까 싶다 뭐 여튼
내가 해야한다
이것도 내가해야한다
얼마 없던 이슈사항이 발생했다...
1번 같은경우 입찰금액이 int에 최대값을 넘으면 오버플로우가 발생하는데
이때 문제가 이게 try-catch나 if문으로 예외처리를 시도해보았는데
그래도 계속난다 처음엔 값을 long으로 바꿔 보았지만
sql에선 long이 없는지 처리가 안 됨
2번은 div가 글자의 길이에 따라 바뀐다 진짜 프론트 막막하기만하다
처음하는 프로젝트고 팀원들도 다 처음인지라 우여곡절이 일주일동안 많았다
아파서 힘들게 코딩을 하는데 시간이 잘가거나 하기 싫다는 생각이 안 드는거보면 재밌긴 한가보다 그리고 젤 절실하게 느끼는건 왜 프론트와 백엔드 웹디자이너등 각자의 역할이 단일 책임의 원칙마냥 구분되어있는지 잘 느끼는 하루하루다
근데 또 요즘은 다시 경계가 모호해지고 있다는데 어렵다 어려워 내가 개발자가 될 수 있을까 ?