[BE] [FEAT] 복식부기 기반 포인트 원장 시스템 도입#200
Hidden character warning
Conversation
Walkthrough포인트 시스템을 단순 이력에서 복식부기 원장 기반으로 전환했습니다. Account/PointTransaction/LedgerEntry 엔티티·리포지토리·서비스와 초기화·마이그레이션 로직이 추가되었고, BettingService·회원가입 흐름이 원장 기반 트랜잭션으로 전환되었으며 일부 테스트가 비활성화되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UserService as UserService
participant AccountService as AccountService
participant PointLedgerService as PointLedgerService
participant AccountRepository as AccountRepository
participant LedgerEntryRepository as LedgerEntryRepository
User->>UserService: signup()
activate UserService
UserService->>AccountService: createUserAccount(userId)
activate AccountService
AccountService->>AccountRepository: save(new Account)
AccountRepository-->>AccountService: account
deactivate AccountService
UserService->>PointLedgerService: processTransaction(SIGNUP_REWARD, refId, AccountEntry...)
activate PointLedgerService
PointLedgerService->>AccountRepository: findByAccountName(SYSTEM_ISSUANCE)
AccountRepository-->>PointLedgerService: systemAccount
PointLedgerService->>PointLedgerService: validate sum == 0
rect rgba(100, 200, 100, 0.5)
Note over PointLedgerService: 각 AccountEntry 처리
PointLedgerService->>AccountRepository: update/save account balance
PointLedgerService->>LedgerEntryRepository: save(LedgerEntry)
end
LedgerEntryRepository-->>PointLedgerService: entries saved
deactivate PointLedgerService
UserService-->>User: signup complete
deactivate UserService
sequenceDiagram
actor Bettor
participant BettingService as BettingService
participant BetRoundRepository as BetRoundRepository
participant PointLedgerService as PointLedgerService
participant AccountService as AccountService
participant AccountRepository as AccountRepository
Bettor->>BettingService: postUserBet(roundId, stake)
activate BettingService
rect rgba(100, 150, 200, 0.5)
Note over BettingService: 유료 베팅(stake>0)인 경우
BettingService->>AccountService: getUserAccount(userId)
BettingService->>PointLedgerService: processTransaction(BETTING_STAKE, refId, entries...)
PointLedgerService->>AccountRepository: update balances / save ledger entries
end
BettingService->>BetRoundRepository: incrementUpStats(roundId, stake)
BetRoundRepository-->>BettingService: updated
BettingService-->>Bettor: bet recorded
deactivate BettingService
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75분 Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java (1)
21-24: Tag 설명과 실제 기능 불일치Tag 설명에 "리더보드 관련 API 제공"이 포함되어 있지만, 리더보드 엔드포인트는 제거되었습니다. 설명을 현재 기능에 맞게 업데이트하세요.
💡 제안하는 수정
`@Tag`( - name = "포인트 내역 및 리더보드 API", - description = "포인트 내역 조회 및 리더보드 관련 API 제공" + name = "포인트 내역 API", + description = "포인트 내역 조회 API 제공" )backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java (3)
111-142: OAuth 회원가입 시 포인트 계정 미생성
findOrCreateUser에서 새 사용자를 생성할 때completeSignup()이 호출되지 않습니다. 이로 인해 OAuth로 가입한 사용자는 포인트 계정과 가입 포인트를 받지 못합니다. 일반 회원가입(signUp)과의 일관성을 위해 OAuth 경로에서도completeSignup을 호출해야 합니다.🐛 제안하는 수정
User savedUser = userRepository.save(newUser); String encryptedToken = tokenEncryptor.encrypt(oauthInfo.getAccessToken()); UserOauthAccount newOauth = UserOauthAccount.builder() .user(savedUser) .provider(oauthInfo.getProvider()) .providerUid(providerUid) .accessToken(encryptedToken) .build(); oauthAccountRepository.save(newOauth); + // 포인트 계정 생성 및 기본 포인트 제공 + completeSignup(savedUser); return savedUser;
332-360:upsertOAuthUser에서도 포인트 계정 미생성
upsertOAuthUser메서드에서도 새 사용자 생성 시completeSignup()이 호출되지 않습니다.findOrCreateUser와 동일하게 수정이 필요합니다.🐛 제안하는 수정
User savedUser = userRepository.save(newUser); UserOauthAccount oauthAccount = UserOauthAccount.builder() .user(savedUser) .provider(authProvider) .providerUid(providerUid) .build(); oauthAccountRepository.save(oauthAccount); + // 포인트 계정 생성 및 기본 포인트 제공 + completeSignup(savedUser); return savedUser;
100-108: OAuth 회원가입 경로에서 포인트 계정 및 가입 보상 미지급
findOrCreateUser()(라인 113)와upsertOAuthUser()(라인 334)는completeSignup()을 호출하지 않아, OAuth를 통한 회원가입 사용자가 포인트 계정 생성 및 100P 가입 보상을 받지 못합니다. 일반 회원가입(signUp)과 일관성 있게 두 OAuth 경로에서도completeSignup()을 호출하세요.추가로,
completeSignup()에서processTransaction()호출 시 발생할 수 있는CustomException은 현재의DataIntegrityViolationException핸들러로 처리되지 않습니다.
🤖 Fix all issues with AI agents
In
`@backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java`:
- Around line 24-30: The JPQL UPDATE in BetRoundRepository.incrementUpStats uses
`@Modifying` but doesn't clear the persistence context, so subsequent reads in the
same transaction return stale BetRound state; update the `@Modifying` annotation
on incrementUpStats to include clearAutomatically = true (i.e.,
`@Modifying`(clearAutomatically = true)) so the EntityManager cache is cleared
after the bulk update and later reads (e.g., postUserBet() reading
round.getUpBetCount()/getDownBetCount()) see the updated values.
In
`@backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java`:
- Around line 347-354: The BETTING_REWARD branch violates PointLedgerService's
zero-sum requirement because it credits both poolAccount and userAccount; update
the call to processTransaction (in BettingService) to make the user entry a
debit instead of a credit (use AccountEntry.debit for userAccount while keeping
poolAccount as AccountEntry.credit) so the sum of entries is zero and user
balance changes directionally consistent with stake/cancel branches; ensure the
TransactionReason.BETTING_REWARD invocation and any totalStake handling
(totalStake += reward) remain correct after this change.
In `@backend/src/main/java/org/sejongisc/backend/point/dto/PointHistoryItem.java`:
- Around line 18-25: The static factory PointHistoryItem.from calls
entry.getTransaction().getReason() which can NPE if LedgerEntry.transaction is
null; either add a null-safe check in PointHistoryItem.from to handle a missing
transaction (e.g., use a fallback reason or throw a clear IllegalStateException
mentioning entry.getEntryId()), or enforce non-null at the entity level by
adding `@JoinColumn`(nullable = false) on LedgerEntry.transaction; update
PointHistoryItem.from (referencing getTransaction(), getReason(), and
getEntryId()) to perform the null-check and handle/report the absence if you
choose the defensive fix.
In `@backend/src/main/java/org/sejongisc/backend/point/entity/Account.java`:
- Line 32: The Account entity's balance field (private Long balance = 0L;) will
be ignored by Lombok's `@Builder` unless annotated with `@Builder.Default`; annotate
the balance field with `@Builder.Default` (keeping the = 0L initializer) so that
the Account builder (class Account using `@Builder`) will apply the default value
when not explicitly set.
In
`@backend/src/test/java/org/sejongisc/backend/point/controller/PointHistoryControllerTest.java`:
- Around line 1-80: The PointHistoryControllerTest class is entirely commented
out (PointHistoryControllerTest and its test methods 리더보드_성공_200리턴 /
리더보드_잘못된_period시_예외), which hides test intent and reduces coverage; choose one
remediation: (A) delete the file and create a follow-up issue to reintroduce
tests against the new ledger system, (B) re-enable the test class and mark it
with `@Disabled`("ledger refactor - ISSUE-XXXX") to keep it discoverable, or (C)
update the tests now to match the new ledger API; implement the chosen option
consistently (add `@Disabled` with the issue ID if picking B, or open a tracking
issue and reference PointHistoryControllerTest and the two test methods if
picking A), and ensure any MockBean usages (pointHistoryService, AuditorAware,
JpaMetamodelMappingContext) are adjusted when re-enabling or rewriting tests.
In
`@backend/src/test/java/org/sejongisc/backend/point/service/PointHistoryServiceTest.java`:
- Around line 1-182: The entire PointHistoryServiceTest class is commented out;
restore test coverage by either (A) uncommenting and updating the tests to call
the new ledger-based PointHistoryService API (replace usages of
getPointLeaderboard, createPointHistory, getPointHistoryListByUserId,
deleteAllPointHistoryByUserId with the new ledger methods and adapt
inputs/expectations), (B) remove these legacy tests and create new tests
targeting the new ledger methods (mock PointHistoryRepository/UserRepository
usage as needed), or (C) if this is temporary during refactor, annotate the test
class with `@Disabled`("리팩토링 진행 중") to preserve intent; locate the class
PointHistoryServiceTest and its test methods (포인트리더보드_성공, 포인트기록_생성_성공_출석체크_적립,
포인트기록_페이징조회_성공, 유저탈퇴시_포인트기록_삭제성공, etc.) to apply the chosen fix.
🧹 Nitpick comments (15)
backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java (2)
24-54: 반환 타입을int로 변경하여 업데이트 성공 여부 확인 권장현재
void반환 타입으로는 해당BetRound가 존재하지 않아 업데이트된 행이 없는 경우를 감지할 수 없습니다. 잘못된 ID로 호출 시 조용히 실패하여 데이터 불일치 문제를 야기할 수 있습니다.♻️ 권장 수정안 (incrementUpStats 예시)
`@Modifying` `@Query`( "UPDATE BetRound b " + "SET b.upBetCount = b.upBetCount + 1, b.upTotalPoints = b.upTotalPoints + :points " + "WHERE b.betRoundID = :id") - void incrementUpStats(`@Param`("id") UUID id, `@Param`("points") long points); + int incrementUpStats(`@Param`("id") UUID id, `@Param`("points") long points);서비스 레이어에서 반환값 검증:
int updated = betRoundRepository.incrementUpStats(id, points); if (updated == 0) { throw new CustomException(ErrorCode.BET_ROUND_NOT_FOUND); }
40-46:decrement메서드의 음수 값 방지 검토취소 시 통계를 감소시키는 로직에서, 동시성 이슈나 잘못된 호출로 인해
upBetCount또는upTotalPoints가 음수가 될 가능성이 있습니다. 데이터베이스 레벨의 CHECK constraint 또는 서비스 레이어의 사전 검증을 고려해 보세요.backend/src/main/java/org/sejongisc/backend/point/entity/Account.java (1)
37-39: 잔액 음수 검증 부재로 인한 데이터 무결성 위험
updateBalance메서드에서 잔액이 음수가 되는 것을 방지하는 검증이 없습니다. 복식부기 시스템에서 사용자 포인트 계정이 음수가 되면 안 되는 경우, 검증 로직 추가를 고려해 주세요.♻️ 검증 로직 추가 예시
public void updateBalance(Long amount) { + if (this.balance + amount < 0) { + throw new IllegalArgumentException("잔액이 부족합니다."); + } this.balance += amount; }backend/src/main/java/org/sejongisc/backend/point/repository/TransactionalRepository.java (1)
8-9: 리포지토리 이름이 혼란을 줄 수 있습니다.
TransactionalRepository라는 이름은 Spring의@Transactional어노테이션과 혼동될 수 있습니다. 엔티티 이름과 일치하도록PointTransactionRepository로 변경하는 것이 더 명확합니다.♻️ 이름 변경 제안
-public interface TransactionalRepository extends JpaRepository<PointTransaction, UUID> { +public interface PointTransactionRepository extends JpaRepository<PointTransaction, UUID> { }backend/src/main/java/org/sejongisc/backend/point/service/PointDataInitializer.java (2)
38-47: 시스템 계정 초기화가 트랜잭션 외부에서 수행됨
initSystemAccount호출이 트랜잭션 외부에서 수행되고 있습니다. 만약 첫 번째 시스템 계정은 생성되고 두 번째 계정 생성이 실패하면, 부분적으로만 초기화된 상태가 됩니다. 시스템 계정 초기화도 트랜잭션 내에서 수행하거나, 각 초기화를 개별 트랜잭션으로 분리하여 명시적으로 관리하는 것이 좋습니다.♻️ 제안하는 수정
`@Override` public void run(String... args) { log.info("=== 포인트 시스템 계정 초기화 및 데이터 마이그레이션 시작 ==="); - // 시스템 계정 초기화 - accountService.initSystemAccount(AccountName.SYSTEM_ISSUANCE, AccountType.SYSTEM); - accountService.initSystemAccount(AccountName.BETTING_POOL, AccountType.PLATFORM); - // TODO: 기능 추가 시 계정 초기화도 추가 필요 - - // 사용자 계정 생성 + 포인트 마이그레이션 - transactionTemplate.execute(status -> { - migrateExistingUsers(); - return null; - }); + transactionTemplate.execute(status -> { + // 시스템 계정 초기화 + accountService.initSystemAccount(AccountName.SYSTEM_ISSUANCE, AccountType.SYSTEM); + accountService.initSystemAccount(AccountName.BETTING_POOL, AccountType.PLATFORM); + // TODO: 기능 추가 시 계정 초기화도 추가 필요 + + // 사용자 계정 생성 + 포인트 마이그레이션 + migrateExistingUsers(); + return null; + }); log.info("=== 포인트 시스템 초기화 완료 ==="); }
78-80: 마이그레이션 실패 시 추적 방안 부재개별 사용자 마이그레이션 실패 시 로그만 남기고 계속 진행합니다. 실패한 사용자 목록을 별도로 수집하여 마이그레이션 종료 시 요약 로그를 남기거나, 실패 건수가 임계치를 초과하면 전체 마이그레이션을 중단하는 전략을 고려해 보세요.
♻️ 실패 추적 로직 추가 예시
public void migrateExistingUsers() { List<User> users = userService.findAllUsersMissingAccount(); Account systemAccount = accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE); + List<String> failedUsers = new ArrayList<>(); for (User user : users) { try { // ... existing logic } catch (Exception e) { log.error("유저 마이그레이션 실패: {}", user.getEmail(), e); + failedUsers.add(user.getEmail()); } } + + if (!failedUsers.isEmpty()) { + log.warn("마이그레이션 실패한 사용자 수: {}, 목록: {}", failedUsers.size(), failedUsers); + } }backend/src/main/java/org/sejongisc/backend/point/controller/PointHistoryController.java (1)
34-38: 페이지네이션 파라미터 유효성 검증 부재
pageNumber와pageSize에 대한 유효성 검증이 없습니다. 음수 값이나 0이 전달되면PageRequest.of()에서IllegalArgumentException이 발생하여 500 에러가 반환됩니다.@Min또는@Positive어노테이션을 사용하여 400 응답을 반환하도록 개선하세요.♻️ 제안하는 수정
+ import jakarta.validation.constraints.Min; + import jakarta.validation.constraints.Positive; - public ResponseEntity<PointHistoryResponse> getPointHistory(`@RequestParam` int pageNumber, `@RequestParam` int pageSize, + public ResponseEntity<PointHistoryResponse> getPointHistory( + `@RequestParam` `@Min`(0) int pageNumber, + `@RequestParam` `@Positive` int pageSize, `@AuthenticationPrincipal` CustomUserDetails customUserDetails) {backend/src/main/java/org/sejongisc/backend/point/entity/PointTransaction.java (2)
26-26:refId필드의 용도가 불명확함
refId필드가 어떤 엔티티를 참조하는지 명확하지 않습니다. 베팅 ID, 주문 ID 등 참조 대상에 대한 JavaDoc 주석을 추가하면 유지보수성이 향상됩니다.📝 문서화 예시
+ /** + * 트랜잭션과 연관된 외부 참조 ID (예: 베팅 ID, 이벤트 ID 등) + */ private UUID refId;
12-27: 조회 성능을 위한 인덱스 고려
reason이나refId로 트랜잭션을 조회하는 쿼리가 예상된다면, 해당 컬럼에 인덱스를 추가하는 것이 좋습니다.💡 인덱스 추가 예시
`@Entity` `@Getter` `@NoArgsConstructor` `@AllArgsConstructor` `@Builder` +@Table(indexes = { + `@Index`(name = "idx_point_transaction_reason", columnList = "reason"), + `@Index`(name = "idx_point_transaction_ref_id", columnList = "refId") +}) public class PointTransaction extends BasePostgresEntity {backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java (1)
382-388: 회원가입 포인트 상수 추출 권장회원가입 시 지급되는 포인트
100L이 하드코딩되어 있습니다. 설정 파일이나 상수로 추출하면 향후 변경 시 유지보수가 용이합니다.♻️ 상수 추출 예시
+ private static final long SIGNUP_REWARD_POINTS = 100L; private void completeSignup(User user) { Account userAccount = accountService.createUserAccount(user.getUserId()); pointLedgerService.processTransaction( TransactionReason.SIGNUP_REWARD, user.getUserId(), - AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), 100L), - AccountEntry.debit(userAccount, 100L) + AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), SIGNUP_REWARD_POINTS), + AccountEntry.debit(userAccount, SIGNUP_REWARD_POINTS) ); - log.info("[SIGNUP_COMPLETE] User: {}, Account created and 100P issued", user.getEmail()); + log.info("[SIGNUP_COMPLETE] User: {}, Account created and {}P issued", user.getEmail(), SIGNUP_REWARD_POINTS); }backend/src/main/java/org/sejongisc/backend/point/repository/LedgerEntryRepository.java (1)
17-22: JOIN FETCH와 페이지네이션 조합 시 count 쿼리 주의
JOIN FETCH와Pageable을 함께 사용할 때, Spring Data가 자동 생성하는 count 쿼리에서 문제가 발생할 수 있습니다.@ManyToOne관계에서는 대부분 정상 동작하지만, 명시적인countQuery를 지정하면 더 안정적입니다.♻️ countQuery 명시 예시
`@Query`( + value = "SELECT le FROM LedgerEntry le " + "JOIN FETCH le.transaction " + "WHERE le.account.ownerId = :ownerId " + - "ORDER BY le.createdDate DESC") + "ORDER BY le.createdDate DESC", + countQuery = "SELECT COUNT(le) FROM LedgerEntry le WHERE le.account.ownerId = :ownerId") Page<LedgerEntry> findAllByOwnerId(`@Param`("ownerId") UUID ownerId, Pageable pageable);backend/src/main/java/org/sejongisc/backend/point/service/PointLedgerService.java (1)
36-43: 빈 entries 배열에 대한 검증이 누락되었습니다.
entries가 비어있는 경우 sum은 0이 되어 검증을 통과하지만, 의미 없는 트랜잭션이 생성됩니다. 빈 배열 검증을 추가하는 것이 좋습니다.♻️ 제안된 수정
`@Transactional` public void processTransaction(TransactionReason reason, UUID refId, AccountEntry... entries) { List<AccountEntry> entryList = Arrays.asList(entries); + if (entryList.isEmpty()) { + throw new CustomException(ErrorCode.INVALID_POINT_AMOUNT); + } + // 분개 항목의 amount의 합이 0인지 검증 long sum = entryList.stream().mapToLong(AccountEntry::amount).sum(); if (sum != 0) { throw new CustomException(ErrorCode.POINT_TRANSACTION_TOTAL_MISMATCH); }backend/src/main/java/org/sejongisc/backend/point/service/AccountService.java (1)
53-58: check-then-act 패턴에서 race condition 가능성이 있습니다.
existsByAccountName확인 후saveAccount호출 사이에 다른 스레드가 동일 계정을 생성할 수 있습니다. 애플리케이션 시작 시 한 번만 호출되는 경우라면 문제없지만, 그렇지 않다면 unique constraint 예외 처리 또는INSERT ... ON CONFLICT DO NOTHING패턴을 고려하세요.♻️ 예외 처리를 통한 멱등성 보장 제안
`@Transactional` public void initSystemAccount(AccountName name, AccountType type) { - if (!accountRepository.existsByAccountName(name)) { - saveAccount(null, name, type); - } + try { + if (!accountRepository.existsByAccountName(name)) { + saveAccount(null, name, type); + } + } catch (DataIntegrityViolationException e) { + log.debug("시스템 계정이 이미 존재합니다: name={}", name); + } }backend/src/main/java/org/sejongisc/backend/point/entity/LedgerEntry.java (2)
22-26: @joincolumn 명시를 권장합니다.현재 코드도 동작하지만, FK 컬럼명을 명시적으로 지정하면 스키마 가독성과 마이그레이션 관리가 용이해집니다.
♻️ 제안된 수정
`@ManyToOne`(fetch = FetchType.LAZY) + `@JoinColumn`(name = "transaction_id", nullable = false) private PointTransaction transaction; `@ManyToOne`(fetch = FetchType.LAZY) + `@JoinColumn`(name = "account_id", nullable = false) private Account account;
28-32: amount 필드에 대한 null 제약 조건을 고려하세요.원장 항목에서 금액이 null인 경우는 의미가 없으므로,
@Column(nullable = false)추가를 권장합니다.♻️ 제안된 수정
+ `@Column`(nullable = false) private Long amount;
backend/src/main/java/org/sejongisc/backend/betting/repository/BetRoundRepository.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/point/dto/PointHistoryItem.java
Show resolved
Hide resolved
backend/src/main/java/org/sejongisc/backend/point/entity/Account.java
Outdated
Show resolved
Hide resolved
backend/src/test/java/org/sejongisc/backend/point/controller/PointHistoryControllerTest.java
Show resolved
Hide resolved
backend/src/test/java/org/sejongisc/backend/point/service/PointHistoryServiceTest.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java (2)
113-144: OAuth 사용자에게 포인트 계정이 생성되지 않음
signUp메서드에는completeSignup이 호출되어 포인트 계정 생성과 가입 보상이 지급되지만,findOrCreateUser와upsertOAuthUser메서드에서는 이 로직이 누락되어 있습니다.OAuth로 가입하는 사용자는 포인트 계정이 없고 가입 보상도 받지 못하게 됩니다.
findAllUsersMissingAccount()메서드가 마이그레이션용으로 추가된 것 같지만, 향후 OAuth 가입자에게도 동일한 처리가 필요합니다.💡 제안: findOrCreateUser에 completeSignup 호출 추가
User savedUser = userRepository.save(newUser); + + // 포인트 계정 생성 및 기본 포인트 제공 + completeSignup(savedUser); String encryptedToken = tokenEncryptor.encrypt(oauthInfo.getAccessToken());
upsertOAuthUser메서드에도 동일하게 적용해 주세요.
60-110: OAuth 회원가입 메서드에서completeSignup()호출 누락
signUp()메서드는 포인트 계정 생성을 위해completeSignup()을 호출하지만,findOrCreateUser()와upsertOAuthUser()메서드는 이를 호출하지 않습니다. 이로 인해 OAuth로 생성된 사용자는 포인트 계정과 초기 포인트가 할당되지 않아 일관성 문제가 발생합니다. OAuth 메서드들에서도completeSignup()을 호출하도록 수정해주세요.
🧹 Nitpick comments (1)
backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java (1)
380-393: 하드코딩된 가입 보상 포인트 값
100L포인트가 직접 하드코딩되어 있습니다. 향후 보상 금액 변경 시 유지보수성을 위해 상수나 설정값으로 분리하는 것이 좋습니다.또한,
findAllUsersMissingAccount()가 마이그레이션용으로 추가된 것으로 보이는데,completeSignup이private이라 기존 사용자에 대한 계정 생성 및 보상 지급 시 재사용이 어렵습니다.♻️ 상수 분리 제안
+ private static final long SIGNUP_REWARD_POINTS = 100L; + /** * 사용자의 포인트 계정 생성 및 기본 포인트 지급 */ private void completeSignup(User user) { // 사용자의 포인트 계정 생성 Account userAccount = accountService.createUserAccount(user.getUserId()); // 회원가입 포인트 지급 pointLedgerService.processTransaction( TransactionReason.SIGNUP_REWARD, user.getUserId(), - AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), 100L), - AccountEntry.debit(userAccount, 100L) + AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), SIGNUP_REWARD_POINTS), + AccountEntry.debit(userAccount, SIGNUP_REWARD_POINTS) ); log.info("회원가입 완료: 회원가입 및 초기 포인트 지급이 완료되었습니다. User: {}", user.getEmail()); }
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@backend/src/main/java/org/sejongisc/backend/point/entity/Account.java`:
- Around line 39-41: Account.updateBalance(Long amount) is null-unsafe because
unboxing a null Long will NPE; change the signature to use primitive long
(updateBalance(long amount)) and adjust all call sites (e.g., where
PointLedgerService sums mapToLong(AccountEntry::amount).sum() and where
AccountEntry.amount() is used) so callers never pass null, or if you prefer
defensive checks keep Long but add a null guard inside updateBalance (treat null
as 0 or throw IllegalArgumentException) and ensure AccountEntry validation
prevents null amounts; update all references to match the chosen option.
🧹 Nitpick comments (3)
backend/src/main/java/org/sejongisc/backend/point/entity/Account.java (1)
22-22:ownerId필드에 대한 제약조건 확인이 필요합니다.
ownerId에@Column어노테이션이 없어 nullable로 설정됩니다. 시스템 계정(예: SYSTEM_ISSUANCE)은 소유자가 없을 수 있으므로 의도된 설계라면 문제없지만, 명시적으로 nullable 여부를 표현하면 코드 가독성이 향상됩니다.+ `@Column`(nullable = true) private UUID ownerId;backend/src/main/java/org/sejongisc/backend/betting/service/BettingService.java (2)
319-320: 정산 루프에서 사용자 계정 조회 N+1 가능성베팅 수가 많으면
accountService.getUserAccount호출이 과도해질 수 있습니다. 사용자 ID를 모아 배치 조회/캐시 후 사용을 고려해주세요.
366-371: 잔여금 이체 시 기존 로드 계정 재사용 권장이미
poolAccount,systemAccount를 가져왔으니 재조회 없이 재사용하면 쿼리 절감됩니다.♻️ 제안 변경
- AccountEntry.credit(accountService.getAccountByName(AccountName.BETTING_POOL), residual), - AccountEntry.debit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), residual) + AccountEntry.credit(poolAccount, residual), + AccountEntry.debit(systemAccount, residual)
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@backend/src/main/java/org/sejongisc/backend/point/entity/Account.java`:
- Around line 24-35: The ownerId field in the Account entity is currently
nullable at the DB level; annotate the ownerId field in the Account class with
`@Column`(nullable = false) (and optionally specify a columnDefinition if desired)
so the database enforces non-nullability consistent with
AccountService.saveAccount(), AccountRepository.findByOwnerIdAndType(), and the
other non-null fields (accountName, type, balance).
|
고생하셨습니다 ! 혹시 소셜 로그인(findOrCreateUser) 시에도 포인트 지갑이 생성 되도록 되어있을까용 ?? |
discipline24
left a comment
There was a problem hiding this comment.
오 고생하셨습니다~ 조금 복잡한 것 같은데 이번 회의때 팀원들에게 간단히 설명해주시면 좋을 것 같아요!
835a3b1
|
@cksdid202 오 그 부분을 놓쳤네용 감사합니다!! |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java (1)
337-365:upsertOAuthUser에서completeSignup호출 누락 - 포인트 계정 미생성이 메서드도 새로운 사용자를 생성하지만
completeSignup을 호출하지 않습니다.signUp과findOrCreateUser는 모두completeSignup을 호출하여 포인트 계정을 생성하는데, 이 메서드를 통해 생성된 사용자는 포인트 계정이 없고 가입 보상 포인트도 받지 못합니다.PR 목표인 "모든 회원가입 시 포인트 지갑 생성"과 일관성이 맞지 않습니다.
🐛 수정 제안
User newUser = User.builder() .email(email) .name(name) .role(Role.TEAM_MEMBER) .build(); User savedUser = userRepository.save(newUser); + completeSignup(savedUser); + UserOauthAccount oauthAccount = UserOauthAccount.builder() .user(savedUser) .provider(authProvider) .providerUid(providerUid) .build(); oauthAccountRepository.save(oauthAccount); return savedUser;
🧹 Nitpick comments (1)
backend/src/main/java/org/sejongisc/backend/user/service/UserServiceImpl.java (1)
383-396: 회원가입 보상 포인트 매직 넘버 및 PII 로깅 고려사항
매직 넘버: 회원가입 보상 포인트
100L이 하드코딩되어 있습니다. 설정값이나 상수로 추출하면 유지보수성이 향상됩니다.PII 로깅: INFO 레벨에서
user.getEmail()을 로깅하고 있습니다. OAuth 사용자의 경우 이메일이 null일 수 있고, 개인정보 보호 관점에서 userId 등 비식별 정보로 대체하는 것을 고려해주세요.♻️ 개선 제안
+ private static final long SIGNUP_REWARD_POINTS = 100L; + /** * 사용자의 포인트 계정 생성 및 기본 포인트 지급 */ private void completeSignup(User user) { // 사용자의 포인트 계정 생성 Account userAccount = accountService.createUserAccount(user.getUserId()); // 회원가입 포인트 지급 pointLedgerService.processTransaction( TransactionReason.SIGNUP_REWARD, user.getUserId(), - AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), 100L), - AccountEntry.debit(userAccount, 100L) + AccountEntry.credit(accountService.getAccountByName(AccountName.SYSTEM_ISSUANCE), SIGNUP_REWARD_POINTS), + AccountEntry.debit(userAccount, SIGNUP_REWARD_POINTS) ); - log.info("회원가입 완료: 회원가입 및 초기 포인트 지급이 완료되었습니다. User: {}", user.getEmail()); + log.info("회원가입 완료: 회원가입 및 초기 포인트 지급이 완료되었습니다. userId: {}", user.getUserId()); }
Summary by CodeRabbit
새로운 기능
개선 사항
테스트
✏️ Tip: You can customize this high-level summary in your review settings.