Skip to content

[BE] [FEAT] 복식부기 기반 포인트 원장 시스템 도입#200

Merged
Yooonjeong merged 30 commits intomainfrom
20260122_#191_복식부기_기반_포인트_원장_시스템_도입
Jan 28, 2026

Hidden character warning

The head ref may contain hidden characters: "20260122_#191_\ubcf5\uc2dd\ubd80\uae30_\uae30\ubc18_\ud3ec\uc778\ud2b8_\uc6d0\uc7a5_\uc2dc\uc2a4\ud15c_\ub3c4\uc785"
Merged

[BE] [FEAT] 복식부기 기반 포인트 원장 시스템 도입#200
Yooonjeong merged 30 commits intomainfrom
20260122_#191_복식부기_기반_포인트_원장_시스템_도입

Conversation

@Yooonjeong
Copy link
Contributor

@Yooonjeong Yooonjeong commented Jan 26, 2026

Summary by CodeRabbit

  • 새로운 기능

    • 원장(ledger) 기반 포인트 시스템 도입 및 시스템/풀/사용자 계정 추가
    • 가입 시 자동 100포인트 지급 자동화
  • 개선 사항

    • 베팅 흐름이 원장 기반으로 전환되어 포인트 차감·환급·보상·잔여 정산 명확화
    • 포인트 이력 응답 형식 간소화 및 조회 개선
    • 운영 로그(가시성) 강화 및 오류 코드 추가로 문제 식별 개선
  • 테스트

    • 일부 포인트 관련 테스트가 주석 처리되어 비활성화됨

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

Walkthrough

포인트 시스템을 단순 이력에서 복식부기 원장 기반으로 전환했습니다. Account/PointTransaction/LedgerEntry 엔티티·리포지토리·서비스와 초기화·마이그레이션 로직이 추가되었고, BettingService·회원가입 흐름이 원장 기반 트랜잭션으로 전환되었으며 일부 테스트가 비활성화되었습니다.

Changes

Cohort / File(s) 변경 요약
포인트 엔티티 계층
backend/src/main/java/org/sejongisc/backend/point/entity/*
\Account.java`, `AccountName.java`, `AccountType.java`, `EntryType.java`, `LedgerEntry.java`, `PointTransaction.java`, `TransactionReason.java``
복식부기 모델 추가: Account(잔액 갱신 메서드 포함), PointTransaction, LedgerEntry 및 관련 enum 추가.
포인트 DTO 및 응답
backend/src/main/java/org/sejongisc/backend/point/dto/*
\AccountEntry.java`, `PointHistoryItem.java`, `PointHistoryResponse.java``
AccountEntry 레코드(유효성 검사 포함)와 PointHistoryItem 추가. PointHistoryResponse를 Page을 갖는 레코드로 변경(leaderboard 제거).
포인트 저장소 계층
backend/src/main/java/org/sejongisc/backend/point/repository/*
\AccountRepository.java`, `LedgerEntryRepository.java`, `TransactionalRepository.java``
Account, LedgerEntry, PointTransaction용 JPA 리포지토리 추가(조회·페이징 메서드 포함).
포인트 서비스 계층
backend/src/main/java/org/sejongisc/backend/point/service/*
\AccountService.java`, `PointLedgerService.java`, `PointHistoryService.java`, `PointDataInitializer.java`"`
Account 관리, 트랜잭션 처리(합계 검증·원장 엔트리 생성), 읽기 전용 이력 조회, 초기화·마이그레이션 로직 추가.
베팅 계층
backend/src/main/java/org/sejongisc/backend/betting/*
\BetRoundRepository.java`, `BettingService.java`"`
BetRoundRepository의 원자적 업데이트 어노테이션·포맷 변경. BettingService가 PointLedgerService·AccountService로 포인트 흐름을 전환하고 로깅 추가.
사용자 계층
backend/src/main/java/org/sejongisc/backend/user/*
\UserRepository.java`, `UserService.java`, `UserServiceImpl.java`"`
계정이 없는 사용자 조회 메서드 추가. 회원가입 흐름에 포인트 계정 생성과 SIGNUP_REWARD 트랜잭션 통합(completeSignup).
예외 정의
backend/src/main/java/org/sejongisc/backend/common/exception/ErrorCode.java
ACCOUNT_NOT_FOUND, ACCOUNT_REQUIRED, POINT_TRANSACTION_TOTAL_MISMATCH 에러 코드 추가.
테스트
backend/src/test/java/org/sejongisc/backend/point/*
\PointHistoryControllerTest.java`, `PointHistoryServiceTest.java`"`
해당 테스트 파일들의 테스트 코드가 주석 처리되어 비활성화됨.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75분

Possibly related PRs

Suggested reviewers

  • cksdid202
  • discipline24

Poem

🐇 새 계정엔 당근 한 움큼, 원장은 두 줄로 맞춰요.
왼쪽엔 출금, 오른쪽엔 입금, 숫자들이 서로 인사해요.
가입엔 보상 주고, 베팅엔 스테이크 적어요.
토끼가 깡충—원장에 기록 남기니 마음이 놓여요. 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 복식부기 기반 포인트 원장 시스템 도입이라는 주요 변경사항을 명확하게 설명하고 있으며, 실제 변경사항(Account, LedgerEntry, PointTransaction 등 새로운 엔티티 추가 및 포인트 관리 로직 전환)과 완전히 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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: 페이지네이션 파라미터 유효성 검증 부재

pageNumberpageSize에 대한 유효성 검증이 없습니다. 음수 값이나 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 FETCHPageable을 함께 사용할 때, 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;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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이 호출되어 포인트 계정 생성과 가입 보상이 지급되지만, findOrCreateUserupsertOAuthUser 메서드에서는 이 로직이 누락되어 있습니다.

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()가 마이그레이션용으로 추가된 것으로 보이는데, completeSignupprivate이라 기존 사용자에 대한 계정 생성 및 보상 지급 시 재사용이 어렵습니다.

♻️ 상수 분리 제안
+    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());
     }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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).

@cksdid202
Copy link
Contributor

고생하셨습니다 ! 혹시 소셜 로그인(findOrCreateUser) 시에도 포인트 지갑이 생성 되도록 되어있을까용 ??

cksdid202
cksdid202 previously approved these changes Jan 28, 2026
discipline24
discipline24 previously approved these changes Jan 28, 2026
Copy link
Contributor

@discipline24 discipline24 left a comment

Choose a reason for hiding this comment

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

오 고생하셨습니다~ 조금 복잡한 것 같은데 이번 회의때 팀원들에게 간단히 설명해주시면 좋을 것 같아요!

@Yooonjeong Yooonjeong dismissed stale reviews from discipline24 and cksdid202 via 835a3b1 January 28, 2026 14:23
@Yooonjeong
Copy link
Contributor Author

@cksdid202 오 그 부분을 놓쳤네용 감사합니다!!
@discipline24 넹 ~ 수정사항이 있어서 approve 한 번만 더 부탁드려여

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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을 호출하지 않습니다. signUpfindOrCreateUser는 모두 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 로깅 고려사항

  1. 매직 넘버: 회원가입 보상 포인트 100L이 하드코딩되어 있습니다. 설정값이나 상수로 추출하면 유지보수성이 향상됩니다.

  2. 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());
     }

@Yooonjeong Yooonjeong merged commit 6ce9ba5 into main Jan 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[기능추가][BE] 복식부기 기반 포인트 원장 시스템 도입

3 participants