Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,4 @@ public ResponseEntity<?> withdraw(@PathVariable Long clubId) {
clubService.leaveClub(clubId);
return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(null));
}

@Operation(summary = "가입하고 있는 모임 조회", description = "가입하고 있는 모임을 조회한다.")
@GetMapping
public ResponseEntity<?> getClubNames() {
List<ClubNameResponseDto> clubNameResponseDto = clubService.getClubNames();
return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(clubNameResponseDto));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,16 @@ public interface UserClubRepository extends JpaRepository<UserClub,Long> {
List<Long> findUserIdByClubIds(@Param("clubIds") List<Long> clubIds);

List<UserClub> findByUserUserIdIn(Collection<Long> userIds);

@Query("""
select c,
(select count(uc2)
from UserClub uc2
where uc2.club = c)
from UserClub uc
join uc.club c
where uc.user.userId = :userId
group by c
""")
List<Object[]> findMyClubsWithMemberCount(@Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,19 @@ public void leaveClub(Long clubId) {
userClubRepository.delete(userClub);
}

/* 가입하고 있는 모임 조회*/
public List<ClubNameResponseDto> getClubNames() {
Long userId = userService.getCurrentUser().getUserId();

List<UserClub> userClubs = userClubRepository.findByUserUserId(userId);

return userClubs.stream()
.map(UserClub::getClub)
.filter(Objects::nonNull)
.map(c -> ClubNameResponseDto.builder()
.clubId(c.getClubId())
.name(c.getName())
.build())
.toList();
}
// /* 가입하고 있는 모임 조회*/
// public List<ClubNameResponseDto> getClubNames() {
// Long userId = userService.getCurrentUser().getUserId();
//
// List<UserClub> userClubs = userClubRepository.findByUserUs(userId);
//
// return userClubs.stream()
// .map(UserClub::getClub)
// .filter(Objects::nonNull)
// .map(c -> ClubNameResponseDto.builder()
// .clubId(c.getClubId())
// .name(c.getName())
// .build())
// .toList();
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public ResponseEntity<?> confirmPayment(@RequestBody ConfirmTossPayRequest req)
return ResponseEntity.ok(CommonResponse.success(response));
}

@Operation(summary = "결제 실패 기록", description = "토스페이먼츠 결제 요청의 최종 실패를 기록합니다.")
@PostMapping(value = "/fail")
public ResponseEntity<?> failPayment(@RequestBody ConfirmTossPayRequest req) {
paymentService.reportFail(req);
return ResponseEntity.ok(CommonResponse.success(null));
}

// // 결제 취소 요청
// @PostMapping("/cancel/{paymentKey}")
// public ResponseEntity<?> cancelPayment(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.onlyone.domain.payment.entity;

import com.example.onlyone.domain.payment.dto.response.ConfirmTossPayResponse;
import com.example.onlyone.domain.wallet.entity.WalletTransaction;
import com.example.onlyone.global.BaseTimeEntity;
import jakarta.persistence.*;
Expand All @@ -22,14 +23,14 @@ public class Payment extends BaseTimeEntity {
@Column(name = "toss_payment_key", nullable = false, unique = true)
private String tossPaymentKey;

@Column(name = "toss_order_id", nullable = false)
@Column(name = "toss_order_id", nullable = false, unique = true)
private String tossOrderId;

@Column(name = "total_amount", nullable = false)
private Long totalAmount;

@Enumerated(EnumType.STRING)
@Column(name = "method", nullable = false)
@Column(name = "method")
private Method method;

@Enumerated(value = EnumType.STRING)
Expand All @@ -39,4 +40,12 @@ public class Payment extends BaseTimeEntity {
@OneToOne(mappedBy = "payment", fetch = FetchType.LAZY)
private WalletTransaction walletTransaction;

public void updateStatus(Status status) {
this.status = status;
}

public void updateOnConfirm(String status, String method) {
this.status = Status.valueOf(status);
this.method = Method.valueOf(method);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.example.onlyone.domain.payment.repository;

import com.example.onlyone.domain.payment.entity.Payment;
import jakarta.persistence.LockModeType;
import jakarta.validation.constraints.NotBlank;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;

import java.util.Optional;

public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByTossOrderId(String tossOrderId);
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Payment> findByTossPaymentKey(String tossPaymentKey);

boolean existsByTossPaymentKey(String paymentKey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
Expand Down Expand Up @@ -77,16 +78,25 @@ public void confirmPayment(@Valid SavePaymentRequestDto dto, HttpSession session
// session.removeAttribute(dto.getOrderId());
}

/* 토스페이먼츠 결제 승인 */
@Transactional
public ConfirmTossPayResponse confirm(ConfirmTossPayRequest req) {
// 멱등성 체크: 이미 처리된 주문인지 확인
Optional<Payment> existingPayment = paymentRepository.findByTossOrderId(req.getOrderId());
if (existingPayment.isPresent()) {
// 멱등성, 동시성 보호: paymentKey로 행 잠금
Payment payment = paymentRepository.findByTossPaymentKey(req.getPaymentKey())
.orElseGet(() -> {
Payment p = Payment.builder()
.tossOrderId(req.getOrderId())
.tossOrderId(req.getOrderId())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중복된 tossOrderId 설정

tossOrderId가 두 번 설정되어 있습니다.

                    Payment p = Payment.builder()
                            .tossOrderId(req.getOrderId())
-                            .tossOrderId(req.getOrderId())
                            .status(Status.IN_PROGRESS)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.tossOrderId(req.getOrderId())
Payment payment = paymentRepository.findByTossPaymentKey(req.getPaymentKey())
.orElseGet(() -> {
Payment p = Payment.builder()
.tossOrderId(req.getOrderId())
.status(Status.IN_PROGRESS)
.totalAmount(req.getAmount())
.build();
return paymentRepository.save(p);
});
🤖 Prompt for AI Agents
In src/main/java/com/example/onlyone/domain/payment/service/PaymentService.java
around line 88, the builder/setter call .tossOrderId(req.getOrderId()) is
applied twice; remove the duplicate invocation (or if the second was meant to
set a different field, replace it with the correct setter) so tossOrderId is set
only once and the builder state is unambiguous.

.status(Status.IN_PROGRESS)
.totalAmount(req.getAmount())
.build();
return paymentRepository.save(p);
});
if (payment.getStatus() == Status.DONE) {
throw new CustomException(ErrorCode.ALREADY_COMPLETED_PAYMENT);
}
ConfirmTossPayResponse response;
// 토스페이먼츠 승인 API 호출
final ConfirmTossPayResponse response;
try {
// tossPaymentClient를 통해 호출
response = tossPaymentClient.confirmPayment(req);
} catch (FeignException.BadRequest e) {
throw new CustomException(ErrorCode.INVALID_PAYMENT_INFO);
Expand All @@ -95,13 +105,14 @@ public ConfirmTossPayResponse confirm(ConfirmTossPayRequest req) {
} catch (Exception e) {
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
// 지갑/거래 업데이트
User user = userService.getCurrentUser();
Wallet wallet = walletRepository.findByUser(user)
.orElseThrow(() -> new CustomException(ErrorCode.WALLET_NOT_FOUND));
int amount = Math.toIntExact(req.getAmount());
// 포인트 업데이트
int amount = Math.toIntExact(response.getTotalAmount());
// 지갑 증액
wallet.updateBalance(wallet.getBalance() + amount);
// 충전(결제) 기록
// 거래 기록
WalletTransaction walletTransaction = WalletTransaction.builder()
.type(Type.CHARGE)
.amount(amount)
Expand All @@ -111,15 +122,47 @@ public ConfirmTossPayResponse confirm(ConfirmTossPayRequest req) {
.targetWallet(wallet)
.build();
walletTransactionRepository.save(walletTransaction);
Payment payment = Payment.builder()
.tossPaymentKey(response.getPaymentKey())
.tossOrderId(response.getOrderId())
.totalAmount(response.getTotalAmount())
.method(Method.from(response.getMethod()))
.status(Status.from(response.getStatus()))
.walletTransaction(walletTransaction)
.build();
payment.updateOnConfirm(response.getStatus(), response.getMethod());
walletTransaction.updatePayment(payment);
return response;
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reportFail(ConfirmTossPayRequest req) {
// 멱등성, 동시성 보호: paymentKey로 행 잠금
Payment payment = paymentRepository.findByTossPaymentKey(req.getPaymentKey())
.orElseGet(() -> {
Payment p = Payment.builder()
.tossOrderId(req.getOrderId())
.totalAmount(req.getAmount())
.status(Status.IN_PROGRESS)
.build();
return paymentRepository.save(p);
});
// 이미 완료된 결제면 기록하지 않음
if (payment.getStatus() == Status.DONE) {
return;
}
if (req.getPaymentKey() != null &&
payment.getTossPaymentKey() != null &&
payment.getTossPaymentKey().equals(req.getPaymentKey())) {
return;
}
Comment on lines +146 to +150
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이상한 멱등성 체크 로직

이 멱등성 체크 로직이 부자연스럽습니다. 이미 findByTossPaymentKey로 조회한 payment이므로 paymentKey가 일치하는 것은 당연합니다. 불필요한 코드입니다.

-        if (req.getPaymentKey() != null &&
-                payment.getTossPaymentKey() != null &&
-                payment.getTossPaymentKey().equals(req.getPaymentKey())) {
-            return;
-        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (req.getPaymentKey() != null &&
payment.getTossPaymentKey() != null &&
payment.getTossPaymentKey().equals(req.getPaymentKey())) {
return;
}
🤖 Prompt for AI Agents
In src/main/java/com/example/onlyone/domain/payment/service/PaymentService.java
around lines 146-150, the if-block that checks req.getPaymentKey() against
payment.getTossPaymentKey() is redundant because payment was already retrieved
by that key; remove this entire conditional and its return to simplify the flow
(ensure any intended idempotency is covered by the earlier lookup path or by
more appropriate checks elsewhere).

// 실패 기록
User user = userService.getCurrentUser();
Wallet wallet = walletRepository.findByUser(user)
.orElseThrow(() -> new CustomException(ErrorCode.WALLET_NOT_FOUND));
WalletTransaction walletTransaction = WalletTransaction.builder()
.type(Type.CHARGE)
.amount(Math.toIntExact(req.getAmount()))
.balance(wallet.getBalance())
.walletTransactionStatus(WalletTransactionStatus.FAILED)
.wallet(wallet)
.targetWallet(wallet)
.build();
walletTransactionRepository.save(walletTransaction);
// Payment 갱신
payment.updateStatus(Status.CANCELED);
walletTransaction.updatePayment(payment);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.example.onlyone.domain.search.controller;

import com.example.onlyone.domain.search.dto.request.SearchFilterDto;
import com.example.onlyone.domain.search.dto.response.ClubResponseDto;
import com.example.onlyone.domain.search.service.SearchService;
import com.example.onlyone.global.common.CommonResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@Tag(name = "Search")
@RequiredArgsConstructor
Expand Down Expand Up @@ -72,4 +76,10 @@ public ResponseEntity<?> getClubsByTeammates(@RequestParam(defaultValue = "0") i
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(CommonResponse.success(searchService.getClubsByTeammates(page, size)));
}

@Operation(summary = "가입하고 있는 모임 조회", description = "가입하고 있는 모임을 조회한다.")
@GetMapping("/user")
public ResponseEntity<?> getClubNames() {
return ResponseEntity.status(HttpStatus.OK).body(CommonResponse.success(searchService.getMyClubs()));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.onlyone.domain.search.service;

import com.example.onlyone.domain.club.entity.Club;
import com.example.onlyone.domain.club.entity.UserClub;
import com.example.onlyone.domain.club.repository.ClubRepository;
import com.example.onlyone.domain.club.repository.UserClubRepository;
import com.example.onlyone.domain.interest.entity.Category;
Expand Down Expand Up @@ -177,4 +178,16 @@ private List<ClubResponseDto> convertToClubResponseDtoWithJoinStatus(List<Object
return ClubResponseDto.from(club, memberCount, isJoined);
}).toList();
}

// 내 모임 목록 조회
public List<ClubResponseDto> getMyClubs() {
Long userId = userService.getCurrentUser().getUserId();
List<Object[]> rows = userClubRepository.findMyClubsWithMemberCount(userId);
return rows.stream().map(row -> {
Club club = (Club) row[0];
Long memberCount = (Long) row[1];
return ClubResponseDto.from(club, memberCount, true);
}).toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum SettlementStatus {
REQUESTED,
COMPLETED,
PENDING,
FAILED
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ public void updateSettlement(SettlementStatus settlementStatus, LocalDateTime co
this.settlementStatus = settlementStatus;
this.completedTime = completedTime;
}

public void updateStatus(SettlementStatus settlementStatus) {
this.settlementStatus = settlementStatus;
}
}
Loading