[BE/FE] feat: 장부 및 추가 회비 관련 오류 수정#119
Conversation
📝 WalkthroughWalkthrough은행 거래 동기화가 잔액 인식 기반으로 재구성되고, 정산/결제 요청·참석·투표 관련 API와 프런트엔드 UI·타입이 확장되며 매칭·저장소 쿼리·예외 처리·테스트·마이그레이션 데이터가 함께 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant BankAccountController as "BankAccountController"
participant BankService as "BankService"
participant BankHistoryRepo as "BankTransactionHistoryRepo"
participant TransactionLogRepo as "TransactionLogRepo"
participant PaymentRequestService as "PaymentRequestService"
Client->>BankAccountController: GET /bank/transactions/unmatched?sync=true
BankAccountController->>BankService: requestUnmatched(clubId, recalc, sync)
alt sync == true
BankService->>BankHistoryRepo: fetch external bank txs / findPreviousHistory(...)
BankHistoryRepo-->>BankService: bank histories
BankService->>TransactionLogRepo: findFirstByClubIdAndBankHistoryIdIsNullAndCreatedAtBefore(...)
TransactionLogRepo-->>BankService: latestManualLog
BankService->>BankHistoryRepo: save/update BankTransactionHistory
BankService->>TransactionLogRepo: create/update TransactionLog (updateBalanceAfter)
BankService->>PaymentRequestService: schedule autoMatchTransactions (afterCommit)
end
BankService-->>BankAccountController: UnmatchedTransactionsResponse
BankAccountController-->>Client: 200 / 404 / 500
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
frontend/src/app/components/group/GroupLayout.tsx (1)
60-60:location이 정의되지 않아 런타임 에러가 발생합니다.
useLocationimport와const location = useLocation()선언이 제거되었지만, dependency 배열에서 여전히location.pathname을 참조하고 있습니다. 이로 인해TypeError: Cannot read properties of undefined (reading 'pathname')런타임 에러가 발생합니다.만약 pathname 변경 시 데이터를 다시 fetch해야 한다면
useLocation을 다시 추가하고, 그렇지 않다면 dependency에서 제거해야 합니다.🐛 제안된 수정 방안
방안 1: pathname 변경 감지가 필요 없는 경우 - dependency에서 제거:
- }, [groupId, location.pathname]); + }, [groupId]);방안 2: pathname 변경 감지가 필요한 경우 - useLocation 복원:
Line 1의 import 수정:
-import { Outlet, NavLink, useParams, Link, useNavigate } from 'react-router-dom'; +import { Outlet, NavLink, useParams, Link, useNavigate, useLocation } from 'react-router-dom';Line 12 이후에 location 선언 추가:
const navigate = useNavigate(); + const location = useLocation();backend/src/main/resources/db/migration/schedule_ledger_test_data.sql (3)
16-31: 테스트 데이터가 프로덕션 마이그레이션 경로에 있습니다.이 파일은
src/main/resources/db/migration/에 위치해 있어 프로덕션 환경에서도 실행될 수 있습니다.StubBankProvider와 같은 테스트용 데이터가 포함되어 있으므로, 테스트 전용 경로(src/test/resources/db/migration/)로 이동하거나 프로파일 기반 조건부 실행을 고려해 주세요.또한, Flyway 버전 프리픽스(예:
V1__schedule_ledger_test_data.sql)가 없어 마이그레이션으로 인식되지 않을 수 있습니다.
98-110:bank_id=10이 존재하지 않을 수 있습니다.
bank_accounts테이블에서 모든 계좌가bank_id=10을 참조하지만,banks테이블 INSERT에서는bank_id를 명시적으로 설정하지 않습니다.bank_id가 auto-increment인 경우, 'STUB' 은행이 ID 10을 받는다는 보장이 없습니다.♻️ 제안된 수정: bank_id를 명시적으로 설정하거나 서브쿼리 사용
옵션 1: banks INSERT 시 bank_id 명시
INSERT INTO banks (bank_id, bank_code, bank_name, provider_class_name, is_active) VALUES (1, '004', '국민은행', ...), ... (10, 'STUB','오픈은행', ...);옵션 2: bank_accounts에서 서브쿼리 사용
INSERT INTO bank_accounts (club_id, bank_code, user_id, bank_id, account_number, depositor_name, ...) VALUES (1, 'STUB', 27, (SELECT bank_id FROM banks WHERE bank_code = 'STUB'), '110-42-1000001', '두효정', ...), ...
66-67: BANNED 사용자의 상태값이 일관되지 않습니다.정지된 사용자(24, 25)는
banned_at값이 설정되어 있지만,status가'ACTIVE'로 되어 있습니다. Users 엔티티의ban()메서드는 상태를'BANNED'으로 설정하므로, 테스트 데이터에서도status='BANNED'으로 수정되어야 합니다. 탈퇴한 사용자(22-23)가status='DELETED'로 설정된 것과 일관성 있게 처리되어야 합니다.frontend/src/app/data/userRoles.ts (1)
33-44: 총무(treasurer) 역할 설명이 새 권한을 반영하지 않습니다.
canManageGroup,canManageMembers,canDeletePosts,canDeleteComments권한이 추가되었지만, Line 160의ROLE_DESCRIPTIONS는 여전히 "회비 관리, 지분 확인, 출금이 가능합니다"라고만 표시됩니다. 사용자에게 일관된 정보를 제공하려면 설명을 업데이트해야 합니다.📝 제안된 수정
export const ROLE_DESCRIPTIONS: Record<UserRole, string> = { owner: '모든 권한을 가진 모임 관리자입니다.', - treasurer: '회비 관리, 지분 확인, 출금이 가능합니다.', + treasurer: '회비 관리, 지분 확인, 출금, 멤버 관리, 게시글/댓글 삭제가 가능합니다.', manager: '멤버 관리, 게시글/댓글 삭제가 가능합니다.', member: '일반적인 모임 활동이 가능합니다.', pending: '가입 승인 대기 중입니다.', };backend/src/test/java/back/service/ledger/EventFundServiceTest.java (1)
194-195: 테스트에서 호출하는 메서드명을 실제 리포지토리 시그니처와 일치시켜야 합니다.
TransactionLogRepository에 존재하는 메서드는findByClubIdAndCreatedAtBetween()이 아니라findByClubIdAndCreatedAtBetweenOrderByCreatedAtDescTransactionIdDesc()입니다. 194-195번 줄과 251-252번 줄의 스텁을 다음과 같이 수정하세요:given(transactionLogRepository.findByClubIdAndCreatedAtBetweenOrderByCreatedAtDescTransactionIdDesc(any(), any(), any())) .willReturn(List.of(log1));
🤖 Fix all issues with AI agents
In `@backend/src/main/java/back/bank/controller/BankAccountController.java`:
- Around line 60-75: The getAccount method in BankAccountController currently
returns e.getMessage() in the 500 response and prints the stack trace; remove
the sensitive exception message from the response and replace the
System.err.println/e.printStackTrace calls with a proper logger call that logs
the full exception (e.g., logger.error("Failed to get account for clubId {}",
clubId, e)); change the ResponseEntity.internalServerError().body(...) to return
a fixed, non-sensitive message like "계좌 조회 중 오류가 발생했습니다." while keeping detailed
exception info only in the server logs; ensure you reference getAccount and
BankAccountResponseDTO.from(bankAccount) to find the correct method and response
handling.
- Around line 249-267: The current getUnmatchedTransactions method is annotated
with `@Transactional` and calls bankService.syncTransactionsStub(...) which
performs external API calls (provider.getTransactionsStub()) and can hold DB
connections/locks; move the sync out of the transactional scope by either (A)
performing the sync before entering the `@Transactional` method (call
syncTransactionsStub(...) from a non-transactional wrapper or from the
controller before starting the transaction) or (B) change the sync method’s
transactional behavior to not participate in the transaction (annotate the
service method syncTransactionsStub or a new sync wrapper with
`@Transactional`(propagation = Propagation.NOT_SUPPORTED) or
`@Transactional`(propagation = Propagation.REQUIRES_NEW) so the external call runs
outside the caller’s transaction), ensuring only the DB update logic remains
inside the transaction; reference getUnmatchedTransactions and
bankService.syncTransactionsStub when you relocate or re-annotate the call.
- Around line 290-308: The System.out.println debug lines in
BankAccountController that iterate availableRequests and allRequests expose PII
(memberName, expectedAmount, expectedDate, createdAt); replace these
System.out.println calls with SLF4J logger.debug and remove or mask PII
fields—log only non-PII identifiers/status (e.g., requestId, status, size) or a
hashed/id-only representation for PaymentRequest entries (variables
availableRequests, allRequests, and the loop over PaymentRequest req). Ensure
you use the existing logger instance and debug level so these details aren't
emitted to stdout in production.
In `@backend/src/main/java/back/bank/service/BankService.java`:
- Around line 289-305: The syncTransactions path incorrectly uses tx.amount()
as-is when computing runningBalance (see syncTransactions and variables
runningBalance, tx.amount(), TransactionLog.updateBalanceAfter,
transactionLogRepository.save), so adjust the amount sign the same way as
syncTransactionsStub: derive a signedAmount by checking tx.type (DEPOSIT vs
WITHDRAW) and use runningBalance = runningBalance.add(signedAmount) and
compare/update log.getBalanceAfter() against the computed balance; ensure all
places in syncTransactions that add tx.amount() (the calculation and update
points) use the normalized signedAmount instead.
In `@backend/src/main/java/back/dto/ledger/request/AdditionalFeeRequest.java`:
- Around line 5-8: Annotate the record component amountPerPerson in
AdditionalFeeRequest with Jakarta Validation annotations to forbid null, zero,
or negative values (e.g., add `@NotNull` and `@DecimalMin`(value = "0.01") or
`@Positive`) and add the corresponding imports from jakarta.validation.constraints
so the validator can enforce the constraint at binding time; apply the
annotations directly to the component declaration in the AdditionalFeeRequest
record and ensure imports are updated accordingly.
In `@backend/src/main/java/back/exception/GlobalExceptionHandler.java`:
- Around line 82-93: Update
GlobalExceptionHandler.handleHttpRequestMethodNotSupportedException to (1)
change the log call to use log.warn not log.error, (2) fix the placeholder
mismatch by injecting javax.servlet.http.HttpServletRequest as an extra
parameter and log request.getRequestURI() in place of the incorrect
e.getMessage(), and (3) return
ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) (405) instead of
ErrorCode.INVALID_INPUT’s HTTP status so the handler returns 405
METHOD_NOT_ALLOWED.
In `@backend/src/main/java/back/service/ledger/EventFundService.java`:
- Around line 428-433: The refund update is calling
participantRepository.findByScheduleIdAndUserId(...) with
originalReq.getMemberId(), but that method expects a userId, so the participant
refund state never updates; fix by supplying the correct userId (e.g., use
originalReq.getUserId() or resolve memberId→userId before the call) in
EventFundService where participantRepository.findByScheduleIdAndUserId(...) is
invoked, then keep the existing participant.markRefunded() and
participantRepository.save(participant) logic.
- Around line 437-448: The code builds paidMemberIds from
PaymentRequest::getMemberId but compares it to ScheduleParticipants.getUserId,
causing everyone to be marked notAttend; fix by using the same identifier on
both sides: either change the paidMemberIds set to collect
PaymentRequest::getUserId (if PaymentRequest exposes getUserId) or change the
comparison to use participant.getMemberId() (if ScheduleParticipants has
getMemberId), then run the existing loop (the participant.notAttend() and
participantRepository.save(participant) calls) unchanged so only true non-payers
are marked absent.
In `@backend/src/main/java/back/service/ledger/LedgerService.java`:
- Around line 52-66: The filtering loop is causing N+1 DB calls because
getActualTransactionDate(log) hits the database per log; modify the logic to
reuse the existing prefetch map (the map created during your batch prefetch) to
compute actualTransactionDate for each TransactionLog instead of calling
getActualTransactionDate, e.g. look up related entities/fields from the
prefetched map and derive the date, then perform the same date range check and
add to filteredLogs; apply the same refactor to the other filtering block that
currently uses getActualTransactionDate (the one around the 156-165 region) so
both use the prefetch map and avoid per-log DB queries.
- Around line 134-136: The response string is appending debug info ("[Act:... /
ID:...]") to the user-facing description (constructed via log.getDescription()
and log.getTransactionId()), which should be removed; instead, stop
concatenating logIdToDateMap.get(log.getTransactionId()).toString() and the
transaction ID into the returned description and move that information into a
logger.debug (or logger.info with a debug flag) call inside the same method in
LedgerService.java so the details are only emitted to logs when debug is enabled
(use logger.isDebugEnabled() or an application debug flag to guard the log).
In `@backend/src/main/java/back/service/ledger/PaymentRequestService.java`:
- Around line 55-71: Replace the System.out.println calls in
PaymentRequestService with SLF4J logging: add a private static final Logger
(LoggerFactory.getLogger(PaymentRequestService.class)) and use logger.info/debug
with parameterized messages instead of println for the creation summary,
per-request details, the call to
transactionMatchingService.matchRequestsWithExistingTransactions(clubId,
createdRequests), and the post-match checks that call
paymentRequestRepository.findById(...). Use appropriate log levels (info for
summaries, debug for per-request details), include structured context via
placeholders (e.g., clubId, requestId, status, expectedAmount,
matchedHistoryId), and add the necessary imports (org.slf4j.Logger and
org.slf4j.LoggerFactory).
In `@backend/src/main/java/back/service/ledger/TransactionMatchingService.java`:
- Around line 196-197: Replace the direct System.out.println calls that print
realName, nick, and content in TransactionMatchingService with
non-production-safe logging: remove System.out.println and use the class logger
(e.g., log.trace) and mask sensitive fields via a maskName(masking) helper for
realName and nick, and either drop or mask the transaction content (content)
before logging; reference the existing symbols System.out.println, realName,
nick, content, maskName, and log.trace when making the change so the debug info
is only trace-level and personally identifiable data is masked.
- Around line 156-172: The timezone handling is inconsistent: isMatched()
converts tx.getBankTransactionAt() using ZoneId.of("Asia/Seoul") but
determineFailureReason() still calls tx.getBankTransactionAt().toLocalDate();
update determineFailureReason() to use the same Asia/Seoul conversion or extract
a helper (e.g., toLocalDateInKorea(Instant/OffsetDateTime)) and call it from
both isMatched() and determineFailureReason() so both use the same
ZoneId.of("Asia/Seoul") logic when deriving dates from
tx.getBankTransactionAt().
In `@backend/src/main/java/back/service/vote/VoteService.java`:
- Around line 350-353: The call to syncTransactionsStub in VoteService is using
a hardcoded userId (1L); remove the magic constant and pass the real caller or a
dedicated system account instead. Update the call sites (including the
invocation inside createPaymentRequestsFromVoteResults and the other occurrence
around lines 831-834) to accept a userId parameter or read an injected/system
account (e.g., from an AuthenticationContext or a configured SystemUserProvider)
and propagate that userId into syncTransactionsStub, adjusting method signatures
for createPaymentRequestsFromVoteResults and any calling methods in VoteService
accordingly so the real user or explicit system account is used instead of 1L.
- Around line 333-360: Replace all System.out/err prints in VoteService with the
class SLF4J logger (use org.slf4j.LoggerFactory.getLogger(VoteService.class) / a
private static final Logger field) and standard log levels (info/warn/error) so
logging is consistent with existing calls; specifically convert the prints
around the ATTENDANCE block (the System.out lines that announce vote closing,
schedule/entryFee info, and the System.err stack traces) and any other
occurrences to logger.info/warn/error. Remove or redact PII in those log
messages: never log full userId, clubId, memberId, or realName—mask them (e.g.,
partial hash or last 4 chars) or omit entirely; ensure methods referenced like
updateParticipantsFromVoteResults(...) and
createPaymentRequestsFromVoteResults(...) do not emit raw PII either. Keep
exception logging using logger.warn/error with the exception object (not
e.getMessage() concatenation) to preserve stack traces without embedding
sensitive identifiers.
In `@backend/src/main/resources/db/migration/integration_test_data.sql`:
- Around line 12-16: The cleanup SQL is missing DELETEs for several child tables
and omits user_id=1, causing duplicate-key and FK issues; in
integration_test_data.sql add DELETE statements for schedules, posts, comments,
votes, vote_options, vote_records, reports, transaction_log, payment_requests,
schedule_participants, post_likes, comment_likes (and any
like/participant/report/vote child tables) before deleting
clubs/bank_accounts/club_members/users, and include user_id=1 in the users
DELETE (or remove the INSERT for user_id=1) so the script can be re-run safely;
ensure the deletes run in FK-safe order (children first, then parents) to avoid
constraint errors.
In `@frontend/src/app/components/group/dues/LedgerView.tsx`:
- Around line 71-73: The transactions ordering is inconsistent because
LedgerView sometimes calls setTransactions([...data].reverse()) but other flows
call setTransactions(data) directly; centralize the sorting by adding a single
helper (e.g., normalizeTransactions or sortTransactions) used everywhere in
LedgerView and related handlers so all incoming arrays are copied and reversed
consistently before calling setTransactions; update all places that set
transactions (initial load, add/edit/match callbacks, refresh handlers) to pass
data through that helper instead of calling setTransactions(data) directly.
In `@frontend/src/app/components/group/schedule/ScheduleDetailView.tsx`:
- Around line 571-579: The finalize flow currently ignores the selected refund
recipients because finalizeParticipantIds is only used for UI math and not
passed to the backend; update the handleFinalize flow (the function
handleFinalize invoking finalizeSchedule/finalizeSchedule) to include the
selected IDs in the request payload (e.g., send finalizeParticipantIds or
similarly named array alongside totalSpent) so the backend receives the refund
targets, or if the UI should not control refunds remove the selection UI and
related finalizeParticipantIds state; locate handleFinalize and the
finalizeSchedule/finalizeSchedule call sites (also review the equivalent logic
around lines referenced ~1774-1790) and ensure the request body includes the
participant ID list when calling the finalize API.
🧹 Nitpick comments (8)
frontend/src/app/components/group/GroupLayout.tsx (1)
88-94: 프로필 네비게이션 버튼 구현은 적절합니다.
aria-label이 "프로필"로 업데이트되어 접근성 측면에서 일관성이 있습니다. 다만,Menu아이콘이 프로필 네비게이션에 사용되고 있어 사용자에게 혼란을 줄 수 있습니다.💡 아이콘 변경 제안 (선택 사항)
프로필 네비게이션임을 명확히 하기 위해
User또는UserCircle아이콘 사용을 고려해 보세요:-import { ArrowLeft, Menu } from 'lucide-react'; +import { ArrowLeft, User } from 'lucide-react';- <Menu className="w-6 h-6 text-stone-800" /> + <User className="w-6 h-6 text-stone-800" />backend/src/main/java/back/service/vote/VoteAutoCloseService.java (1)
55-59: 인라인 주석이 명확합니다.56번 라인의 주석은 JavaDoc 문서와 일관되게 동작을 설명하고 있어 좋습니다.
선택적 개선 제안: 배치 저장 고려
현재 각 투표를 개별적으로 저장하고 있습니다. 일반적인 상황에서는 문제없지만, 만료된 투표가 많을 경우 성능 최적화를 위해 배치 저장을 고려할 수 있습니다.
♻️ 배치 저장 예시
if (!expiredVotes.isEmpty()) { log.info("일정 시작 5분 전이 지난 일정 투표 {}개를 자동 종료합니다", expiredVotes.size()); expiredVotes.forEach(vote -> { // 투표만 마감하고 일정은 OPEN 상태로 유지 vote.close(); - voteRepository.save(vote); }); + voteRepository.saveAll(expiredVotes); }backend/src/main/java/back/exception/ScheduleException.java (1)
45-61: 메시지 중복 및 기존 패턴과의 불일치새로 추가된 예외 클래스들이
ErrorCode에 이미 정의된 메시지를 생성자에서 다시 전달하고 있습니다. 이는 메시지 중복을 야기하며, 기존의NotFound,InvalidDateRange등의 클래스들이 사용하는 단일 인자 생성자 패턴과 일치하지 않습니다.메시지가 변경될 경우 두 곳을 모두 수정해야 하는 유지보수 부담이 생깁니다.
♻️ 기존 패턴과 일관성 있게 수정
public static class NotOpen extends ScheduleException { public NotOpen() { - super(ErrorCode.SCHEDULE_NOT_OPEN, "일정이 진행 중이 아닙니다"); + super(ErrorCode.SCHEDULE_NOT_OPEN); } } public static class NotStarted extends ScheduleException { public NotStarted() { - super(ErrorCode.SCHEDULE_NOT_STARTED, "일정이 아직 시작되지 않았습니다"); + super(ErrorCode.SCHEDULE_NOT_STARTED); } } public static class NoAttendees extends ScheduleException { public NoAttendees() { - super(ErrorCode.SCHEDULE_NO_ATTENDEES, "참석자가 없습니다"); + super(ErrorCode.SCHEDULE_NO_ATTENDEES); } }backend/src/main/resources/db/migration/schedule_ledger_test_data.sql (1)
7-11: 삭제 범위가 삽입 데이터와 일치하지 않을 수 있습니다.
club_members테이블에서club_id IN (1, 2, 3, 8)만 삭제하지만, clubs 테이블에서는 1-10까지 삭제합니다. 다른 마이그레이션이나 수동 작업으로 clubs 4-7, 9, 10에 멤버가 추가된 경우, FK 제약 조건 위반이 발생할 수 있습니다.♻️ 제안된 수정
-- 기존 테스트 데이터 삭제 (중복 방지) -DELETE FROM club_members WHERE club_id IN (1, 2, 3, 8); +DELETE FROM club_members WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); DELETE FROM bank_accounts WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); DELETE FROM clubs WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);backend/src/main/java/back/service/ledger/TransactionMatchingService.java (1)
132-132: 프로덕션 코드에서 System.out.println 사용 지양디버그 로깅에
System.out.println을 사용하고 있습니다. 이는 로그 레벨 제어, 로그 포맷팅, 로그 수집이 불가능하며 프로덕션 환경에서 성능 저하를 유발할 수 있습니다. SLF4J 같은 로깅 프레임워크를 사용하세요.♻️ SLF4J 로거 사용 제안
+import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + `@Service` public class TransactionMatchingService { + private static final Logger log = LoggerFactory.getLogger(TransactionMatchingService.class);그리고 로그 호출을:
- System.out.println("=== [isMatched 호출] 거래내역: " + tx.getPrintContent() + " ..."); + log.debug("=== [isMatched 호출] 거래내역: {} ({}원), 요청: {} ({}원) ===", + tx.getPrintContent(), tx.getAmount(), req.getMemberName(), req.getExpectedAmount());backend/src/main/java/back/bank/repository/BankTransactionHistoryRepository.java (1)
22-33: 쿼리 성능을 위해 복합 인덱스를 고려해주세요.
clubId + bankTransactionAt + historyId기준으로 정렬/필터링이 반복되므로 해당 컬럼 조합 인덱스가 있으면 동기화/잔액 계산 성능이 안정됩니다.backend/src/main/java/back/service/ledger/LedgerService.java (1)
38-44: 서비스 로깅은 Logger로 통일하는 것을 권장합니다.
System.out.println/System.err.println은 운영 환경에서 제어가 어렵습니다.Slf4j등으로 레벨 기반 로그로 전환해 주세요.backend/src/main/java/back/controller/ledger/LedgerController.java (1)
40-55: 동기화 호출로 인한 응답 지연 리스크를 고려해주세요.
요청 스레드에서 외부 동기화를 수행하므로 지연/타임아웃 위험이 큽니다. 비동기 처리나 타임아웃/서킷브레이커 적용을 검토하고, 로그는Logger로 전환하는 편이 안전합니다.
| @org.springframework.transaction.annotation.Transactional(readOnly = true) | ||
| public ResponseEntity<?> getAccount(@PathVariable Long clubId) { | ||
| try { | ||
| BankAccounts account = bankAccountRepository.findByClubId(clubId) | ||
| .orElse(null); | ||
|
|
||
| if (account == null) { | ||
| return ResponseEntity.status(404).body("모임 계좌가 존재하지 않습니다."); | ||
| } | ||
|
|
||
| return ResponseEntity.ok(BankAccountResponseDTO.from(account)); | ||
| } catch (Exception e) { | ||
| System.err.println("Failed to get account: " + e.getMessage()); | ||
| e.printStackTrace(); | ||
| return ResponseEntity.internalServerError().body("계좌 조회 실패: " + e.getMessage()); | ||
| } |
There was a problem hiding this comment.
예외 메시지를 응답에 그대로 노출하지 마세요.
500 응답 바디에 e.getMessage()를 포함하면 내부 오류 상세가 외부로 노출될 수 있습니다. 고정 메시지로 반환하고 상세는 서버 로그에만 남기는 편이 안전합니다.
🐛 수정 제안
- return ResponseEntity.internalServerError().body("계좌 조회 실패: " + e.getMessage());
+ return ResponseEntity.internalServerError().body("계좌 조회 실패");🤖 Prompt for AI Agents
In `@backend/src/main/java/back/bank/controller/BankAccountController.java` around
lines 60 - 75, The getAccount method in BankAccountController currently returns
e.getMessage() in the 500 response and prints the stack trace; remove the
sensitive exception message from the response and replace the
System.err.println/e.printStackTrace calls with a proper logger call that logs
the full exception (e.g., logger.error("Failed to get account for clubId {}",
clubId, e)); change the ResponseEntity.internalServerError().body(...) to return
a fixed, non-sensitive message like "계좌 조회 중 오류가 발생했습니다." while keeping detailed
exception info only in the server logs; ensure you reference getAccount and
BankAccountResponseDTO.from(bankAccount) to find the correct method and response
handling.
| * GET /clubs/{clubId}/bank/transactions/unmatched?recalculate=true&sync=false | ||
| */ | ||
| @GetMapping("/transactions/unmatched") | ||
| @org.springframework.transaction.annotation.Transactional | ||
| public ResponseEntity<UnmatchedTransactionsResponse> getUnmatchedTransactions( | ||
| @PathVariable Long clubId, | ||
| @RequestParam(required = false, defaultValue = "true") boolean recalculate) { | ||
| // 조회 전 은행 동기화 (최신 거래내역 반영) | ||
| try { | ||
| bankService.syncTransactionsStub(clubId, 1L, null, null); // 입금 내역 | ||
| bankService.syncTransactionsStub(clubId, 2L, null, null); // 출금 내역 | ||
| } catch (Exception e) { | ||
| // 동기화 실패 시 로깅만 하고 계속 진행 | ||
| System.err.println("Bank sync failed during unmatched query: " + e.getMessage()); | ||
| @RequestParam(required = false, defaultValue = "true") boolean recalculate, | ||
| @RequestParam(required = false, defaultValue = "false") boolean sync) { | ||
| // 조회 전 은행 동기화 (최신 거래내역 반영) - sync=true인 경우만 실행 | ||
| // 동기화는 자동 매칭을 수행하므로, 거래 내역이 사라지는 것을 방지하기 위해 기본값은 false | ||
| if (sync) { | ||
| try { | ||
| bankService.syncTransactionsStub(clubId, 1L, null, null); // 입금 내역 | ||
| bankService.syncTransactionsStub(clubId, 2L, null, null); // 출금 내역 | ||
| } catch (Exception e) { | ||
| // 동기화 실패 시 로깅만 하고 계속 진행 | ||
| System.err.println("Bank sync failed during unmatched query: " + e.getMessage()); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# syncTransactionsStub 구현에서 외부 I/O 여부 확인
rg -n "syncTransactionsStub" -C3 backend/src/main/javaRepository: kosa-02team/moida
Length of output: 7957
🏁 Script executed:
#!/bin/bash
# syncTransactionsStub 전체 구현 확인
rg -n "public List<TransactionLog> syncTransactionsStub" -A50 backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 2888
🏁 Script executed:
#!/bin/bash
# syncTransactionsStub 전체 구현 계속 확인
rg -n "public List<TransactionLog> syncTransactionsStub" -A100 backend/src/main/java/back/bank/service/BankService.java | head -120Repository: kosa-02team/moida
Length of output: 6638
🏁 Script executed:
#!/bin/bash
# syncTransactionsStub 전체 구현 끝까지 확인
sed -n '383,550p' backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 11039
🏁 Script executed:
#!/bin/bash
# getUnmatchedTransactions 메서드 확인
sed -n '249,270p' backend/src/main/java/back/bank/controller/BankAccountController.javaRepository: kosa-02team/moida
Length of output: 1154
동기화 호출을 트랜잭션 밖으로 분리하세요.
syncTransactionsStub()은 line 413에서 provider.getTransactionsStub()으로 외부 오픈뱅킹 API를 호출합니다. sync=true일 때 이 메서드를 @Transactional 범위에서 수행하면 외부 API 응답 시간 동안 DB 커넥션과 트랜잭션 락이 점유됩니다. 동기화를 먼저 완료한 뒤 필요한 DB 업데이트만 트랜잭션으로 감싸거나, propagation=NOT_SUPPORTED를 적용하여 트랜잭션 분리를 권장합니다.
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/bank/controller/BankAccountController.java` around
lines 249 - 267, The current getUnmatchedTransactions method is annotated with
`@Transactional` and calls bankService.syncTransactionsStub(...) which performs
external API calls (provider.getTransactionsStub()) and can hold DB
connections/locks; move the sync out of the transactional scope by either (A)
performing the sync before entering the `@Transactional` method (call
syncTransactionsStub(...) from a non-transactional wrapper or from the
controller before starting the transaction) or (B) change the sync method’s
transactional behavior to not participate in the transaction (annotate the
service method syncTransactionsStub or a new sync wrapper with
`@Transactional`(propagation = Propagation.NOT_SUPPORTED) or
`@Transactional`(propagation = Propagation.REQUIRES_NEW) so the external call runs
outside the caller’s transaction), ensuring only the DB update logic remains
inside the transaction; reference getUnmatchedTransactions and
bankService.syncTransactionsStub when you relocate or re-annotate the call.
| // 매칭 가능한 입금요청 조회 (수동 매칭용: PENDING + EXPIRED 포함) | ||
| List<PaymentRequest> availableRequests = paymentRequestRepository.findManualMatchableRequests(clubId); | ||
|
|
||
| // 디버깅: 입금요청 조회 결과 로깅 | ||
| System.out.println("🔍 [미매칭 거래 조회] clubId=" + clubId + ", 조회된 입금요청 수=" + availableRequests.size()); | ||
| for (PaymentRequest req : availableRequests) { | ||
| System.out.println(" - requestId=" + req.getRequestId() + ", memberName=" + req.getMemberName() + | ||
| ", status=" + req.getStatus() + ", expectedAmount=" + req.getExpectedAmount() + | ||
| ", expectedDate=" + req.getExpectedDate()); | ||
| } | ||
|
|
||
| // PaymentRequest를 DTO로 변환 (닉네임 포함) | ||
| List<PaymentRequestWithNickname> requestsWithNickname = availableRequests.stream() | ||
| .map(req -> { | ||
| String displayName = req.getMemberName(); | ||
| // 디버깅: 모든 입금요청 조회 (상태 무관) | ||
| List<PaymentRequest> allRequests = paymentRequestRepository.findByClubIdOrderByCreatedAtDesc(clubId); | ||
| System.out.println("🔍 [전체 입금요청] clubId=" + clubId + ", 전체 입금요청 수=" + allRequests.size()); | ||
| for (PaymentRequest req : allRequests) { | ||
| System.out.println(" - requestId=" + req.getRequestId() + ", memberName=" + req.getMemberName() + | ||
| ", status=" + req.getStatus() + ", expectedAmount=" + req.getExpectedAmount() + | ||
| ", expectedDate=" + req.getExpectedDate() + ", createdAt=" + req.getCreatedAt()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and check the specific lines mentioned
wc -l backend/src/main/java/back/bank/controller/BankAccountController.javaRepository: kosa-02team/moida
Length of output: 134
🏁 Script executed:
# Read the specific lines mentioned in the review
sed -n '290,308p' backend/src/main/java/back/bank/controller/BankAccountController.javaRepository: kosa-02team/moida
Length of output: 1344
🏁 Script executed:
# Find all System.out/err.println calls in the codebase to assess scope
rg -n "System\.(out|err)\.println" --type javaRepository: kosa-02team/moida
Length of output: 38315
🏁 Script executed:
# Check if there's a logging framework configured (logger usage)
rg -n "private.*Logger|LoggerFactory\.|log\.debug\|log\.info" --type java -A1 -B1 | head -100Repository: kosa-02team/moida
Length of output: 1737
System.out 로그에서 개인정보 제거 필요
memberName, expectedAmount, expectedDate, createdAt 등이 System.out.println으로 stdout에 직접 노출되어 운영 로그에 PII가 남습니다. 이미 프로젝트에 SLF4J가 도입되어 있으니, 이 디버그 로그를 logger.debug() 레벨로 전환하거나 제거해주세요.
제안하는 수정
- System.out.println("🔍 [미매칭 거래 조회] clubId=" + clubId + ", 조회된 입금요청 수=" + availableRequests.size());
- for (PaymentRequest req : availableRequests) {
- System.out.println(" - requestId=" + req.getRequestId() + ", memberName=" + req.getMemberName() +
- ", status=" + req.getStatus() + ", expectedAmount=" + req.getExpectedAmount() +
- ", expectedDate=" + req.getExpectedDate());
- }
-
- // 디버깅: 모든 입금요청 조회 (상태 무관)
- List<PaymentRequest> allRequests = paymentRequestRepository.findByClubIdOrderByCreatedAtDesc(clubId);
- System.out.println("🔍 [전체 입금요청] clubId=" + clubId + ", 전체 입금요청 수=" + allRequests.size());
- for (PaymentRequest req : allRequests) {
- System.out.println(" - requestId=" + req.getRequestId() + ", memberName=" + req.getMemberName() +
- ", status=" + req.getStatus() + ", expectedAmount=" + req.getExpectedAmount() +
- ", expectedDate=" + req.getExpectedDate() + ", createdAt=" + req.getCreatedAt());
- }🤖 Prompt for AI Agents
In `@backend/src/main/java/back/bank/controller/BankAccountController.java` around
lines 290 - 308, The System.out.println debug lines in BankAccountController
that iterate availableRequests and allRequests expose PII (memberName,
expectedAmount, expectedDate, createdAt); replace these System.out.println calls
with SLF4J logger.debug and remove or mask PII fields—log only non-PII
identifiers/status (e.g., requestId, status, size) or a hashed/id-only
representation for PaymentRequest entries (variables availableRequests,
allRequests, and the loop over PaymentRequest req). Ensure you use the existing
logger instance and debug level so these details aren't emitted to stdout in
production.
| // 잔액 재계산: 이전 잔액에 현재 거래 금액을 더함 (한 번만 계산) | ||
| BigDecimal calculatedBalance = runningBalance.add(tx.amount()); | ||
| System.out.println(" → 계산된 잔액: " + calculatedBalance); | ||
|
|
||
| // 잔액이 다르면 업데이트 | ||
| TransactionLog log = existingLog.get(); | ||
| if (log.getBalanceAfter().compareTo(calculatedBalance) != 0) { | ||
| System.out.println(" → 잔액 수정: " + log.getBalanceAfter() + " → " + calculatedBalance); | ||
| // TransactionLog의 잔액을 업데이트 | ||
| log.updateBalanceAfter(calculatedBalance); | ||
| transactionLogRepository.save(log); | ||
| } else { | ||
| System.out.println(" → 잔액 일치 (수정 불필요)"); | ||
| } | ||
| // runningBalance는 항상 계산된 값으로 업데이트 (저장된 값 신뢰하지 않음) | ||
| runningBalance = calculatedBalance; | ||
| System.out.println(" → 현재 잔액: " + runningBalance); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the BankService file and check its structure
find . -name "BankService.java" -type fRepository: kosa-02team/moida
Length of output: 119
🏁 Script executed:
# Search for BankTransaction definition
rg -n "record BankTransaction|class BankTransaction" -A 5Repository: kosa-02team/moida
Length of output: 1251
🏁 Script executed:
# Look at the specific lines mentioned in the review (289-305 and 350-353)
rg -n "syncTransactions" -B 2 -A 15 BankService.javaRepository: kosa-02team/moida
Length of output: 117
🏁 Script executed:
# Check how amounts are created in transaction providers
rg -n "BankTransaction" -B 2 -A 2 --type java | head -100Repository: kosa-02team/moida
Length of output: 9819
🏁 Script executed:
# Read the BankService file around the lines mentioned in the review
wc -l ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 123
🏁 Script executed:
# Read lines around 289-305
sed -n '280,310p' ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 2601
🏁 Script executed:
# Read lines around 350-353
sed -n '340,360p' ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 1504
🏁 Script executed:
# Search for syncTransactions and syncTransactionsStub methods
rg -n "def syncTransactions|void syncTransactions|private.*syncTransactions" ./backend/src/main/java/back/bank/service/BankService.java -A 20Repository: kosa-02team/moida
Length of output: 43
🏁 Script executed:
# Search for how amounts are created/transformed in bank providers
find ./backend -name "*Provider*.java" -o -name "*provider*.java" | head -5Repository: kosa-02team/moida
Length of output: 395
🏁 Script executed:
# Look at the full method signatures and implementations around syncTransactions
sed -n '250,330p' ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 6056
🏁 Script executed:
# Look at syncTransactionsStub method around 350-353
sed -n '330,400p' ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 4138
🏁 Script executed:
# Check how BankTransaction amounts are created in OpenBankingProvider
cat ./backend/src/main/java/back/bank/provider/OpenBankingProvider.java | head -100Repository: kosa-02team/moida
Length of output: 2287
🏁 Script executed:
# Check StubBankProvider to see how it creates amounts
cat ./backend/src/main/java/back/bank/provider/stub/StubBankProvider.javaRepository: kosa-02team/moida
Length of output: 12888
🏁 Script executed:
# Get more of the syncTransactionsStub method to see balance calculation logic
sed -n '330,500p' ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 10758
🏁 Script executed:
# Check if there's any sign handling in the balance calculation for syncTransactionsStub
rg -n "runningBalance.*add|change.*abs\(\)|negate\(\)" ./backend/src/main/java/back/bank/service/BankService.javaRepository: kosa-02team/moida
Length of output: 1136
🏁 Script executed:
# Look at test files to understand the amount sign convention
grep -n "BankTransaction\|amount\|DEPOSIT\|WITHDRAW" ./backend/src/test/java/back/bank/service/BankServiceTests.java | head -40Repository: kosa-02team/moida
Length of output: 501
syncTransactions의 잔액 계산이 출금에 대해 잘못될 수 있습니다.
동일한 로직을 syncTransactionsStub에서는 입/출금에 따라 부호를 보정하지만(lines 470-475, 500-505, 545-550), syncTransactions에서는 tx.amount()를 그대로 더하고 있습니다(lines 290, 313, 351). API에서 거래 금액은 항상 양수로 전달되고 type 필드(DEPOSIT/WITHDRAW)로 방향을 구분하므로, 부호 정규화 없이는 출금이 잔액을 증가시킬 위험이 있습니다. 두 경로를 동일하게 수정해주세요.
🛠️ 수정 예시
- BigDecimal calculatedBalance = runningBalance.add(tx.amount());
+ BigDecimal change = tx.amount();
+ if ("WITHDRAW".equalsIgnoreCase(tx.type())) {
+ change = change.abs().negate();
+ } else {
+ change = change.abs();
+ }
+ BigDecimal calculatedBalance = runningBalance.add(change);- runningBalance = runningBalance.add(tx.amount());
+ BigDecimal change = tx.amount();
+ if ("WITHDRAW".equalsIgnoreCase(tx.type())) {
+ change = change.abs().negate();
+ } else {
+ change = change.abs();
+ }
+ runningBalance = runningBalance.add(change);📝 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.
| // 잔액 재계산: 이전 잔액에 현재 거래 금액을 더함 (한 번만 계산) | |
| BigDecimal calculatedBalance = runningBalance.add(tx.amount()); | |
| System.out.println(" → 계산된 잔액: " + calculatedBalance); | |
| // 잔액이 다르면 업데이트 | |
| TransactionLog log = existingLog.get(); | |
| if (log.getBalanceAfter().compareTo(calculatedBalance) != 0) { | |
| System.out.println(" → 잔액 수정: " + log.getBalanceAfter() + " → " + calculatedBalance); | |
| // TransactionLog의 잔액을 업데이트 | |
| log.updateBalanceAfter(calculatedBalance); | |
| transactionLogRepository.save(log); | |
| } else { | |
| System.out.println(" → 잔액 일치 (수정 불필요)"); | |
| } | |
| // runningBalance는 항상 계산된 값으로 업데이트 (저장된 값 신뢰하지 않음) | |
| runningBalance = calculatedBalance; | |
| System.out.println(" → 현재 잔액: " + runningBalance); | |
| // 잔액 재계산: 이전 잔액에 현재 거래 금액을 더함 (한 번만 계산) | |
| BigDecimal change = tx.amount(); | |
| if ("WITHDRAW".equalsIgnoreCase(tx.type())) { | |
| change = change.abs().negate(); | |
| } else { | |
| change = change.abs(); | |
| } | |
| BigDecimal calculatedBalance = runningBalance.add(change); | |
| System.out.println(" → 계산된 잔액: " + calculatedBalance); | |
| // 잔액이 다르면 업데이트 | |
| TransactionLog log = existingLog.get(); | |
| if (log.getBalanceAfter().compareTo(calculatedBalance) != 0) { | |
| System.out.println(" → 잔액 수정: " + log.getBalanceAfter() + " → " + calculatedBalance); | |
| // TransactionLog의 잔액을 업데이트 | |
| log.updateBalanceAfter(calculatedBalance); | |
| transactionLogRepository.save(log); | |
| } else { | |
| System.out.println(" → 잔액 일치 (수정 불필요)"); | |
| } | |
| // runningBalance는 항상 계산된 값으로 업데이트 (저장된 값 신뢰하지 않음) | |
| runningBalance = calculatedBalance; | |
| System.out.println(" → 현재 잔액: " + runningBalance); |
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/bank/service/BankService.java` around lines 289 -
305, The syncTransactions path incorrectly uses tx.amount() as-is when computing
runningBalance (see syncTransactions and variables runningBalance, tx.amount(),
TransactionLog.updateBalanceAfter, transactionLogRepository.save), so adjust the
amount sign the same way as syncTransactionsStub: derive a signedAmount by
checking tx.type (DEPOSIT vs WITHDRAW) and use runningBalance =
runningBalance.add(signedAmount) and compare/update log.getBalanceAfter()
against the computed balance; ensure all places in syncTransactions that add
tx.amount() (the calculation and update points) use the normalized signedAmount
instead.
| public record AdditionalFeeRequest( | ||
| BigDecimal amountPerPerson, // 1인당 추가 금액 | ||
| String reason // 추가 회비 사유 (선택) | ||
| ) {} |
There was a problem hiding this comment.
금액 필드에 유효성 검증이 누락되었습니다.
amountPerPerson은 금융 데이터로, null, 0, 또는 음수 값이 허용되면 안 됩니다. Jakarta Validation 어노테이션을 추가하세요.
✅ 제안된 수정
package back.dto.ledger.request;
import java.math.BigDecimal;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
public record AdditionalFeeRequest(
+ `@NotNull`(message = "1인당 추가 금액은 필수입니다")
+ `@Positive`(message = "1인당 추가 금액은 양수여야 합니다")
BigDecimal amountPerPerson, // 1인당 추가 금액
String reason // 추가 회비 사유 (선택)
) {}📝 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.
| public record AdditionalFeeRequest( | |
| BigDecimal amountPerPerson, // 1인당 추가 금액 | |
| String reason // 추가 회비 사유 (선택) | |
| ) {} | |
| package back.dto.ledger.request; | |
| import java.math.BigDecimal; | |
| import jakarta.validation.constraints.NotNull; | |
| import jakarta.validation.constraints.Positive; | |
| public record AdditionalFeeRequest( | |
| `@NotNull`(message = "1인당 추가 금액은 필수입니다") | |
| `@Positive`(message = "1인당 추가 금액은 양수여야 합니다") | |
| BigDecimal amountPerPerson, // 1인당 추가 금액 | |
| String reason // 추가 회비 사유 (선택) | |
| ) {} |
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/dto/ledger/request/AdditionalFeeRequest.java`
around lines 5 - 8, Annotate the record component amountPerPerson in
AdditionalFeeRequest with Jakarta Validation annotations to forbid null, zero,
or negative values (e.g., add `@NotNull` and `@DecimalMin`(value = "0.01") or
`@Positive`) and add the corresponding imports from jakarta.validation.constraints
so the validator can enforce the constraint at binding time; apply the
annotations directly to the component declaration in the AdditionalFeeRequest
record and ensure imports are updated accordingly.
| // ATTENDANCE 투표 마감 시 투표 결과를 기반으로 참석자 상태 업데이트 및 참가비 요청 생성 | ||
| if ("ATTENDANCE".equals(vote.getVoteType()) && vote.getScheduleId() != null) { | ||
| System.out.println("🗳️ [투표 종료] ATTENDANCE 투표 마감 시작: voteId=" + voteId + ", scheduleId=" + vote.getScheduleId()); | ||
|
|
||
| Schedules schedule = scheduleRepository.findById(vote.getScheduleId()) | ||
| .orElse(null); | ||
|
|
||
| if (schedule != null) { | ||
| BigDecimal entryFee = schedule.getEntryFee(); | ||
| System.out.println(" → 일정 조회 성공: entryFee=" + entryFee); | ||
|
|
||
| // 투표 결과를 기반으로 참석자 상태 업데이트 | ||
| updateParticipantsFromVoteResults(vote.getVoteId(), vote.getScheduleId()); | ||
|
|
||
| // 참가비가 있고, 0보다 큰 경우에만 참가비 요청 생성 | ||
| if (entryFee != null && entryFee.compareTo(BigDecimal.ZERO) > 0) { | ||
| System.out.println(" → 참가비 요청 생성 시도: clubId=" + clubId + ", scheduleId=" + vote.getScheduleId() + ", userId=" + userId); | ||
| try { | ||
| // 투표 마감 시에는 권한 체크를 우회하고 직접 참가비 요청 생성 | ||
| createPaymentRequestsFromVoteResults(clubId, vote.getVoteId(), vote.getScheduleId(), entryFee); | ||
| System.out.println(" ✓ 참가비 요청 생성 완료"); | ||
| } catch (Exception e) { | ||
| // 참가비 요청 생성 실패 시 로깅만 하고 계속 진행 | ||
| System.err.println(" ❌ 참가비 요청 생성 실패: clubId=" + clubId + ", scheduleId=" + vote.getScheduleId() + ", error=" + e.getMessage()); | ||
| e.printStackTrace(); | ||
| org.slf4j.LoggerFactory.getLogger(VoteService.class) | ||
| .warn("ATTENDANCE 투표 마감 시 참가비 요청 생성 실패: clubId={}, scheduleId={}, error={}", | ||
| clubId, vote.getScheduleId(), e.getMessage(), e); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# VoteService.java 파일의 System.out/err 사용 위치 확인
rg -n "System\.(out|err)" backend/src/main/java/back/service/vote/VoteService.javaRepository: kosa-02team/moida
Length of output: 2395
🏁 Script executed:
# 파일의 총 라인 수와 구조 확인
wc -l backend/src/main/java/back/service/vote/VoteService.javaRepository: kosa-02team/moida
Length of output: 121
🏁 Script executed:
# 파일이 Slf4j를 사용하는지 확인
rg -n "Slf4j|LoggerFactory" backend/src/main/java/back/service/vote/VoteService.java | head -20Repository: kosa-02team/moida
Length of output: 214
🏁 Script executed:
# 819-821 줄 주변 코드 확인
sed -n '815,825p' backend/src/main/java/back/service/vote/VoteService.javaRepository: kosa-02team/moida
Length of output: 507
🏁 Script executed:
# 코드베이스에서 Slf4j 사용 패턴 확인
fd "\.java$" backend/src/main/java/back/service/ | head -5 | xargs rg -l "private.*Logger" | head -3Repository: kosa-02team/moida
Length of output: 43
System.out/err 및 실명을 포함한 로그 출력을 Slf4j로 전환이 필요합니다.
System.out/err 호출이 파일 전반에 걸쳐 24개 이상 존재하며, 특히 819줄의 realName 필드는 개인식별정보(PII)를 명시적으로 노출합니다. 예를 들어:
- 349줄:
userId,clubId포함 - 723줄, 789줄:
userId노출 - 819줄:
memberName=+realName(명시적 실명 기록)
이 파일의 285줄과 358줄에서 이미 Slf4j를 사용하고 있으므로, 모든 System.out/err 호출을 통일된 로거로 변경하고 민감한 식별자(userId, clubId, memberId)는 마스킹하거나 제거하세요.
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/service/vote/VoteService.java` around lines 333 -
360, Replace all System.out/err prints in VoteService with the class SLF4J
logger (use org.slf4j.LoggerFactory.getLogger(VoteService.class) / a private
static final Logger field) and standard log levels (info/warn/error) so logging
is consistent with existing calls; specifically convert the prints around the
ATTENDANCE block (the System.out lines that announce vote closing,
schedule/entryFee info, and the System.err stack traces) and any other
occurrences to logger.info/warn/error. Remove or redact PII in those log
messages: never log full userId, clubId, memberId, or realName—mask them (e.g.,
partial hash or last 4 chars) or omit entirely; ensure methods referenced like
updateParticipantsFromVoteResults(...) and
createPaymentRequestsFromVoteResults(...) do not emit raw PII either. Keep
exception logging using logger.warn/error with the exception object (not
e.getMessage() concatenation) to preserve stack traces without embedding
sensitive identifiers.
| try { | ||
| // 투표 마감 시에는 권한 체크를 우회하고 직접 참가비 요청 생성 | ||
| createPaymentRequestsFromVoteResults(clubId, vote.getVoteId(), vote.getScheduleId(), entryFee); | ||
| System.out.println(" ✓ 참가비 요청 생성 완료"); |
There was a problem hiding this comment.
은행 동기화 호출에 하드코딩된 userId(1L) 제거가 필요합니다.
syncTransactionsStub에 1L을 고정하면 권한/감사 추적이 깨지고 실제 사용자와 불일치할 수 있습니다. 호출자 userId를 전달하거나 별도의 시스템 계정을 명확히 주입하세요.
🐛 제안 수정
- private void createPaymentRequestsFromVoteResults(Long clubId, Long voteId, Long scheduleId, BigDecimal entryFee) {
+ private void createPaymentRequestsFromVoteResults(Long clubId, Long voteId, Long scheduleId, BigDecimal entryFee, Long requestedByUserId) {
...
- if (bankAccountRepository.findByClubId(clubId).isPresent()) {
- bankService.syncTransactionsStub(clubId, 1L, null, null);
- }
+ if (bankAccountRepository.findByClubId(clubId).isPresent()) {
+ bankService.syncTransactionsStub(clubId, requestedByUserId, null, null);
+ }- createPaymentRequestsFromVoteResults(clubId, vote.getVoteId(), vote.getScheduleId(), entryFee);
+ createPaymentRequestsFromVoteResults(clubId, vote.getVoteId(), vote.getScheduleId(), entryFee, userId);Also applies to: 831-834
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/service/vote/VoteService.java` around lines 350 -
353, The call to syncTransactionsStub in VoteService is using a hardcoded userId
(1L); remove the magic constant and pass the real caller or a dedicated system
account instead. Update the call sites (including the invocation inside
createPaymentRequestsFromVoteResults and the other occurrence around lines
831-834) to accept a userId parameter or read an injected/system account (e.g.,
from an AuthenticationContext or a configured SystemUserProvider) and propagate
that userId into syncTransactionsStub, adjusting method signatures for
createPaymentRequestsFromVoteResults and any calling methods in VoteService
accordingly so the real user or explicit system account is used instead of 1L.
| -- 기존 테스트 데이터 삭제 (중복 방지) | ||
| DELETE FROM club_members WHERE club_id IN (1, 2, 3, 8); | ||
| DELETE FROM bank_accounts WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); | ||
| DELETE FROM clubs WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); | ||
| DELETE FROM users WHERE user_id IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28); |
There was a problem hiding this comment.
테스트 데이터 정리가 불완전합니다.
schedules, posts, comments, votes, vote_options, vote_records, reports, transaction_log, payment_requests, schedule_participants, post_likes, comment_likes 테이블에 대한 DELETE 문이 누락되어 있습니다. 테스트 스크립트를 여러 번 실행할 때 중복 데이터 문제가 발생할 수 있습니다.
또한 user_id=1이 DELETE 문에서 제외되어 있지만 INSERT 문(라인 57)에는 포함되어 있어, 재실행 시 중복 키 오류가 발생할 수 있습니다.
🔧 누락된 DELETE 문 추가 제안
DELETE FROM club_members WHERE club_id IN (1, 2, 3, 8);
DELETE FROM bank_accounts WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
DELETE FROM clubs WHERE club_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
-DELETE FROM users WHERE user_id IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28);
+DELETE FROM users WHERE user_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28);
+DELETE FROM comment_likes WHERE like_id IN (1, 2, 3, 4, 5);
+DELETE FROM post_likes WHERE like_id IN (1, 2, 3, 4, 5, 6);
+DELETE FROM schedule_participants WHERE participant_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+DELETE FROM payment_requests WHERE request_id IN (1, 2, 3, 4, 5, 6);
+DELETE FROM transaction_log WHERE log_id IN (1, 2, 3, 4, 5, 6);
+DELETE FROM reports WHERE report_id IN (1, 2, 3, 4, 5);
+DELETE FROM vote_records WHERE record_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9);
+DELETE FROM vote_options WHERE option_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9);
+DELETE FROM votes WHERE vote_id IN (1, 2, 3, 4);
+DELETE FROM comments WHERE comment_id IN (1, 2, 3, 4, 5, 6);
+DELETE FROM posts WHERE post_id IN (1, 2, 3, 4, 5, 6, 7);
+DELETE FROM schedules WHERE schedule_id IN (1, 2, 3, 4, 5, 6, 7);🤖 Prompt for AI Agents
In `@backend/src/main/resources/db/migration/integration_test_data.sql` around
lines 12 - 16, The cleanup SQL is missing DELETEs for several child tables and
omits user_id=1, causing duplicate-key and FK issues; in
integration_test_data.sql add DELETE statements for schedules, posts, comments,
votes, vote_options, vote_records, reports, transaction_log, payment_requests,
schedule_participants, post_likes, comment_likes (and any
like/participant/report/vote child tables) before deleting
clubs/bank_accounts/club_members/users, and include user_id=1 in the users
DELETE (or remove the INSERT for user_id=1) so the script can be re-run safely;
ensure the deletes run in FK-safe order (children first, then parents) to avoid
constraint errors.
| // 백엔드에서 오래된 것부터 정렬되어 오므로, 최신순으로 표시하기 위해 reverse | ||
| setTransactions([...data].reverse()); | ||
|
|
There was a problem hiding this comment.
최신순 정렬이 일부 재조회 경로에서 깨집니다.
Line 71~72에서 reverse를 적용했지만, 수동 기록 추가/수정/매칭 후 재조회에서는 setTransactions(data)로 그대로 들어가 정렬이 다시 과거순으로 바뀔 수 있습니다. 동일한 정렬을 공통화해 주세요.
🔧 수정 예시 (정렬 로직 공통화)
+ const normalizeTransactions = (data: TransactionLogResponse[]) => [...data].reverse();
...
- setTransactions([...data].reverse());
+ setTransactions(normalizeTransactions(data));
...
- setTransactions(data);
+ setTransactions(normalizeTransactions(data));
...
- setTransactions(transactionData);
+ setTransactions(normalizeTransactions(transactionData));🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/dues/LedgerView.tsx` around lines 71 - 73,
The transactions ordering is inconsistent because LedgerView sometimes calls
setTransactions([...data].reverse()) but other flows call setTransactions(data)
directly; centralize the sorting by adding a single helper (e.g.,
normalizeTransactions or sortTransactions) used everywhere in LedgerView and
related handlers so all incoming arrays are copied and reversed consistently
before calling setTransactions; update all places that set transactions (initial
load, add/edit/match callbacks, refresh handlers) to pass data through that
helper instead of calling setTransactions(data) directly.
| // 일정 마무리 처리 (정산 + 환급 + 마감) | ||
| // 장부에 기록된 지출을 기반으로 자동 계산 | ||
| const handleFinalize = async () => { | ||
| if (!groupId || !scheduleId || !schedule) return; | ||
|
|
||
| try { | ||
| setIsFinalizing(true); | ||
| // totalSpent를 전송하지 않으면 백엔드에서 TransactionLog 기반으로 자동 계산 | ||
| await finalizeSchedule(Number(groupId), Number(scheduleId)); | ||
| await finalizeSchedule(Number(groupId), Number(scheduleId), { | ||
| totalSpent: finalizeTotalSpent, | ||
| }); |
There was a problem hiding this comment.
환급 대상 선택이 서버로 전달되지 않아 UI와 실제 정산이 불일치합니다.
finalizeParticipantIds는 UI 계산에만 쓰이고 finalizeSchedule 호출에는 포함되지 않아 선택이 무시됩니다.
선택 UI를 제거하거나, 환급 대상 ID를 요청에 포함해 백엔드 정산/환급 로직에 반영하도록 맞춰 주세요.
Also applies to: 1774-1790
🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/schedule/ScheduleDetailView.tsx` around
lines 571 - 579, The finalize flow currently ignores the selected refund
recipients because finalizeParticipantIds is only used for UI math and not
passed to the backend; update the handleFinalize flow (the function
handleFinalize invoking finalizeSchedule/finalizeSchedule) to include the
selected IDs in the request payload (e.g., send finalizeParticipantIds or
similarly named array alongside totalSpent) so the backend receives the refund
targets, or if the UI should not control refunds remove the selection UI and
related finalizeParticipantIds state; locate handleFinalize and the
finalizeSchedule/finalizeSchedule call sites (also review the equivalent logic
around lines referenced ~1774-1790) and ensure the request body includes the
participant ID list when calling the finalize API.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/src/app/components/group/dues/LedgerView.tsx`:
- Around line 267-273: The type assertion for the Promise.all result is wrong:
getUnmatchedTransactions returns an UnmatchedTransactionsResponse (not an
array), so update the cast on the Promise.all result in LedgerView.tsx to use
[TransactionLogResponse[], UnmatchedTransactionsResponse] (remove the trailing
[]), then assign transactionData to setTransactions and unmatchedDataRefresh to
setUnmatchedData as before; ensure references are to getLedger,
getUnmatchedTransactions, transactionData, unmatchedDataRefresh, setTransactions
and setUnmatchedData.
| const [transactionData, unmatchedDataRefresh] = await Promise.all([ | ||
| getLedger(Number(groupId), startDate || undefined, endDate || undefined), | ||
| getUnmatchedTransactions(Number(groupId)) | ||
| ]); | ||
| getUnmatchedTransactions(Number(groupId)), | ||
| ]) as [TransactionLogResponse[], UnmatchedTransactionsResponse[]]; | ||
|
|
||
| setTransactions(transactionData); | ||
| setUnmatchedData(unmatchedDataRefresh); |
There was a problem hiding this comment.
타입 단언이 잘못되었습니다.
getUnmatchedTransactions는 UnmatchedTransactionsResponse를 반환하지만, 타입 단언에서 UnmatchedTransactionsResponse[](배열)로 선언되어 있습니다. 배열 대괄호([])를 제거해야 합니다.
🔧 수정 제안
const [transactionData, unmatchedDataRefresh] = await Promise.all([
getLedger(Number(groupId), startDate || undefined, endDate || undefined),
getUnmatchedTransactions(Number(groupId)),
- ]) as [TransactionLogResponse[], UnmatchedTransactionsResponse[]];
+ ]) as [TransactionLogResponse[], UnmatchedTransactionsResponse];📝 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.
| const [transactionData, unmatchedDataRefresh] = await Promise.all([ | |
| getLedger(Number(groupId), startDate || undefined, endDate || undefined), | |
| getUnmatchedTransactions(Number(groupId)) | |
| ]); | |
| getUnmatchedTransactions(Number(groupId)), | |
| ]) as [TransactionLogResponse[], UnmatchedTransactionsResponse[]]; | |
| setTransactions(transactionData); | |
| setUnmatchedData(unmatchedDataRefresh); | |
| const [transactionData, unmatchedDataRefresh] = await Promise.all([ | |
| getLedger(Number(groupId), startDate || undefined, endDate || undefined), | |
| getUnmatchedTransactions(Number(groupId)), | |
| ]) as [TransactionLogResponse[], UnmatchedTransactionsResponse]; | |
| setTransactions(transactionData); | |
| setUnmatchedData(unmatchedDataRefresh); |
🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/dues/LedgerView.tsx` around lines 267 -
273, The type assertion for the Promise.all result is wrong:
getUnmatchedTransactions returns an UnmatchedTransactionsResponse (not an
array), so update the cast on the Promise.all result in LedgerView.tsx to use
[TransactionLogResponse[], UnmatchedTransactionsResponse] (remove the trailing
[]), then assign transactionData to setTransactions and unmatchedDataRefresh to
setUnmatchedData as before; ensure references are to getLedger,
getUnmatchedTransactions, transactionData, unmatchedDataRefresh, setTransactions
and setUnmatchedData.
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/test/java/back/service/ledger/EventFundServiceTest.java (2)
103-135: clubMemberRepository 스텁 누락으로 인한 PaymentRequest 저장 실패 확인됨EventFundService의
collectEntryFees메서드 (128-131줄)는 각 참가자에 대해clubMemberRepository.findByClubIdAndUserIdAndStatus()를 호출하여 memberId를 조회합니다. 현재 테스트에서 이 메서드의 스텁이 없어 빈 Optional이 반환되며, 이로 인해 memberId가 null이 되고 (133줄 조건), 루프가 continue하면서 PaymentRequest 저장이 스킵됩니다.테스트에 다음 스텁을 추가하세요:
- 참가자 1 (userId=10L):
clubMemberRepository.findByClubIdAndUserIdAndStatus(clubId, 10L, Status.ACTIVE)가 유효한 ClubMembers 객체를 반환하도록 설정- 참가자 2 (userId=11L):
clubMemberRepository.findByClubIdAndUserIdAndStatus(clubId, 11L, Status.ACTIVE)가 유효한 ClubMembers 객체를 반환하도록 설정
160-207: bankAccountRepository와 transactionLogRepository 조회 결과 스텁이 필요합니다.CI의 NPE는
bankAccountRepository.findByClubId()호출 시 Mock이 스텁되지 않아 null을 반환하면서 발생합니다 (391줄, 453줄). 추가로transactionLogRepository.findLatestByClubId()도 동일하게 스텁되지 않았으므로 (395줄, 457줄) 함께 수정이 필요합니다.스텁할 항목:
given(bankAccountRepository.findByClubId(clubId)).willReturn(Optional.empty());또는 필요시 BankAccounts 객체를 포함한 Optionalgiven(transactionLogRepository.findLatestByClubId(clubId)).willReturn(Optional.empty());또는 필요시 TransactionLog 객체를 포함한 Optional
♻️ Duplicate comments (1)
backend/src/test/java/back/service/ledger/EventFundServiceTest.java (1)
221-271: 동일 NPE 이슈 재현 구간입니다.
Line 271에서도 동일 호출 경로가 있어 위 스텁 보강이 필요합니다.
…/kosa-02team/moida into feat/#107-schedule-additionalfee
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@backend/src/main/java/back/service/vote/VoteService.java`:
- Around line 687-690: The current matching in VoteService using
options.stream().filter(opt -> "참석".equals(opt.getOptionText()) ||
opt.getOptionText().contains("참석")) can false-positive on texts like "불참";
change the logic to use a precise match (e.g., equals or regex for word
boundaries) or explicitly exclude "불참" (check opt.getOptionText().contains("불참")
first) — alternatively prefer matching by optionOrder (e.g., optionOrder == 1
for 참석, 2 for 불참) using the VoteOptions.getOptionOrder() to reliably identify
the 참석 option.
In
`@backend/src/test/java/back/service/ledger/TransactionMatchingServiceTest.java`:
- Around line 122-124: 테스트 내에서 선언만 되고 사용되지 않는 PaymentRequest requestB 변수를 제거하거나
동명이인 시나리오를 실제로 검증하도록 변경하세요; 즉, TransactionMatchingServiceTest의 requestB 선언을 삭제하여
불필요한 mock을 제거하거나 requestB에 대한 mock 설정(예: when(...) 호출)과 해당 검증(assert/verify)을
추가해 requestA와 다른 사용자이지만 동일 이름인 케이스를 실제로 테스트하도록 만드세요.
♻️ Duplicate comments (6)
backend/src/main/java/back/service/vote/VoteService.java (3)
329-365: System.out/err 사용을 Slf4j 로거로 통일하고, PII 노출을 제거해야 합니다.이 블록 전체에서
System.out.println과System.err.println이 사용되고 있으며,userId,clubId,scheduleId같은 식별자가 로그에 노출됩니다. Lines 279-281과 355-357에서는 이미 Slf4j를 사용하고 있으므로, 모든 로깅을 통일해야 합니다.또한 Line 332-333에서
orElse(null)을 사용하면 일정을 찾지 못했을 때 조용히 실패합니다. 투표 마감 시 참가비 요청 생성이 누락될 수 있습니다.🐛 제안된 수정
- System.out.println( - "🗳️ [투표 종료] ATTENDANCE 투표 마감 시작: voteId=" + voteId + ", scheduleId=" + vote.getScheduleId()); - - Schedules schedule = scheduleRepository.findById(vote.getScheduleId()) - .orElse(null); - - if (schedule != null) { + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VoteService.class); + log.info("ATTENDANCE 투표 마감 시작"); + + Schedules schedule = scheduleRepository.findById(vote.getScheduleId()) + .orElseThrow(() -> new ResourceException.NotFound("Schedule not found")); + BigDecimal entryFee = schedule.getEntryFee(); - System.out.println(" → 일정 조회 성공: entryFee=" + entryFee); + log.debug("일정 조회 성공");
838-844: 하드코딩된userId(1L)제거가 필요합니다.
syncTransactionsStub에1L을 고정하면 권한/감사 추적이 깨지고 실제 사용자와 불일치할 수 있습니다. 메서드 시그니처에requestedByUserId를 추가하여 실제 호출자를 전달하세요.🐛 제안된 수정
메서드 시그니처 변경:
- private void createPaymentRequestsFromVoteResults(Long clubId, Long voteId, Long scheduleId, BigDecimal entryFee) { + private void createPaymentRequestsFromVoteResults(Long clubId, Long voteId, Long scheduleId, BigDecimal entryFee, Long requestedByUserId) {호출부 변경 (Line 348):
- createPaymentRequestsFromVoteResults(clubId, vote.getVoteId(), vote.getScheduleId(), entryFee); + createPaymentRequestsFromVoteResults(clubId, vote.getVoteId(), vote.getScheduleId(), entryFee, userId);syncTransactionsStub 호출 변경:
- bankService.syncTransactionsStub(clubId, 1L, null, null); + bankService.syncTransactionsStub(clubId, requestedByUserId, null, null);
826-828: 로그에realName(PII)이 노출됩니다.
memberName=" + realName은 개인식별정보를 로그에 기록합니다. GDPR/개인정보보호법 준수를 위해 민감한 정보는 마스킹하거나 제거해야 합니다.🐛 제안된 수정
- System.out.println(" ✓ 참가비 요청 생성: requestId=" + savedReq.getRequestId() + - ", memberName=" + realName + ", amount=" + entryFee + - ", expectedDate=" + expectedDate); + log.info("참가비 요청 생성 완료: requestId={}", savedReq.getRequestId());backend/src/main/java/back/bank/controller/BankAccountController.java (3)
60-75: 예외 메시지 응답 노출 제거
500 응답에e.getMessage()를 포함하면 내부 정보가 외부로 노출될 수 있습니다. 고정 메시지로 응답하고 상세는 서버 로그로만 남겨주세요.🐛 수정 제안
- System.err.println("Failed to get account: " + e.getMessage()); - e.printStackTrace(); - return ResponseEntity.internalServerError().body("계좌 조회 실패: " + e.getMessage()); + log.error("계좌 조회 실패: clubId={}", clubId, e); + return ResponseEntity.internalServerError().body("계좌 조회 중 오류가 발생했습니다.");로거 필드 선언이 필요합니다. (예:
private static final Logger log = LoggerFactory.getLogger(BankAccountController.class);)
251-265: 외부 동기화 호출을 트랜잭션 밖으로 분리
@Transactional범위에서syncTransactionsStub(외부 API) 호출이 발생하면 DB 커넥션/락 점유가 길어질 수 있습니다. 동기화를 트랜잭션 밖에서 수행하거나Propagation.NOT_SUPPORTED등으로 분리하세요.
292-307: PII 포함 디버그 로그 제거/마스킹
memberName,expectedAmount,printContent, 실명/닉네임 등 개인정보가 stdout에 그대로 출력됩니다. 운영 로그 노출 위험이 크므로 logger.debug로 전환하고 식별자만 남기거나 마스킹하세요. (아래 예시와 동일한 원칙을determineFailureReason로그에도 적용 필요)🛡️ 예시 수정
- System.out.println("🔍 [미매칭 거래 조회] clubId=" + clubId + ", 조회된 입금요청 수=" + availableRequests.size()); - for (PaymentRequest req : availableRequests) { - System.out.println(" - requestId=" + req.getRequestId() + ", memberName=" + req.getMemberName() + - ", status=" + req.getStatus() + ", expectedAmount=" + req.getExpectedAmount() + - ", expectedDate=" + req.getExpectedDate()); - } + log.debug("미매칭 거래 조회: clubId={}, 요청 수={}", clubId, availableRequests.size()); + for (PaymentRequest req : availableRequests) { + log.debug(" - requestId={}, status={}", req.getRequestId(), req.getStatus()); + }Also applies to: 373-431
🧹 Nitpick comments (8)
backend/src/test/java/back/service/ledger/TransactionMatchingServiceTest.java (3)
95-150: 동명이인 테스트 케이스 개선 제안버그 수정 검증 테스트로서 좋은 접근입니다. 다만, 실제 동명이인 시나리오를 더 완벽히 검증하려면:
requestA와requestB두 요청 모두를findMatchableRequests에서 반환하도록 설정- 정확히 하나의 요청만 매칭되는지 검증
- 매칭되지 않은 요청은 그대로 남아있는지 검증
현재 테스트는 단일 요청만 반환하므로, 동명이인 상황에서 "올바른" 요청이 선택되는 로직까지는 검증하지 못합니다.
31-47: 들여쓰기가 일관적이지 않습니다.클래스 내부의 필드와 메서드가 8칸 들여쓰기를 사용하고 있는데, 일반적인 Java 컨벤션(4칸 또는 탭)과 다릅니다. 프로젝트의 코드 스타일 가이드에 맞게 수정하는 것을 권장합니다.
44-47: @Injectmocks에 필요한 모킹이지만 테스트에서 검증되지 않습니다.
scheduleParticipantRepository와auditLogsRepository는TransactionMatchingService생성자 주입의 의존성이므로@InjectMocks가 정상 작동하기 위해 필요합니다. 다만 현재 테스트에서는 이 모키들이 직접 검증(verify)되고 있지 않습니다.현재 테스트 설정상
request.getScheduleId()가 null을 반환하므로,scheduleParticipantRepository를 사용하는 코드 경로가 실행되지 않을 수 있습니다.auditLogsRepository는 다른 메서드(manualMatch,cancelMatch등)에서 사용되며, 이들 메서드를 테스트하려면 적절한 검증이 필요합니다.backend/src/main/java/back/service/vote/VoteService.java (3)
59-59: 미사용 의존성:EventFundService가 주입되었지만 사용되지 않습니다.
EventFundService가 의존성으로 선언되어 있지만 이 클래스 내에서 어디에서도 사용되지 않습니다. 불필요한 의존성은 제거하거나, 향후 사용 계획이 있다면 TODO 주석을 추가해 주세요.♻️ 제안된 수정
- private final EventFundService eventFundService;
408-415: 권한 체크에 try-catch를 사용하는 것은 제어 흐름 안티패턴입니다.예외를 제어 흐름으로 사용하는 것보다 명시적인 boolean 반환 메서드가 더 명확합니다.
ClubAuthService에hasAtLeastManagerPermission(clubId, userId)같은 메서드를 추가하거나, 기존 로직을 활용하는 것이 좋습니다.♻️ 제안된 수정
- boolean hasManagerPermission = false; - try { - clubsAuthorizationService.assertAtLeastManager(clubId, userId); - hasManagerPermission = true; - } catch (back.exception.ClubException e) { - // 운영진 권한이 없음 - ClubException의 모든 하위 예외 처리 - hasManagerPermission = false; - } + // ClubAuthService에 hasAtLeastManagerPermission 메서드 추가 권장 + boolean hasManagerPermission = isManagerOrAbove(clubId, userId);
VoteService에 헬퍼 메서드 추가:private boolean isManagerOrAbove(Long clubId, Long userId) { try { clubsAuthorizationService.assertAtLeastManager(clubId, userId); return true; } catch (back.exception.ClubException e) { return false; } }
818-819: 매직 넘버를 상수로 추출하는 것이 좋습니다.
10(매칭 범위 일수)과14(만료 일수)가 하드코딩되어 있습니다. 이 값들을 상수로 추출하면 의미가 명확해지고 유지보수가 용이해집니다.♻️ 제안된 수정
클래스 상단에 상수 추가:
private static final int PAYMENT_MATCH_DAYS_RANGE = 10; private static final int PAYMENT_EXPIRY_DAYS = 14;사용부:
- 10, // ±10일 범위 - schedule.getEventDate().plusDays(14), + PAYMENT_MATCH_DAYS_RANGE, + schedule.getEventDate().plusDays(PAYMENT_EXPIRY_DAYS),backend/src/main/java/back/bank/repository/BankTransactionHistoryRepository.java (1)
25-34: 이전 거래 조회 쿼리용 복합 인덱스 고려
findPreviousHistory는clubId,bankTransactionAt,historyId조건/정렬을 함께 사용합니다. 데이터가 커질수록 정렬/페이지네이션 비용이 커지므로(club_id, bank_transaction_at, history_id)복합 인덱스 추가를 권장합니다.backend/src/main/java/back/bank/controller/BankAccountController.java (1)
223-227: TransactionHistory N+1 조회 완화 제안
루프마다findById호출은 N+1을 유발할 수 있습니다.findAllById로 한번에 가져와 맵으로 조회하는 방식이 더 효율적입니다.♻️ 제안 리팩터링
- List<TransactionDetail> details = new ArrayList<>(); - for (TransactionLog log : scheduleLogs) { - BankTransactionHistory history = null; - PaymentRequest matchedRequest = null; - - if (log.getBankHistoryId() != null) { - history = transactionHistoryRepository.findById(log.getBankHistoryId()).orElse(null); - if (history != null) { - matchedRequest = historyIdToRequest.get(history.getHistoryId()); - } - } + List<Long> historyIds = scheduleLogs.stream() + .map(TransactionLog::getBankHistoryId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + Map<Long, BankTransactionHistory> historyMap = transactionHistoryRepository.findAllById(historyIds) + .stream() + .collect(Collectors.toMap(BankTransactionHistory::getHistoryId, h -> h, (a, b) -> a)); + + List<TransactionDetail> details = new ArrayList<>(); + for (TransactionLog log : scheduleLogs) { + BankTransactionHistory history = log.getBankHistoryId() != null + ? historyMap.get(log.getBankHistoryId()) + : null; + PaymentRequest matchedRequest = history != null ? historyIdToRequest.get(history.getHistoryId()) : null;
| VoteOptions attendOption = options.stream() | ||
| .filter(opt -> "참석".equals(opt.getOptionText()) || opt.getOptionText().contains("참석")) | ||
| .findFirst() | ||
| .orElse(null); |
There was a problem hiding this comment.
문자열 매칭이 취약합니다: contains("참석")이 "불참"도 매칭할 수 있습니다.
opt.getOptionText().contains("참석")은 "불참"이라는 텍스트도 매칭합니다. "불참"을 먼저 체크하거나, 정확한 매칭 로직을 사용해야 합니다.
🐛 제안된 수정
VoteOptions attendOption = options.stream()
- .filter(opt -> "참석".equals(opt.getOptionText()) || opt.getOptionText().contains("참석"))
+ .filter(opt -> "참석".equals(opt.getOptionText()) ||
+ (opt.getOptionText().contains("참석") && !opt.getOptionText().contains("불참")))
.findFirst()
.orElse(null);또는 옵션 순서(optionOrder)를 활용하여 매칭하는 것이 더 안정적입니다 (참석=1, 불참=2).
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/service/vote/VoteService.java` around lines 687 -
690, The current matching in VoteService using options.stream().filter(opt ->
"참석".equals(opt.getOptionText()) || opt.getOptionText().contains("참석")) can
false-positive on texts like "불참"; change the logic to use a precise match
(e.g., equals or regex for word boundaries) or explicitly exclude "불참" (check
opt.getOptionText().contains("불참") first) — alternatively prefer matching by
optionOrder (e.g., optionOrder == 1 for 참석, 2 for 불참) using the
VoteOptions.getOptionOrder() to reliably identify the 참석 option.
| // Request B (Another person same name) | ||
| PaymentRequest requestB = mock(PaymentRequest.class); | ||
| // We only mock requestA behavior to verify it matches AT LEAST ONE. |
There was a problem hiding this comment.
사용되지 않는 변수 requestB를 제거하세요.
requestB가 선언되었지만 테스트에서 사용되지 않습니다. 주석에 "Another person same name"이라고 설명되어 있지만, 실제로 mock 설정이나 검증에 사용되지 않으므로 혼란을 줄 수 있습니다. 동명이인 시나리오를 완전히 테스트하려면 requestB도 mock 설정을 추가하거나, 불필요하다면 제거하세요.
🔧 제안: 사용되지 않는 변수 제거
- // Request B (Another person same name)
- PaymentRequest requestB = mock(PaymentRequest.class);
- // We only mock requestA behavior to verify it matches AT LEAST ONE.
-
when(paymentRequestRepository.findMatchableRequests(clubId))
.thenReturn(List.of(requestA));📝 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.
| // Request B (Another person same name) | |
| PaymentRequest requestB = mock(PaymentRequest.class); | |
| // We only mock requestA behavior to verify it matches AT LEAST ONE. | |
| when(paymentRequestRepository.findMatchableRequests(clubId)) | |
| .thenReturn(List.of(requestA)); |
🤖 Prompt for AI Agents
In
`@backend/src/test/java/back/service/ledger/TransactionMatchingServiceTest.java`
around lines 122 - 124, 테스트 내에서 선언만 되고 사용되지 않는 PaymentRequest requestB 변수를 제거하거나
동명이인 시나리오를 실제로 검증하도록 변경하세요; 즉, TransactionMatchingServiceTest의 requestB 선언을 삭제하여
불필요한 mock을 제거하거나 requestB에 대한 mock 설정(예: when(...) 호출)과 해당 검증(assert/verify)을
추가해 requestA와 다른 사용자이지만 동일 이름인 케이스를 실제로 테스트하도록 만드세요.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@backend/src/main/java/back/service/ledger/EventFundService.java`:
- Around line 750-760: In EventFundService replace the incorrect
PaymentRequest.RequestType.SETTLEMENT used for additional fees with
PaymentRequest.RequestType.DEPOSIT so the created PaymentRequest (in the
PaymentRequest(...) call) matches deposit semantics and will be accepted by
TransactionMatchingService matching logic; locate the PaymentRequest
instantiation in EventFundService and change the RequestType enum value to
DEPOSIT (same approach as the earlier fee request creation that uses
RequestType.DEPOSIT).
In `@backend/src/main/java/back/service/ledger/TransactionMatchingService.java`:
- Around line 203-246: The current matching logic in TransactionMatchingService
allows non-unique realName/nick (variables realNameUnique, nickUnique, realName,
nick, content) to auto-match if content.contains(name); change this so only
exact matches or unique-contained matches return true, and remove/replace the
blocks that return true for non-unique includes (the two if-blocks that check
!realName.isBlank() && content.contains(realName) and !nick.isBlank() &&
content.contains(nick) near the end). Instead, flag these cases for manual
review (e.g., return a special status or set a “requiresManualReview” flag on
the match result) so non-unique-name contains do not produce automatic matches;
keep the logging but do not return true in those branches.
♻️ Duplicate comments (3)
backend/src/main/java/back/service/ledger/EventFundService.java (1)
437-443: 환급 처리에서 memberId를 userId로 사용 중입니다.
participantRepository.findByScheduleIdAndUserId(...)는 userId를 기대하지만originalReq.getMemberId()를 전달하고 있어 참가자 환급 상태가 업데이트되지 않습니다. 이전 리뷰에서 지적된 동일한 문제입니다.🔧 수정 제안
- Optional<ScheduleParticipants> participantOpt = participantRepository - .findByScheduleIdAndUserId(scheduleId, originalReq.getMemberId()); + // memberId -> userId 변환 + Long participantUserId = clubMemberRepository.findById(originalReq.getMemberId()) + .map(back.domain.club.ClubMembers::getUserId) + .orElse(null); + Optional<ScheduleParticipants> participantOpt = (participantUserId == null) + ? Optional.empty() + : participantRepository.findByScheduleIdAndUserId(scheduleId, participantUserId);backend/src/main/java/back/service/ledger/TransactionMatchingService.java (2)
199-201: 민감 정보가 콘솔에 그대로 노출됩니다Line 199-201에서 실명/닉네임/거래내용을
System.out.println으로 출력하고 있어 프로덕션 로그에 PII가 남습니다. 마스킹 + 로거로 전환하거나 제거가 필요합니다.🔒 로깅 전환 예시
+ private static final Logger log = LoggerFactory.getLogger(TransactionMatchingService.class); ... - System.out.println("멤버 실명: [" + realName + "], 닉네임: [" + nick + "]"); - System.out.println("거래 내용: [" + content + "]"); + log.trace("멤버 실명: [{}], 닉네임: [{}]", mask(realName), mask(nick)); + log.trace("거래 내용: [{}]", maskContent(content));
158-175: 실패 사유 판단에서도 동일 타임존 사용 필요Line 158-171에서 Asia/Seoul 기준으로 날짜를 변환했는데,
determineFailureReason()는 여전히toLocalDate()를 사용합니다. 자정 경계에서 매칭/실패 사유가 달라질 수 있습니다. 동일한 변환을 재사용하세요.🕒 일관된 타임존 변환
+ private static final ZoneId KOREA_ZONE = ZoneId.of("Asia/Seoul"); + + private LocalDate toKoreaDate(LocalDateTime dateTime) { + return dateTime.atZone(KOREA_ZONE).toLocalDate(); + } ... - ZoneId koreaZone = ZoneId.of("Asia/Seoul"); - LocalDate txDate = tx.getBankTransactionAt().atZone(koreaZone).toLocalDate(); + LocalDate txDate = toKoreaDate(tx.getBankTransactionAt()); ... - LocalDate txDate = tx.getBankTransactionAt().toLocalDate(); + LocalDate txDate = toKoreaDate(tx.getBankTransactionAt());
🧹 Nitpick comments (8)
backend/src/test/java/back/service/vote/VoteServiceTest.java (1)
503-513: 권한 예외 stubbing과 기대 예외가 충돌할 수 있음현재
assertAtLeastManager에doThrow(AuthStaffRequired)를 넣었는데, 테스트는AlreadyClosed를 기대합니다. 실제 서비스 로직이 권한 체크를 먼저 수행하면 이 테스트는 실패하거나 의도를 흐릴 수 있어요. 의도가 “종료 상태가 우선”이라면 권한 호출이 없음을 검증하거나 stubbing을 제거해 명확히 해주세요. 반대로 권한이 우선이라면 기대 예외를AuthStaffRequired로 바꾸는 편이 더 일관적입니다.제안 변경(종료 상태 우선 의도일 때)
- doThrow(new back.exception.ClubException.AuthStaffRequired()) - .when(clubsAuthorizationService).assertAtLeastManager(clubId, userId); - // when & then assertThatThrownBy(() -> voteService.answerVote(clubId, voteId, userId, request)) .isInstanceOf(VoteException.AlreadyClosed.class); + + then(clubsAuthorizationService).shouldHaveNoInteractions();backend/src/main/java/back/service/ledger/EventFundService.java (4)
61-70: 디버그용System.out.println대신 SLF4J 로거 사용을 권장합니다.이 파일 전체에 걸쳐
System.out.println과System.err.println이 디버깅 목적으로 사용되고 있습니다. 프로덕션 코드에서는 로그 레벨 제어와 구조화된 로깅을 위해 SLF4J 로거를 사용해야 합니다.♻️ 수정 제안
클래스 상단에 로거를 선언하고:
private static final Logger log = LoggerFactory.getLogger(EventFundService.class);그런 다음
System.out.println을 적절한 로그 레벨로 교체하세요:- System.out.println("💰 [참가비 요청 생성 시작] clubId=" + clubId + ", scheduleId=" + scheduleId + ", userId=" + userId); + log.debug("참가비 요청 생성 시작: clubId={}, scheduleId={}, userId={}", clubId, scheduleId, userId);
82-85: 참가비 부재 시NotFound예외 사용은 의미상 부적절합니다.일정은 조회되었으나 참가비가 없거나 0원인 경우입니다.
ScheduleException.NotFound보다 더 적합한 예외(예:InvalidEntryFee또는 별도의 비즈니스 예외)를 사용하거나, 단순히 early return 처리하는 것이 좋습니다.
503-689:previewSettlement와settleAndRefund간 중복 코드가 많습니다.지출 계산 로직(중복 제거, 날짜 범위 필터링, 환급 거래 제외)이 두 메서드에서 거의 동일하게 반복됩니다. 공통 로직을 private 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.
♻️ 리팩토링 방향
// 예시: 공통 로직 추출 private record SettlementCalculation( List<PaymentRequest> deduplicatedRequests, BigDecimal totalIncome, BigDecimal totalSpent, BigDecimal balance, BigDecimal refundPerPerson ) {} private SettlementCalculation calculateSettlement(Long clubId, Long scheduleId, Schedules schedule) { // 공통 계산 로직 }
468-470:bankAccountRepository.findByClubId()중복 호출.Line 396에서 이미 동일한 조회를 수행했습니다. 환급 처리와 잔액 귀속 처리에서
accountId를 재사용하도록 변수를 상위 스코프로 이동하면 불필요한 DB 조회를 줄일 수 있습니다.backend/src/test/java/back/service/ledger/EventFundServiceTest.java (3)
136-172: 과도한lenient()사용으로 테스트 안정성이 저하될 수 있습니다.대부분의 mock이
lenient()로 설정되어 있어, 실제로 호출되지 않는 mock 설정이 있어도 테스트가 통과합니다. 이는 잘못된 테스트 구성을 숨길 수 있습니다.필수적인 상호작용에는
given(...).willReturn(...)을 사용하고, 선택적인 상호작용에만lenient()를 제한적으로 사용하세요.
196-344: 새로 추가된previewSettlement와requestAdditionalFee메서드에 대한 테스트가 없습니다.
EventFundService에 추가된 주요 public 메서드들의 테스트 커버리지가 누락되어 있습니다. 특히:
previewSettlement: 정산 미리보기 계산 로직requestAdditionalFee: 추가회비 요청 생성 및 알림 발송이 메서드들의 핵심 시나리오에 대한 테스트 추가를 권장합니다.
261-266: 두 개의 환급 요청 중 첫 번째만 검증하고 있습니다.
times(2)로 저장을 확인하지만, 실제 assertion은captor.getAllValues().get(0)만 검증합니다. 두 번째 환급 요청도 올바르게 생성되었는지 확인하면 테스트 신뢰도가 높아집니다.
| PaymentRequest paymentRequest = new PaymentRequest( | ||
| clubId, | ||
| memberId, // userId 대신 memberId 사용 | ||
| user.getRealName(), | ||
| PaymentRequest.RequestType.SETTLEMENT, | ||
| request.amountPerPerson(), | ||
| expectedDate, | ||
| 10, | ||
| null, | ||
| scheduleId, | ||
| null); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Step 1: Find PaymentRequest class and its RequestType enum
echo "=== Finding PaymentRequest class ==="
fd -e java -type f | xargs grep -l "class PaymentRequest" | head -5
echo ""
echo "=== Searching for RequestType enum definition ==="
rg -A 10 "enum RequestType" --type javaRepository: kosa-02team/moida
Length of output: 1231
🏁 Script executed:
#!/bin/bash
# Step 2: Examine SETTLEMENT and DEPOSIT usage patterns
echo "=== SETTLEMENT type usages ==="
rg -n "RequestType\.SETTLEMENT" --type java -B 2 -A 2 | head -100
echo ""
echo "=== DEPOSIT type usages ==="
rg -n "RequestType\.DEPOSIT" --type java -B 2 -A 2 | head -100Repository: kosa-02team/moida
Length of output: 10020
🏁 Script executed:
#!/bin/bash
# Step 3: Check EventFundService around line 750 for context
echo "=== EventFundService lines 740-770 ==="
sed -n '740,770p' backend/src/main/java/back/service/ledger/EventFundService.javaRepository: kosa-02team/moida
Length of output: 1193
🏁 Script executed:
#!/bin/bash
# Step 4: Find method definition for requestAdditionalFee
echo "=== Looking for requestAdditionalFee method ==="
rg -n "requestAdditionalFee|추가회비" --type java -B 2 -A 5 | head -150Repository: kosa-02team/moida
Length of output: 2509
추가회비 요청에서 SETTLEMENT 타입 사용은 오류입니다.
추가회비는 멤버로부터 수금하는 입금(DEPOSIT)이므로 RequestType.DEPOSIT을 사용해야 합니다. 현재 SETTLEMENT 타입을 사용하면 TransactionMatchingService의 매칭 로직에서 명시적으로 거부됩니다:
if ("DEPOSIT".equalsIgnoreCase(txType)) {
// DEPOSIT 거래는 SETTLEMENT 타입을 거부
if (req.getRequestType() == PaymentRequest.RequestType.SETTLEMENT)
return false;
}
이로 인해 멤버의 은행 입금이 추가회비 요청과 자동 매칭되지 않습니다. 기존 회비 요청(line 152-156)과 동일하게 RequestType.DEPOSIT으로 변경하세요.
🤖 Prompt for AI Agents
In `@backend/src/main/java/back/service/ledger/EventFundService.java` around lines
750 - 760, In EventFundService replace the incorrect
PaymentRequest.RequestType.SETTLEMENT used for additional fees with
PaymentRequest.RequestType.DEPOSIT so the created PaymentRequest (in the
PaymentRequest(...) call) matches deposit semantics and will be accepted by
TransactionMatchingService matching logic; locate the PaymentRequest
instantiation in EventFundService and change the RequestType enum value to
DEPOSIT (same approach as the earlier fee request creation that uses
RequestType.DEPOSIT).
| // 5) 실명/닉네임이 클럽 내 유일한지 확인 | ||
| boolean realNameUnique = !realName.isBlank() | ||
| && clubMemberRepository.countByClubIdAndRealName(req.getClubId(), realNameRaw) == 1; | ||
|
|
||
| boolean nickUnique = !nick.isBlank() | ||
| && clubMemberRepository.countByClubIdAndClubNickname(req.getClubId(), nickRaw) == 1; | ||
|
|
||
| // 6) 유일한 경우에만 contains 허용 | ||
| if (realNameUnique && content.contains(realName)) | ||
| System.out.println(" 실명 유일성: " + realNameUnique + ", 닉네임 유일성: " + nickUnique); | ||
|
|
||
| // 6) 이름 매칭 로직 (완화된 버전) | ||
| // 6-1) 이름이 정확히 일치하면 매칭 (유일성 무관, 최우선) | ||
| if (!realName.isBlank() && content.equals(realName)) { | ||
| System.out.println(" ✓ 실명 정확 일치: [" + realName + "] == [" + content + "]"); | ||
| return true; | ||
| if (nickUnique && content.contains(nick)) | ||
| } | ||
| if (!nick.isBlank() && content.equals(nick)) { | ||
| System.out.println(" ✓ 닉네임 정확 일치: [" + nick + "] == [" + content + "]"); | ||
| return true; | ||
| } | ||
|
|
||
| if (nickUnique && content.contains(nick)) | ||
| // 6-2) 이름이 포함되어 있고 유일하면 매칭 | ||
| if (realNameUnique && !realName.isBlank() && content.contains(realName)) { | ||
| System.out.println(" ✓ 실명 포함 매칭 (유일): [" + realName + "]이(가) [" + content + "]에 포함됨"); | ||
| return true; | ||
| } | ||
| if (nickUnique && !nick.isBlank() && content.contains(nick)) { | ||
| System.out.println(" ✓ 닉네임 포함 매칭 (유일): [" + nick + "]이(가) [" + content + "]에 포함됨"); | ||
| return true; | ||
| } | ||
|
|
||
| // 6-3) 유일하지 않더라도 이름이 포함되어 있으면 매칭 허용 (금액과 날짜가 맞으면) | ||
| // 금액과 날짜가 이미 확인되었으므로, 이름만 포함되어 있으면 매칭 | ||
| if (!realName.isBlank() && content.contains(realName)) { | ||
| System.out.println(" ⚠️ 실명 포함되지만 유일하지 않음: [" + realName + "]이(가) [" + content + "]에 포함됨 (유일성: " | ||
| + realNameUnique + ")"); | ||
| System.out.println(" → 금액과 날짜가 일치하므로 매칭 허용"); | ||
| return true; | ||
| } | ||
| if (!nick.isBlank() && content.contains(nick)) { | ||
| System.out.println( | ||
| " ⚠️ 닉네임 포함되지만 유일하지 않음: [" + nick + "]이(가) [" + content + "]에 포함됨 (유일성: " + nickUnique + ")"); | ||
| System.out.println(" → 금액과 날짜가 일치하므로 매칭 허용"); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
비유일 이름 포함 매칭은 오매칭 위험
Line 233-246에서 유일하지 않은 실명/닉네임도 포함 매칭을 허용하면, 동명이인이나 공백/특수문자만 다른 이름에서 잘못 자동 매칭될 수 있습니다(정규화로 중복 가능성 ↑). 최소한 비유일 케이스는 수동 매칭으로 돌려주세요.
✅ 비유일 이름은 자동 매칭 제외
- if (!realName.isBlank() && content.contains(realName)) {
- System.out.println(" ⚠️ 실명 포함되지만 유일하지 않음: [" + realName + "]이(가) [" + content + "]에 포함됨 (유일성: "
- + realNameUnique + ")");
- System.out.println(" → 금액과 날짜가 일치하므로 매칭 허용");
- return true;
- }
- if (!nick.isBlank() && content.contains(nick)) {
- System.out.println(
- " ⚠️ 닉네임 포함되지만 유일하지 않음: [" + nick + "]이(가) [" + content + "]에 포함됨 (유일성: " + nickUnique + ")");
- System.out.println(" → 금액과 날짜가 일치하므로 매칭 허용");
- return true;
- }
+ if ((!realName.isBlank() && content.contains(realName) && !realNameUnique)
+ || (!nick.isBlank() && content.contains(nick) && !nickUnique)) {
+ System.out.println(" ✗ 이름 포함되나 유일하지 않음 → 수동 매칭 필요");
+ return false;
+ }🤖 Prompt for AI Agents
In `@backend/src/main/java/back/service/ledger/TransactionMatchingService.java`
around lines 203 - 246, The current matching logic in TransactionMatchingService
allows non-unique realName/nick (variables realNameUnique, nickUnique, realName,
nick, content) to auto-match if content.contains(name); change this so only
exact matches or unique-contained matches return true, and remove/replace the
blocks that return true for non-unique includes (the two if-blocks that check
!realName.isBlank() && content.contains(realName) and !nick.isBlank() &&
content.contains(nick) near the end). Instead, flag these cases for manual
review (e.g., return a special status or set a “requiresManualReview” flag on
the match result) so non-unique-name contains do not produce automatic matches;
keep the logging but do not return true in those branches.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@backend/src/test/java/back/bank/service/BankServiceTests.java`:
- Around line 176-201: The test syncTransactions_AutoDates_FirstTime uses
LocalDate.now() twice which can make the assertion flaky; capture the current
date once into a local variable (e.g., LocalDate today = LocalDate.now()) at the
start of the test and use today.minusDays(30) and today in the verify call to
bankProvider.getTransactions (and any other places in this test that use
LocalDate.now()) so both expected arguments use the same captured date.
| @Test | ||
| @DisplayName("거래내역 동기화 - 날짜 자동 (첫 동기화)") | ||
| void syncTransactions_AutoDates_FirstTime() { | ||
| Banks bank = mock(Banks.class); | ||
| Long clubId = 1L; | ||
|
|
||
| BankAccounts account = new BankAccounts(clubId, "STUB", 1L, bank, "110-1234", "홍길동"); | ||
|
|
||
| given(transactionLogRepository.findLatestByClubId(clubId)) | ||
| .willReturn(Optional.empty()); | ||
| given(bankAccountRepository.findByClubId(clubId)) | ||
| .willReturn(Optional.of(account)); | ||
| given(registry.get("STUB")) | ||
| .willReturn(bankProvider); | ||
| given(bankProvider.getTransactions(anyString(), any(), any())) | ||
| .willReturn(new java.util.ArrayList<>(List.of())); | ||
|
|
||
| bankService.syncTransactions(clubId, null, null); | ||
|
|
||
| triggerAfterCommit(); | ||
|
|
||
| verify(bankProvider).getTransactions( | ||
| anyString(), | ||
| eq(LocalDate.now().minusDays(30)), | ||
| eq(LocalDate.now())); | ||
| } |
There was a problem hiding this comment.
LocalDate.now() 이중 호출로 인한 테스트 불안정성 가능성이 있습니다.
동일 일자 기준 검증을 위해 날짜를 한 번만 캡처해 사용하세요.
✅ 제안 변경
- bankService.syncTransactions(clubId, null, null);
-
- triggerAfterCommit();
-
- verify(bankProvider).getTransactions(
- anyString(),
- eq(LocalDate.now().minusDays(30)),
- eq(LocalDate.now()));
+ LocalDate today = LocalDate.now();
+
+ bankService.syncTransactions(clubId, null, null);
+
+ triggerAfterCommit();
+
+ verify(bankProvider).getTransactions(
+ anyString(),
+ eq(today.minusDays(30)),
+ eq(today));📝 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.
| @Test | |
| @DisplayName("거래내역 동기화 - 날짜 자동 (첫 동기화)") | |
| void syncTransactions_AutoDates_FirstTime() { | |
| Banks bank = mock(Banks.class); | |
| Long clubId = 1L; | |
| BankAccounts account = new BankAccounts(clubId, "STUB", 1L, bank, "110-1234", "홍길동"); | |
| given(transactionLogRepository.findLatestByClubId(clubId)) | |
| .willReturn(Optional.empty()); | |
| given(bankAccountRepository.findByClubId(clubId)) | |
| .willReturn(Optional.of(account)); | |
| given(registry.get("STUB")) | |
| .willReturn(bankProvider); | |
| given(bankProvider.getTransactions(anyString(), any(), any())) | |
| .willReturn(new java.util.ArrayList<>(List.of())); | |
| bankService.syncTransactions(clubId, null, null); | |
| triggerAfterCommit(); | |
| verify(bankProvider).getTransactions( | |
| anyString(), | |
| eq(LocalDate.now().minusDays(30)), | |
| eq(LocalDate.now())); | |
| } | |
| `@Test` | |
| `@DisplayName`("거래내역 동기화 - 날짜 자동 (첫 동기화)") | |
| void syncTransactions_AutoDates_FirstTime() { | |
| Banks bank = mock(Banks.class); | |
| Long clubId = 1L; | |
| BankAccounts account = new BankAccounts(clubId, "STUB", 1L, bank, "110-1234", "홍길동"); | |
| given(transactionLogRepository.findLatestByClubId(clubId)) | |
| .willReturn(Optional.empty()); | |
| given(bankAccountRepository.findByClubId(clubId)) | |
| .willReturn(Optional.of(account)); | |
| given(registry.get("STUB")) | |
| .willReturn(bankProvider); | |
| given(bankProvider.getTransactions(anyString(), any(), any())) | |
| .willReturn(new java.util.ArrayList<>(List.of())); | |
| LocalDate today = LocalDate.now(); | |
| bankService.syncTransactions(clubId, null, null); | |
| triggerAfterCommit(); | |
| verify(bankProvider).getTransactions( | |
| anyString(), | |
| eq(today.minusDays(30)), | |
| eq(today)); | |
| } |
🤖 Prompt for AI Agents
In `@backend/src/test/java/back/bank/service/BankServiceTests.java` around lines
176 - 201, The test syncTransactions_AutoDates_FirstTime uses LocalDate.now()
twice which can make the assertion flaky; capture the current date once into a
local variable (e.g., LocalDate today = LocalDate.now()) at the start of the
test and use today.minusDays(30) and today in the verify call to
bankProvider.getTransactions (and any other places in this test that use
LocalDate.now()) so both expected arguments use the same captured date.
😉 연관 이슈
#107
🚀 작업 내용
장부를 고치고 추가회비를 활성화
Summary by CodeRabbit
새로운 기능
개선사항
수정
✏️ Tip: You can customize this high-level summary in your review settings.