- 결제 완료 후 결제 완료된 데이터와 결제 취소를 할 때 데이터 처리를 어떻게 해야 할지 고민하던 중 2가지 방식을 찾았다.
- paymemt 테이블에 status를 사용하여 결제 완료, 취소 데이터 모두를 저장하는 방법
- paymentComplete, paymentCancel 테이블을 구분해서 만드는 방법
- 찾아본 결과 2번은 금융권에서 많이 사용하는 방법이므로 1번이 더 적합하다고 생각해서 기존 status를 사용하고 취소에 관한 추가적인 컬럼을 추가하기로 했다.
- 프로젝트/기능명: [payment 엔티티 수정]
- 진행한 작업:
- [O] 기능 개발:
- CanceledAt, canceledReason 컬럼 추가
- CanceledReason을 Enum 타입으로 설정하여 간단 명료하게 저장하게 사용할 예정이다.
TossPaymets의 요청을 받고 검증 후 승인까지 구현 완료.
- 프로젝트/기능명: [결제 정보와 저장한 값이 맞는지 검증 후 결제 요청 성공하면 결제가 승인된다.]
- 진행한 작업:
- [O] 기능 개발:
- tossPayment의 결제는 요청-> 인증 -> 승인 3가지로 이루어진다. 인증에 성공하면 paymentKey를 발급하여 url로 넘겨준다.
- 요청
- 결제 요청 전 tossOrderId, amount를 먼저 DB에 저장한다.
requestPayment결제 요청 메서드를 호출하기 전savePaymentRequest를 호출하여 tossOrderId와 amount를 저장한다.
async function savePaymentRequest(orderId,amount) {
const response = await fetch("/saveAmount", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
orderId: tossOrderId.textContent,
amount: totalDiningTablePrice,
}),
});
if (!response.ok) {
throw new Error("Failed to save payment request.");
}
}
// ------ '결제하기' 버튼 누르면 결제창 띄우기 ------
button.addEventListener("click", async function () {
await savePaymentRequest(orderId,totalDiningTablePrice);
await widgets.requestPayment({
orderId: tossOrderId.textContent,
orderName: String(OrderName),
successUrl: window.location.origin + "/success",
failUrl: window.location.origin + "/fail",
});
});
}
- 인증
- 파라미터로 넘어온 paymentKey, orderId, amount를 받아 OrderId로 저장되어있는 정보를 찾아와 amount를 비교한다.
- 결제 인증이 성공하면 PayStatus를 IN_PROGRESS로 인증 완료 상태로 바꿔준다.
// Controller
@GetMapping("/success")
public String verify(
@RequestParam String paymentKey,
@RequestParam String orderId,
@RequestParam int amount,
Model model
){
tossWidgetService.verify(paymentKey, orderId, amount);
model.addAttribute("paymentKey", paymentKey);
model.addAttribute("orderId", orderId);
model.addAttribute("amount", amount);
return "/tossPay/success.html";
}
// Service
@Transactional
public void verify(String paymentKey ,String orderId, int amount) {
Optional<Payment> paymentOptional = paymentRepository.findByTossOrderId(orderId);
Payment payment = paymentOptional.orElseThrow(
() -> new IllegalArgumentException("orderId로 찾을 수 없음"));
// amount가 Int여서 equals를 사용 불가 int는 기본 타입, Integer는 객체 타입
if(payment.getTotalAmount() != amount){
return;
}
// 결제 인증 완료로 변경
payment.updatePayStatus(PayStatus.IN_PROGRESS);
paymentRepository.save(payment);
}- 검증이 완료되면 status가 변경된다
- 검증이 완료되면 Url
success?paymentType=orderId=&paymentKey=&amount=을 보내준다.
- 결제 승인 페이지
- 승인
- /confirm을 호출하여 결제를 진행한다.
// Controller
@RequestMapping(value = "/confirm")
public ResponseEntity<JSONObject> confirmPayment(@RequestBody String jsonBody){
// 생략 ...
Reader reader = new InputStreamReader(responseStream, StandardCharsets.UTF_8);
JSONObject jsonObject = (JSONObject) parser.parse(reader);
responseStream.close();
System.out.println("########## tossWidgetController confirm #################");
System.out.println("jsonObject = " + jsonObject);
System.out.println("########## tossWidgetController confirm #################");
tossWidgetService.successPayment(jsonObject);
}
// Service
@Transactional
public void successPayment(JSONObject jsonObject) {
PaymentSuccessDto paymentSuccessDto = mapDto(jsonObject);
Optional<Payment> paymentOptional = paymentRepository.findByTossOrderId(
paymentSuccessDto.getTossOrderId());
Payment payment = paymentOptional.orElseThrow(
() -> new IllegalArgumentException("orderId로 찾을 수 없음"));
payment.successPayment(paymentSuccessDto.getPaymentKey(),
paymentSuccessDto.getPayType(),
paymentSuccessDto.getTossOrderName(),
PayStatus.DONE,
paymentSuccessDto.getMethod(),
paymentSuccessDto.getProvider(),
paymentSuccessDto.getApprovedAt()
);
paymentRepository.save(payment);
System.out.println("########### 성공 ##############");
}
private PaymentSuccessDto mapDto(JSONObject jsonObject) {
JSONObject easyPay = (JSONObject) jsonObject.get("easyPay");
String provider = (String) easyPay.get("provider");
return PaymentSuccessDto.builder()
.paymentKey((String) jsonObject.get("paymentKey"))
.tossOrderId((String) jsonObject.get("orderId"))
.tossOrderName((String) jsonObject.get("orderName"))
.method((String) jsonObject.get("method"))
.provider(provider)
.payType(PayType.valueOf((String) jsonObject.get("type")))
.payStatus(PayStatus.valueOf((String) jsonObject.get("status")))
.approvedAt(OffsetDateTime.parse((String) jsonObject.get("approvedAt")).toString())
.build();
}작업 중에 발생한 오류와 이를 어떻게 해결했는지 기록합니다.
- 오류 내용: [결제 정보를 찾는 UUID 발급 방식 문제 발생]
- 원인 분석:
- DB 저장 전
.tossOrderId(UUID.randomUUID().toString())로tossOrderId를 발급하였는데 결제 정보를 tossOrderId로 찾기 때문에 DB저장 전에는 확인할 수가 없어 결제 검증을 할 수가 없다. - UUID의 중복 문제
- DB 저장 전
- 해결 방법:
- 서비스에서 랜덤UUID로 tossOrderId를 만들어서 넘겨주기로 결정하였다.
- 중복문제 해결을 위해 현재시간 밀리초, 랜덤 문자를 사용하여 중복 방지
public class controller{
// tossOrderId로 사용할 랜덤 UUID 생성기
private String generateUniqueOrderId() {
String currentTime = String.valueOf(System.currentTimeMillis()); // 현재 시간을 밀리초로 가져옴
String randomString = UUID.randomUUID().toString().substring(0, 6); // 랜덤 문자열 일부만 사용 (예: 앞 6자리)
return "ORDER-" + currentTime + "-" + randomString; // "ORDER-<현재시간>-<랜덤문자열>"
}
@GetMapping("/tossPay/checkout/{currentName}")
public String GetTossPay(Model model, @PathVariable int currentName) {
DiningTableDto currentOrder = diningTableService.findTableOrder(currentName);
String tossOrderId = generateUniqueOrderId();
model.addAttribute("currentOrder", currentOrder);
model.addAttribute("getTotalDiningTablePrice", currentOrder.getTotalDiningTablePrice());
model.addAttribute("tossOrderId", tossOrderId);
return "/tossPay/checkout.html";
}
}- 바뀐 UUID의 저장된 모습이다.
- 오류 내용: 요청시간, 승인 시간 저장중 문제 발생
- 원인 분석: mySql을 사용중이서 시간을 LocalDateTime으로 저장하고 있었는데 tossPayments는 zoneTiem을 포함해서 보내주고 있다. OffsetDateTime으로 보내주고 있다는것이다. 하지만 mySql은 OffsetDateTime을 지원하지 않는다. 그래서 String으로 저장하기로 하였다.
- 해결 방법:
- 결제 요청을 할 때 정보를 저장할때 LocalDateTime.now() 사용한 것을 formatter사용하여 변환 후 String으로 저장한다.
public Payment toEntity() {
OffsetDateTime now = OffsetDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX");
String formattedNow = now.format(formatter);
return Payment.builder()
.payStatus(PayStatus.READY)
.tossOrderId(orderId)
.totalAmount(amount)
.requestedAt(formattedNow)
.build();
}- tossPayments에서 넘어온 시간은 offsetDateTime으로 받고 String으로 변환 후 넣어주면 된다.
.approvedAt(OffsetDateTime.parse((String) jsonObject.get("approvedAt")).toString())
-
TossPayments가 정말 친절한 Doucment라고 생각한다. 옛날 팀프로젝트를 할때 FullCalender API를 사용한적이 있었는데 정말 어려웠다. 근데 tossPayments는 예제도 있고 자료도 많아서 좀 더 쉽게 구현할 수 있었다. 하지만 아직 부족한 부분이 많고 한번에 이해가 되지는 않는다. 좀 더 Json부분과 Connection부분을 공부해봐야겠다.
-
앞으로의 계획:
- 취소, 조회 구현 예정
작업 중 참고한 자료 또는 링크를 정리합니다.
- tossPayments
-
TossPay API를 사용해서 결제시스템을 구현 중
- 결제 요청을 할 때 결제 내역 검증을 위한 orderId,amount,payStatus 등을 저장 구현
-
프로젝트/기능명: [결제 요청 전 검증을 위한 저장 구현]
-
진행한 작업:
- [O] 기능 개발:
- TossPay에서 제시해주는 결제 흐름이다. 결제 요청전 결제금액 확인, 취소 등을 확인하기 위해 결제 정보를 저장하라고 제시한다.
- PayMent Entity, Enum(PayType, PayStatus), PaymentRequestDto, TossWidgetController, TossWidgetService, tossPay템플릿 구현
- PaymentRequestDto에서 저장하는 값을 Builder 패턴을 사용하여 amount 값 프론트에서 받아오고 TossPay Document의 규칙을 적용하여 OrderId는 UUID를 적용 시켰다.
@Getter @Setter @Builder public class PaymentRequestDto { // 결제 요청을 받을 Dto private PayStatus payStatus; private String tossOrderId; private int amount; private LocalDateTime requestedAt; public Payment toEntity() { return Payment.builder() .payStatus(PayStatus.READY) .tossOrderId(UUID.randomUUID().toString()) .totalAmount(amount) .requestedAt(LocalDateTime.now()) .build(); } }
- POSTMAN에서 ResponseEntity.ok결과값 저장 성공이 잘 넘어 오고 있다.
- DB에도 잘 들어간다.
- TossPay Document
Redis를 사용한 이메일 인증 기능 구현
- 프로젝트/기능명: [이메일 인증 기능]
- 진행한 작업:
- [O] 기능 개발:
- Redis와 SMTP 서버를 사용하여 이메일 인증 기능 구현
- 6자리의 랜덤 숫자 코드를 입력하여 인증
- Ajax를 사용한 비동기 처리
- [O] 기능 개발:
회원 가입 기능 완성
- 프로젝트/기능명: [회원 가입 기능]
- 진행한 작업:
- [O] 기능 개발:
- @Valid를 사용하여 회원가입시 중복검사 기능을 구현 Id값으로 중복검사를 실행한다.
- BCryptPasswordEncoder를 사용하여 비밀번호를 암호화
- [O] 기능 개발:
=============================================================================
음식점의 포스 단말기 시스템을 구현 중입니다. 주된 기능으로는 주문, 주문 취소, 재고 수량 확인, 결제 등이 포함됩니다.
- 중복 검사 및 패스워드 암호화 구현:
- BCryptPasswordEncoder를 사용하여 비밀번호 암호화.
- 레디스와 SMTP 서버를 사용하여 이메일 인증 기능을 완성.
- 6자리 랜덤 숫자 코드를 사용하여 이메일 인증 구현.
- Ajax를 이용한 비동기 처리로 이메일 전송과 유효성 검사를 처리.
- 사용자가 이메일을 입력하고 전송 버튼을 클릭.
emailVerificationService.generateVerificationToken(email)로 6자리 랜덤 코드 생성.emailSenderService.sendVerificationEmail(email, code)로 이메일 전송.- 사용자가 받은 코드를 입력하고
emailVerificationService.verifyToken(code)로 유효성 검증.
- 아이템 카테고리를 구분하기 위해
@DiscriminatorColumn(name = "category")사용. - 추후 추가적인 컬럼을 사용할 계획이었으나, 현재는 enum 타입으로 구분 예정.
- 상품 등록 데이터는 정상적으로 들어가며, 이미지 처리에서 오류 발생:
- 이미지 URL이 DB에 저장되지 않음.
- 선택한 이미지 대신 기본 이미지만 보임.
- AWS S3 연동하여 이미지를 저장.
- Presigned URL을 사용하여 이미지 저장 문제 해결.
ItemType, 이름, 가격을 가져오는 쿼리 작성.- 타입별로 그룹화하여 Map으로 전달.
- 스트림을 사용해
groupingBy()로 타입별 아이템 그룹화.
- Admin과 동일한 방식으로 메뉴판 구현하였으며, 이미지 포함.
- OneToMany 관계에서 DB 부하를 줄이기 위해 중계 테이블 생성 고민.
- 상품을 선택하면 이름, 수량, 가격이 나타나며, 총 수량과 총 가격도 표시.
- 중계 테이블 생성:
Order와Item사이에OrderItem테이블 추가하여 DB 부담 경감. - 테스트 코드 작성:
OrderServiceTest로 데이터 전송 확인 후 정상 작동.
- 아이템 가격을 저장할지, 종합 가격을 저장할지 결정.
- 종합 가격으로 저장하기로 최종 결정.
diningTable엔티티를 만들어 주문과 테이블을 연결.name컬럼을 추가하여 1~10까지의 고정된 테이블 이름을 사용.
- 서버 시작 시 1~10까지의 테이블 이름을 고정값으로 설정하는 로직을 추가:
@Component @Transactional public class DiningTableComponent implements CommandLineRunner { private final DiningTableRepository diningTableRepository; public DiningTableComponent(DiningTableRepository diningTableRepository) { this.diningTableRepository = diningTableRepository; } @Override public void run(String... args) throws Exception { for (int i = 1; i <= 10; i++) { if (!diningTableRepository.existsByName(i)) { diningTableRepository.save(new DiningTable(i)); } } } }
- 포스기에서 실제 테이블을 선택하면 해당 테이블에서 메뉴를 선택할 수 있도록 구현 중.
- 주문이 완료되면, 해당 테이블에 메뉴명과 가격이 표시되도록 구현을 진행 중이다.
OrderController,OrderService에 기능을 구현했는데 유지보수가 편하게DiningController,DiningService로 분리했다.- POS 테이블을 누르면 그 테이블 번호가 넘어가게 만들고 DB에 잘 넣어지는 것까지 확인했다.
- diningTable을 찾는 방식이
int name값으로 찾고 있는데 이게 문제인 거 같다. - 근데 여기저기 기능이 섞여 있어 손 볼 게 많다.
- 뭔가 많이 꼬인 거 같은 느낌이 들고 있다. 머리를 식힌 후 다시 해결해야겠다.
- 일단 직접 테이블에서 호출해서 데이터를 찾아오지 않기위해 dto를 사용하였다.
- 거기에 불필요한 데이터를 넣지 않기위해
Strem을 사용하여 dto에 데이터들을 넣었고 - 화면에 출력까지 성공했다.
- 지금은
totalPrice에 대한 고민 중이다. 화면에서 주문할 때price * count를 해서totalPrice를 보여주는데
- 이미 합계 계산을 했는데 데이터베이스에 넣을 때
합계 로직을 구현할 필요가 없을 거 같아서 안 했는데 - 기능 분리를 위해 만들어야 할 거 같다는 생각이 든다.
- 화면에
totalPrice를 보여주고 싶은데하나의 Order에여러 개의 OrderItem이 있을 수도 있어서
- 그 데이터베이스 안에 있는 전체
OrderItem의totalPrice를 서비스에 합쳐서 전송하려고 구현 중인데 어렵다.
- 서비스에서 form을 호출하면 각각의 Dto에서 데이터를 변환하여 넘겨준다.
- DiningTableService
public DiningTableDto findTableOrder(int name) { DiningTable byName = diningTableRepository.findByName(name); DiningTableDto from = DiningTableDto.from(byName); return from; }
- DiningTableDto
public static DiningTableDto from(DiningTable diningTable) { List<OrderDto> orderDtos = diningTable.getOrders().stream() .map(OrderDto::from) .collect(Collectors.toList()); // 모든 Order의 총 금액 합산 int totalDiningTablePrice = orderDtos.stream() .mapToInt(OrderDto::getTotalOrderPrice) .sum(); System.out.println("########## DiningTableDto ###############"); System.out.println("totalDiningTablePrice = " + totalDiningTablePrice); System.out.println("########## DiningTableDto ###############"); return new DiningTableDto(diningTable.getId(), diningTable.getName(), orderDtos, totalDiningTablePrice); }
- OrderDto
public static OrderDto from(Order order) { // Order에서 OrderItemDto 리스트를 생성 List<OrderItemDto> orderItemDtos = OrderItemDto.fromList(order.getOrderItems()); // OrderItem의 총 가격 합산 int totalOrderPrice = orderItemDtos.stream() .mapToInt(orderItemDto -> orderItemDto.getTotalPrice() * orderItemDto.getCount()) .sum(); System.out.println("########## OrderDTO ###############"); System.out.println("totalOrderPrice = " + totalOrderPrice); System.out.println("########## OrderDTO ###############"); // 변환된 데이터를 사용하여 OrderDto 객체를 생성하고 반환 return new OrderDto(order.getId(), order.getCreated(), orderItemDtos, totalOrderPrice); }
- OrderItemDto
public static OrderItemDto from(OrderItem orderItem) { System.out.println("########## OrderItemDto from #########"); System.out.println("orderItem.getItem().toString() = " + orderItem.getItem().toString()); System.out.println("########## OrderItemDto from #########"); // 단일 OrderItem을 변환 ItemDto itemDto = ItemDto.from(orderItem.getItem()); return new OrderItemDto(orderItem.getId(), orderItem.getTotalPrice(), orderItem.getCount(), itemDto); } public static List<OrderItemDto> fromList(List<OrderItem> orderItems) { // OrderItem 리스트를 변환 System.out.println("########## OrderItemDto List #########"); System.out.println(orderItems.stream() .map(OrderItemDto::from) .collect(Collectors.toList())); System.out.println("########## OrderItemDto List #########"); return orderItems.stream() .map(OrderItemDto::from) .collect(Collectors.toList()); } // == 테스트 코드를 위해 생성// public OrderItemDto(Long itemId, int totalPrice, int count) { this.itemId = itemId; this.totalPrice = totalPrice; this.count = count; }
- ItemDto
public static ItemDto from(Item item) { return new ItemDto(item.getId(), item.getName(), item.getPrice()); }
- 폼에서 호출하던 걸 다 갈아엎었다.
public DiningTableDto findTableOrder(int name) {
DiningTable byName = diningTableRepository.findByName(name);
DiningTableDto diningTableDto = new DiningTableDto();
diningTableDto.setId(byName.getId());
diningTableDto.setName(byName.getName());
List<OrderDto> orders = byName.getOrders().stream()
.map(order -> {
OrderDto orderDtos = new OrderDto();
orderDtos.setId(order.getId());
System.out.println("###############DiningTableService################");
System.out.println("orderDtos.getId() = " + orderDtos.getId());
System.out.println("###############DiningTableService################");
List<OrderItemDto> items = order.getOrderItems().stream()
.map(item -> new OrderItemDto(
item.getId(),
item.getItem().getId(),
item.getOrder().getId(),
item.getItem().getName(),
item.getItem().getPrice(),
item.getCount()
))
.collect(Collectors.toList());
orderDtos.setOrderItems(items);
return orderDtos;
})
.collect(Collectors.toList());
diningTableDto.setOrders(orders);
return diningTableDto;
}- 스트림을 사용하여 필요한 데이터들을 넣어서 model에 담아서 전달하였다.
- 그렇게 하여 주문서에 한 번에 뜨게 만들었다
- 그리고 주문 버튼 하나로 주문, 업데이트가 가능하게 만들었다.
@Transactional
public void orderAdd(OrderDto orderDto) {
// orderId가 존재하면 기존 주문을 업데이트, 없으면 새로운 주문을 생성
Order order = null;
System.out.println("#######OrderService 11 #######");
System.out.println("orderDto = " + orderDto.getId());
System.out.println("#######OrderService 11 #######");
if (orderDto.getId() != null) {
System.out.println("#######OrderService 22 #######");
System.out.println("orderDto = " + orderDto.getId());
System.out.println("#######OrderService 22 #######");
// 기존 주문이 있을 경우, orderId로 주문을 찾아서 업데이트
order = orderRepository.findById(orderDto.getId())
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 주문입니다."));
// 기존 주문을 업데이트
updateExistingOrder(order, orderDto);
} else {
System.out.println("#######OrderService 33 #######");
System.out.println("orderDto = " + orderDto.getId());
System.out.println("#######OrderService 33 #######");
// 새로운 주문이면 새로운 주문 생성
DiningTable diningTable = diningTableRepository.findByName(orderDto.getDiningName());
order = new Order();
order.addDining(diningTable);
// 새로운 주문 항목 추가
Order finalOrder = order;
orderDto.getOrderItems().forEach(orderItemDto -> {
Item item = itemRepository.findById(orderItemDto.getItemId()).orElseThrow();
OrderItem orderItem = new OrderItem();
orderItem.addOrderItem(item, orderItemDto.getTotalPrice(), orderItemDto.getCount());
finalOrder.addOrderItem(orderItem);
});
// 새로운 주문 저장
orderRepository.save(order);
}
}- orderId로 새로운 주문인지 기존의 주문인지 유효성 검사를 하였고
- order에는 여러 개의 OrderItem이 있는 경우가 있을 수 있음으로
- 업데이트는 OrderItemId를 기준으로 업데이트하였다.
- order 수정중 하던 도중 기존 주문에 수정과 새로운 주문을 같이 했을때
- 기존 주문이 새로운 주문으로 함께 입력되는 중복으로 주문 되는 오류가 발생하였다.
- orderId기준으로 기존 주문과 새로운 주문으로 유효성검사를 하였는데
- 밑에 처럼 넘어 오기 때문에 무조건 새로운 주문으로 되는 중 있었다.
디티오OrderDto{id=null, diningName=3, created=null, orderItems=[
OrderItemDto{orderItemId=1702, itemId=52, orderId=1752, totalPrice=9000, count=1, name='null', price=0, itemDtos=null},
OrderItemDto{orderItemId=1703, itemId=102, orderId=1752, totalPrice=10000, count=2, name='null', price=0, itemDtos=null},
OrderItemDto{orderItemId=1704, itemId=202, orderId=1752, totalPrice=1000, count=8, name='null', price=0, itemDtos=null},
OrderItemDto{orderItemId=null, itemId=252, orderId=null, totalPrice=7000, count=5, name='null', price=0, itemDtos=null}]}- add, update 분리
- 컨트롤러에서 list로 만들어 null값을 기준으로 새로운 주문과 기존 주문을 구분하였다.
public ResponseEntity<Map<String, String>> addOrder(@RequestBody OrderDto orderDto) { List<OrderItemDto> addItems = new ArrayList<>(); List<OrderItemDto> updateItems = new ArrayList<>(); // orderDto의 orderItems를 순회하여 분리 for (OrderItemDto orderItem : orderDto.getOrderItems()) { if (orderItem.getOrderId() == null) { addItems.add(orderItem); // orderId가 null인 항목은 추가 } else { updateItems.add(orderItem); // orderId가 null이 아닌 항목은 업데이트 } // 분리된 addItems와 updateItems를 각각 처리 if (!addItems.isEmpty()) { // 새로운 주문 항목을 추가하는 서비스 호출 orderService.orderAdd(orderDto, addItems); } if (!updateItems.isEmpty()) { // 기존 주문 항목을 업데이트하는 서비스 호출 orderService.orderUpdate(orderDto, updateItems); } Map<String, String> response = new HashMap<>(); response.put("redirectUrl", "/orderTable"); return ResponseEntity.ok(response); } }
- list를 활용하여 orderItemId를 기준으로 업데이트 한 후 order를 호출하여 변경하였다.
@Transactional public void orderUpdate(OrderDto orderDto, List<OrderItemDto> updateItems) { System.out.println("####### orderUpdate #######"); System.out.println("orderDto = " + orderDto.getId()); System.out.println("####### orderUpdate #######"); // 1. 업데이트 항목에 해당하는 orderItemId들을 기준으로 각 항목을 업데이트 updateItems.forEach(orderItemDto -> { // updateItems에서 각 항목을 순차적으로 처리 OrderItem existingOrderItem = orderItemRepository.findById(orderItemDto.getOrderItemId()) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 주문 항목입니다.")); // 기존 OrderItem의 변경 감지를 트리거하기 위해 값 설정 existingOrderItem.updateOrderItem(orderItemDto.getTotalPrice(), orderItemDto.getCount()); // 해당 OrderItem을 포함하는 Order를 찾아서 변경된 값을 반영 Order order = existingOrderItem.getOrder(); System.out.println("####### orderUpdate22 #######"); System.out.println("order = " + order); System.out.println("####### orderUpdate 22#######"); orderRepository.save(order); // 변경된 주문을 저장 (변경 감지) }); }
- orderItem을 삭제하는 기능을 추가하였다.
- 엔티티 설계를 잘못한 느낌이 크게 든다.
- 일단 지금은 ajax로 json으로 받아올때 cancelOrderItemId를 같이 받아오기로 하여 삭제 기능을 처리하였다.
const orderDto = {
diningName: tableName,
orderItems: Object.keys(orderSummary).map(itemId => {
const item = orderSummary[itemId];
console.log(currentOrder.orders.id);
console.log("orderSummary itemId:", item.orderId); // 여기서 order.id 확인
return {
orderId: item.orderId,
orderItemId: item.orderItemId,
itemId: itemId,
count: item.quantity,
totalPrice: item.price
};
}),
cancelOrderItemId: cancelOrderItemId
};
@PostMapping("/order/add")
public ResponseEntity<Map<String, String>> addOrder(@RequestBody OrderDto orderDto) {
// 주문 삭제
if (orderDto.getCancelOrderItemId() != null) {
System.out.println("########### 오더 컨트롤러 delete #############");
System.out.println("orderDto.getCancelOrderItemId() = " + orderDto.getCancelOrderItemId());
System.out.println("########### 오더 컨트롤러 delete#############");
System.out.println("===============================================");
orderService.orderItemDelete(orderDto);
}
} @Transactional
public void orderItemDelete(OrderDto orderDto){
orderDto.getCancelOrderItemId().forEach(orderItemId ->{
orderItemRepository.deleteById(orderItemId);
System.out.println("######## OrderDelete ###########");
System.out.println("orderItemId = " + orderItemId + "삭제");
System.out.println("######## OrderDelete ###########");
System.out.println("======================================");
});
}- orderItem이 아주 잘 삭제되었다.
- orderItem을 다 취소한 경우 orderItem은 다 삭제되지만 연관 관계로 연결되어있는 order가 삭제되지 않는 오류가 발생하였다.
orderId=[orderItemId,orderItemId]
- 이런식으로 Map<Long, List>형식으로 데이터를 받아와
public ResponseEntity<Map<String, String>> addOrder(@RequestBody OrderDto orderDto) {
// 주문 삭제
if (orderDto.getCancelOrderId() != null) {
System.out.println("########### 오더 컨트롤러 delete #############");
System.out.println("orderDto.getCancelOrderItemId() = " + orderDto.getCancelOrderId());
System.out.println("########### 오더 컨트롤러 delete#############");
System.out.println("===============================================");
orderService.orderItemDelete(orderDto);
}
}- 유효성 검사를 거친 뒤
@Transactional
public void orderItemDelete(OrderDto orderDto){
orderDto.getCancelOrderId().forEach((orderId, orderItemIds) -> {
orderItemIds.forEach(orderItemId -> {
orderItemRepository.deleteById(orderItemId);
System.out.println("######## orderItemId ###########");
System.out.println("orderItemId = " + orderItemId + "삭제");
System.out.println("######## orderItemId ###########");
System.out.println("======================================");
});
// 영속성 컨텍스트를 DB에 반영
orderItemRepository.flush();
//order를 조회
Optional<Order> optionalOrder = orderRepository.findById(orderId);
if (optionalOrder.isPresent()) {
Order order = optionalOrder.get();
System.out.println("######## OrderDelete ###########");
System.out.println("order = " + order);
System.out.println("######## OrderDelete ###########");
System.out.println("======================================");
if (order.getOrderItems().isEmpty()) {
orderRepository.deleteById(orderId);
System.out.println("######## OrderDelete ###########");
System.out.println("orderId = " + orderId + "삭제");
System.out.println("######## OrderDelete ###########");
System.out.println("======================================");
}
}
});
}- 먼저 orderItem을 지워주고 flush로 db에 바로 반영해 준다.
- 이유는 만약 orderItem을 모두 지우는 상황이 오면 연관관계인 order도 지워야 하기 때문이다.
- 하지만 모든 작업이 끝나기 전에는 영속성 컨텍스트로 남아있기 때문에 DB에 반영이 안 될 수도 있기 때문에 flush()를 사용하는 것이다.
- 신규 주문, 기존 주문 업데이트, 주문 삭제를 동시에 진행하면 기능이 동작은 하지만 변경되지 않는다.
- 이유는 아마 동시성의 문제인 거 같다.
- 3가지 부분의 유효성 검사를 다시 손봐야 할 거 같다.
- 신규 주문, 기존 항목 업데이트, 삭제 따로따로 기능은 잘 실행되었지만
- 동시에 기능을 실행하면 삭제가 안 되는 오류가 발생하였다.
- 동시성의 문제였다.
@PostMapping("/order/add")
public ResponseEntity<Map<String, String>> addOrder(@RequestBody OrderDto orderDto) {
if (orderDto.getCancelOrderId() != null) {
orderService.orderItemDelete(orderDto);
}
if (!orderDto.getOrderItems().isEmpty()) {
List<OrderItemDto> addItems = new ArrayList<>();
List<OrderItemDto> updateItems = new ArrayList<>();
splitOrderItems(orderDto, addItems, updateItems);
if (!addItems.isEmpty()) {
orderService.orderAdd(orderDto, addItems);
}
if (!updateItems.isEmpty()) {
orderService.orderUpdate(orderDto, updateItems);
}
}
Map<String, String> response = new HashMap<>();
response.put("redirectUrl", "/orderTable");
return ResponseEntity.ok(response);
}- 삭제 기능부터 유효성 검사를 시작하였다.
- 삭제할 게 있으면 삭제한 후 flush로 영속성컨텍스에 있는 삭제 항목을 먼저 처리한다.
- 그 후 신규 주문과 업데이트의 분리 작업을 시작한다.
// 유효성 검사를 통해 addItems,updateItems에 뎉이터를 넣고 있다.
private void splitOrderItems(OrderDto orderDto, List<OrderItemDto> addItems, List<OrderItemDto> updateItems) {
for (OrderItemDto orderItem : orderDto.getOrderItems()) {
if (orderItem.getOrderId() == null) {
addItems.add(orderItem);
} else {
updateItems.add(orderItem);
}
}
}- 유효성 검사를 통해 신규 주문 리스트와, 업데이트 리스트를 분리 후 각각의 서비스를 호출한다.
- 토스 poss를 벤치 마킹하였기 때문에 결제 시스템도 toss Pay를 사용하기로 하였다.
- toss Pay의 결제 기능은 구현은 개발 문서에 아주 자세하게 설명되어 있고 샌드박스를 사용할 수 있어서 보다 편하게 구현이 가능하다.
- 출처 : https://docs.tosspayments.com/guides/v2/payment-widget/integration?backend=java
TIL 양식
# TIL (Today I Learned)
## 📅 날짜: YYYY-MM-DD
---
## 1. 진행 상황 🛠️
오늘 어떤 작업을 진행했는지 정리합니다.
- **프로젝트/기능명**: [여기에 작업한 프로젝트나 기능 이름을 작성]
- **진행한 작업**:
- [ ] 기능 개발: [구현한 기능 설명]
- [ ] 테스트 코드 작성: [테스트한 내용]
- [ ] 리팩토링: [리팩토링한 부분과 이유]
- [ ] 문서 작성: [작성한 문서 또는 기록]
---
## 2. 오류 및 해결 방법 ⚠️
작업 중에 발생한 오류와 이를 어떻게 해결했는지 기록합니다.
### 🔍 오류 1
- **오류 내용**: [발생한 오류에 대한 상세 설명]
- **원인 분석**: [오류가 발생한 이유]
- **해결 방법**:
1. [해결 과정 1]
2. [해결 과정 2]
### 🔍 오류 2
- **오류 내용**: [발생한 오류에 대한 상세 설명]
- **원인 분석**: [오류가 발생한 이유]
- **해결 방법**:
1. [해결 과정 1]
2. [해결 과정 2]
---
## 3. 회고 📝
오늘 작업에 대한 회고와 느낀 점, 개선할 점 등을 작성합니다.
- **잘한 점**:
- [어떤 점에서 효율적이었는지, 만족스러웠는지 기록]
- **아쉬운 점**:
- [개선해야 할 부분, 다음에 더 잘할 수 있는 부분 기록]
- **앞으로의 계획**:
- [다음 작업에서 적용할 계획 또는 학습할 주제]
---
## 🔗 참고 자료 📚
작업 중 참고한 자료 또는 링크를 정리합니다.
- [문서 링크나 블로그 포스트]
- [레퍼런스 코드나 관련 강의]
---













