Skip to content

[BE/FE] fix: 정산 오류 수정#121

Merged
pjm040525 merged 5 commits intomainfrom
fix/#120-fix
Jan 25, 2026
Merged

[BE/FE] fix: 정산 오류 수정#121
pjm040525 merged 5 commits intomainfrom
fix/#120-fix

Conversation

@warrockhali
Copy link
Copy Markdown
Collaborator

@warrockhali warrockhali commented Jan 25, 2026

😉 연관 이슈

#120

🚀 작업 내용

💬 리뷰 중점사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 클럽 커버 이미지 업로드 및 관리 기능 추가
    • 그룹 페이지에서 클럽 커버 이미지 표시
  • 개선 사항

    • 모든 회원이 장부 관리 기능에 접근 가능하도록 변경
    • 통장 관리 섹션 표시 범위 확대
    • 이미지 URL 처리 및 표시 로직 최적화

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

@warrockhali warrockhali self-assigned this Jan 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 25, 2026

📝 Walkthrough

개요

클럽 엔티티에 커버 이미지 기능을 추가했습니다. 백엔드에서는 데이터베이스 스키마에 cover_image_url 컬럼을 추가하고, 클럽 생성/수정 시 이미지 업로드 처리 로직을 통합했습니다. 프론트엔드에서는 로컬스토리지 기반 이미지 관리를 서버 제공 이미지 URL로 대체하고, 관리자 뷰와 수수료 뷰의 UI를 재구성했습니다.

변경사항

코호트 / 파일 변경 요약
백엔드 엔티티 및 DTO
backend/src/main/java/back/domain/club/Clubs.java, backend/src/main/java/back/dto/club/ClubRequest.java, backend/src/main/java/back/dto/club/ClubResponse.java
Clubs 엔티티에 coverImageUrl 필드 및 updateCoverImage() 메서드 추가; ClubRequest에 coverImage 필드, ClubResponse에 coverImageUrl 필드 및 매핑 로직 추가
백엔드 서비스 계층
backend/src/main/java/back/service/club/ClubService.java
ImageService 의존성 주입 추가; createClub 및 updateClub에서 Base64 이미지 저장 로직 통합
백엔드 데이터베이스
backend/src/main/resources/db/migration/V23__add_cover_image_url_to_clubs.sql
clubs 테이블에 cover_image_url 컬럼(VARCHAR(255)) 추가
백엔드 기타
backend/src/main/java/back/service/ledger/LedgerService.java
쿼리 파라미터 포장, 날짜 계산 로직 개선, 멤버명 소스 변경(헬퍼 메서드 → PaymentRequest 필드), 디버그 정보 제거 등 가독성 개선
프론트엔드 API 타입
frontend/src/api/club-full.ts
ClubDetailResponse에 coverImageUrl 필드, ClubCreateRequest/ClubUpdateRequest에 coverImage 필드 추가
프론트엔드 그룹 컴포넌트
frontend/src/app/components/group/GroupMainView.tsx
클럽 이미지 소스를 로컬스토리지에서 서버 제공 coverImageUrl로 변경; http:// 프로토콜 정규화 로직 추가
프론트엔드 관리자 뷰
frontend/src/app/components/group/admin/AdminView.tsx, frontend/src/app/components/group/admin/EditGroupView.tsx
AdminView에서 통장 관리 섹션 무조건 표시로 변경; EditGroupView에서 이미지 로컬스토리지 의존성 제거, 서버 coverImageUrl 사용
프론트엔드 수수료 뷰
frontend/src/app/components/group/dues/DuesView.tsx
장부 관리 카드를 관리자 섹션에서 공개 섹션으로 이동, 모든 회원이 접근 가능하도록 변경
프론트엔드 레이아웃
frontend/src/app/components/group/GroupLayout.tsx
마이너 포매팅 수정(공백 제거)

코드 리뷰 예상 난이도

🎯 3 (Moderate) | ⏱️ ~25 minutes

관련 PR

제안 레이블

fix, backend, frontend

제안 검토자

  • pjm040525
  • DooHyoJeong

축하 시

🐰 커버 이미지가 춤을 춘다네요,
서버에서 내려오는 예쁜 URL,
로컬스토리지는 이제 안녕,
클럽도 입고 나니 반짝반짝,
변경도 차곡차곡 잘 쌓인다! 📸✨

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 템플릿 구조는 유지하지만, '작업 내용'과 '리뷰 중점사항' 섹션이 완전히 비어 있어서 변경사항에 대한 구체적인 정보를 제공하지 못합니다. '작업 내용' 섹션에 클럽 이미지 필드 추가, 이미지 서비스 통합 등의 주요 변경사항을 작성하고, '리뷰 중점사항'에 검토 포인트를 추가하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 제목은 '정산 오류 수정'이라고만 표기되어 있어서 매우 모호하며, 실제 변경사항(이미지 필드 추가, 마이그레이션, 프론트엔드 이미지 처리)을 구체적으로 전달하지 못합니다. 제목을 더 구체적으로 변경하세요. 예: '[BE/FE] feat: 클럽 커버 이미지 필드 추가 및 이미지 처리 개선' 또는 실제 변경 내용을 정확히 반영하는 제목을 사용하세요.

✏️ 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.

@pjm040525 pjm040525 merged commit b445e6d into main Jan 25, 2026
2 of 3 checks passed
@pjm040525 pjm040525 deleted the fix/#120-fix branch January 25, 2026 10:47
Copy link
Copy Markdown

@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: 5

🤖 Fix all issues with AI agents
In `@frontend/src/app/components/group/admin/AdminView.tsx`:
- Around line 489-526: Restore frontend restriction in AdminView.tsx by showing
the "통장 관리" / account button only when permissions.canWithdraw is true (wrap the
Link/button render or conditionally render the Account block around
handleShowAccount and the account view); additionally, update backend
BankAccountController endpoints (createAccount, syncTransactions,
refundToMember, getAccount) to include
`@PreAuthorize`("@clubMemberService.isOwnerOrTreasurer(`#clubId`, principal)") (or
equivalent) on each method to enforce owner/treasurer-only access, and
re-evaluate whether getAccount should remain restricted. Ensure the unique
symbols referenced: AdminView.tsx, handleShowAccount, permissions.canWithdraw,
BankAccountController, and the four endpoints POST
/api/clubs/{clubId}/bank/accounts, POST .../bank/sync, POST .../bank/refund, GET
.../bank/account are all updated accordingly.

In `@frontend/src/app/components/group/admin/EditGroupView.tsx`:
- Around line 47-54: The code in EditGroupView.tsx constructs a server image URL
by hardcoding "http://localhost:8080"; replace this with the shared utility used
in GroupMainView.tsx (e.g., a common function like getApiBaseUrl or
buildServerUrl in a utils module) so that the club.coverImageUrl logic (the
conditional using club.coverImageUrl.startsWith('http')) uses that utility to
prepend the base URL instead of the hardcoded string; update the setImage call
to use the result from that utility.
- Around line 114-120: The bug is that EditGroupView is sending the
server-provided image URL back as coverImage which breaks
ImageService.saveBase64Image; modify the request construction around updateClub
to only include coverImage when the local image state is a newly uploaded base64
data URL (e.g., starts with "data:" or matches a base64 pattern) and omit
coverImage when image is a server URL (e.g., starts with "/" or "http"), by
building ClubUpdateRequest conditionally from the image state before calling
updateClub; reference the image state, ClubUpdateRequest, and updateClub to
locate the change.

In `@frontend/src/app/components/group/dues/DuesView.tsx`:
- Around line 177-195: SecurityConfig currently permits GET /api/clubs/**
broadly and LedgerController's endpoints (the `@GetMapping` that returns ledger
data, `@PostMapping`("/manual"), and `@PatchMapping` methods) lack membership
checks; update SecurityConfig to require authentication for club endpoints and
add membership verification in LedgerController by validating that the
authenticated user is a member of the club identified by the clubId parameter
before performing any read or write (either call an existing membership
service/repository method inside each controller method or apply a `@PreAuthorize`
expression that invokes a membership-checking bean), and ensure all controller
methods that accept clubId perform this check and return 403 if the user is not
a member.

In `@frontend/src/app/components/group/GroupMainView.tsx`:
- Around line 116-121: The image src construction in GroupMainView.tsx currently
hardcodes "http://localhost:8080" when club.coverImageUrl is not absolute;
change this to read the API base URL from an environment/config variable (e.g.,
process.env.REACT_APP_API_BASE_URL or import from a central config) and use that
instead of the literal string when building the src for club.coverImageUrl (the
JSX where src={club.coverImageUrl.startsWith('http') ? ...}). Also ensure a
sensible fallback if the env var is missing and normalize joining (avoid double
slashes) so <img src=... alt={club.clubName} /> works in production.
🧹 Nitpick comments (6)
backend/src/main/java/back/service/ledger/LedgerService.java (2)

62-69: N+1 쿼리 잠재적 성능 이슈

getActualTransactionDate(log)가 반복문 내에서 호출되어 각 로그마다 bankTransactionHistoryRepository.findById()를 개별 호출합니다. 아래 76-99 라인에서는 배치 조회(findAllById)를 사용하고 있으므로, 필터링 로직도 동일한 방식으로 개선할 수 있습니다.

♻️ 개선 제안: 필터링 전에 배치 조회 활용
        } else {
            // 넓은 범위로 조회 (동기화 지연 고려)
            LocalDateTime queryStart = startDate.atStartOfDay().minusDays(7);
            LocalDateTime queryEnd = endDate.atTime(23, 59, 59).plusDays(7);

            logs = transactionLogRepository.findByClubIdAndCreatedAtBetweenOrderByCreatedAtDescTransactionIdDesc(clubId,
                    queryStart, queryEnd);
            System.out
                    .println("  → 날짜 범위 조회 (전체): " + logs.size() + "건 (조회 범위: " + queryStart + " ~ " + queryEnd + ")");

+            // 필터링용 히스토리 날짜 미리 조회 (N+1 방지)
+            List<Long> historyIdsForFilter = logs.stream()
+                    .map(TransactionLog::getBankHistoryId)
+                    .filter(id -> id != null)
+                    .distinct()
+                    .collect(Collectors.toList());
+            Map<Long, LocalDateTime> historyDateMapForFilter = bankTransactionHistoryRepository.findAllById(historyIdsForFilter).stream()
+                    .collect(Collectors.toMap(
+                            BankTransactionHistory::getHistoryId,
+                            BankTransactionHistory::getBankTransactionAt));
+
            // 실제 거래 날짜 기준으로 필터링
            List<TransactionLog> filteredLogs = new ArrayList<>();
            LocalDateTime filterStart = startDate.atStartOfDay();
            LocalDateTime filterEnd = endDate.atTime(23, 59, 59);

            for (TransactionLog log : logs) {
-                LocalDateTime actualTransactionDate = getActualTransactionDate(log);
+                LocalDateTime actualTransactionDate = log.getCreatedAt();
+                if (log.getBankHistoryId() != null && historyDateMapForFilter.containsKey(log.getBankHistoryId())) {
+                    actualTransactionDate = historyDateMapForFilter.get(log.getBankHistoryId());
+                }

                // 실제 거래 날짜가 필터 기간 내에 있는지 확인
                if (!actualTransactionDate.isBefore(filterStart) && !actualTransactionDate.isAfter(filterEnd)) {
                    filteredLogs.add(log);
                }
            }

147-151: 미사용 메서드 및 의존성 확인

getMemberName 메서드와 clubMemberRepository 의존성이 더 이상 사용되지 않습니다. 의도적인 변경인 경우, 추후 정리 작업으로 제거를 고려해 주세요.

backend/src/main/java/back/dto/club/ClubRequest.java (1)

36-37: coverImage 필드에 유효성 검사 추가를 고려하세요.

다른 필드들과 달리 coverImage에는 유효성 검사 어노테이션이 없습니다. Base64 이미지 데이터를 받는 경우, 크기 제한이 없으면 매우 큰 페이로드로 인해 메모리 문제나 DoS 공격에 취약할 수 있습니다.

♻️ 제안된 수정
+    `@Size`(max = 5242880, message = "이미지 크기는 5MB를 초과할 수 없습니다.")
     private String coverImage;

또는 서비스 레이어에서 이미지 크기 검증이 이루어지고 있는지 확인해 주세요.

backend/src/main/java/back/domain/club/Clubs.java (1)

56-58: 엔티티 컬럼 길이 명시를 고려하세요.

마이그레이션에서 VARCHAR(255)로 정의되어 있으나, 엔티티의 @Column 어노테이션에는 길이가 명시되어 있지 않습니다. 일관성을 위해 길이를 명시하는 것이 좋습니다.

♻️ 제안된 수정
-    `@Column`(name = "cover_image_url")
+    `@Column`(name = "cover_image_url", length = 255)
     private String coverImageUrl;
frontend/src/app/components/group/GroupMainView.tsx (1)

232-234: 이미지 URL 정규화 로직이 중복됩니다.

위의 club cover image와 동일한 URL 정규화 패턴이 반복됩니다. 유틸리티 함수로 추출하여 재사용하면 유지보수성이 향상됩니다.

♻️ 유틸리티 함수 적용
 <img
-  src={post.imagesUrl[0].startsWith('http') ? post.imagesUrl[0] : `http://localhost:8080${post.imagesUrl[0]}`}
+  src={normalizeImageUrl(post.imagesUrl[0])}
   alt=""
   className="w-full h-full object-cover"
backend/src/main/java/back/service/club/ClubService.java (1)

140-143: 기존 커버 이미지 삭제 처리가 누락되었습니다.

새 커버 이미지를 업로드할 때 기존 이미지 파일이 삭제되지 않아 스토리지 누수가 발생할 수 있습니다. 장기적으로 불필요한 파일이 쌓일 수 있습니다.

♻️ 기존 이미지 삭제 로직 추가 제안
 if (request.getCoverImage() != null && !request.getCoverImage().isEmpty()) {
+    // 기존 이미지 삭제 (선택적)
+    if (club.getCoverImageUrl() != null) {
+        imageService.deleteImage(club.getCoverImageUrl());
+    }
     String imageUrl = imageService.saveBase64Image(request.getCoverImage());
     club.updateCoverImage(imageUrl);
 }

Comment on lines +489 to +526
{/* 통장 관리 - 모든 멤버 */}
<div className="bg-white rounded-xl border border-stone-100 divide-y divide-stone-50 overflow-hidden">
<div className="px-4 py-3 bg-stone-50">
<h3 className="font-medium text-stone-700">통장 관리</h3>
</div>

{/* 장부 관리 */}
<Link to="../dues/ledger" className="block">
<div className="p-4 flex items-center justify-between hover:bg-stone-50 cursor-pointer">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 text-indigo-600 rounded-lg">
<BookOpen className="w-5 h-5" />
</div>
<div>
<p className="font-medium text-stone-900">장부 관리</p>
<p className="text-xs text-stone-500">모임통장 사용 내역 조회</p>
</div>
{/* 장부 관리 */}
<Link to="../dues/ledger" className="block">
<div className="p-4 flex items-center justify-between hover:bg-stone-50 cursor-pointer">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-100 text-indigo-600 rounded-lg">
<BookOpen className="w-5 h-5" />
</div>
<div>
<p className="font-medium text-stone-900">장부 관리</p>
<p className="text-xs text-stone-500">모임통장 사용 내역 조회</p>
</div>
<ChevronRight className="w-5 h-5 text-stone-300" />
</div>
</Link>

{/* 계좌 */}
<button onClick={handleShowAccount} className="block w-full text-left">
<div className="p-4 flex items-center justify-between hover:bg-stone-50 cursor-pointer">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 text-blue-600 rounded-lg">
<Wallet className="w-5 h-5" />
</div>
<div>
<p className="font-medium text-stone-900">계좌 확인</p>
<p className="text-xs text-stone-500">모임통장 계좌 정보 및 사용 안내</p>
</div>
<ChevronRight className="w-5 h-5 text-stone-300" />
</div>
</Link>

{/* 계좌 */}
<button onClick={handleShowAccount} className="block w-full text-left">
<div className="p-4 flex items-center justify-between hover:bg-stone-50 cursor-pointer">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 text-blue-600 rounded-lg">
<Wallet className="w-5 h-5" />
</div>
<div>
<p className="font-medium text-stone-900">계좌 확인</p>
<p className="text-xs text-stone-500">모임통장 계좌 정보 및 사용 안내</p>
</div>
<ChevronRight className="w-5 h-5 text-stone-300" />
</div>
</button>
</div>
)}
<ChevronRight className="w-5 h-5 text-stone-300" />
</div>
</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for bank account related endpoints and authorization
rg -n -A 10 'getBankAccount|createBankAccount|bankAccount' --type java -g '!*Test*' | head -100

Repository: kosa-02team/moida

Length of output: 10328


🏁 Script executed:

# Find handleShowAccount and handleCreateAccount implementations
rg -n -B 2 -A 15 'handleShowAccount|handleCreateAccount' --type typescript --type tsx frontend/

Repository: kosa-02team/moida

Length of output: 95


🏁 Script executed:

# Search for API calls related to bank account in the frontend
rg -n 'account.*api|api.*account|bankAccount.*api' --type typescript --type tsx frontend/src/ | head -50

Repository: kosa-02team/moida

Length of output: 95


🏁 Script executed:

# Check the PR or commit message for context about intentional changes
git log --oneline --all -20 | head -20

Repository: kosa-02team/moida

Length of output: 945


🏁 Script executed:

# Look for any related permission or authorization checks
rg -n -B 3 -A 3 'canWithdraw|withdraw' frontend/src/app/components/group/admin/AdminView.tsx

Repository: kosa-02team/moida

Length of output: 665


🏁 Script executed:

# Find bank account controller with authorization
rg -n -B 2 -A 8 '@PreAuthorize|@Secured|@RolesAllowed' backend/src/main/java/back/bank --type java | grep -A 10 'bankAccount\|getBankAccount\|createAccount\|account'

Repository: kosa-02team/moida

Length of output: 43


🏁 Script executed:

# Find bank controller file
fd -e java bank backend/src/main/java -type f | grep -i controller

Repository: kosa-02team/moida

Length of output: 231


🏁 Script executed:

# Search for handleShowAccount in the codebase
rg -n 'handleShowAccount\|handleCreateAccount' frontend/

Repository: kosa-02team/moida

Length of output: 43


🏁 Script executed:

# Check if canWithdraw gates the 통장 관리 section - search AdminView for conditional rendering
rg -n -B 5 -A 5 '통장 관리' frontend/src/app/components/group/admin/AdminView.tsx

Repository: kosa-02team/moida

Length of output: 617


🏁 Script executed:

# Search for recent commit details about permission changes
git show 71337f6 --stat 2>/dev/null | head -30

Repository: kosa-02team/moida

Length of output: 1786


🏁 Script executed:

# Find and examine BankAccountController
find backend -name '*BankAccountController*' -type f

Repository: kosa-02team/moida

Length of output: 130


🏁 Script executed:

# Search for GET account endpoint with authorization
rg -n -B 3 -A 10 'getAccount|getBankAccount|showAccount' backend/src/main/java/back/bank/controller --max-count 50

Repository: kosa-02team/moida

Length of output: 1629


🏁 Script executed:

# Look for authorization annotations in the entire controller
rg -n '@PreAuthorize|@Secured|hasRole|hasAuthority' backend/src/main/java/back/bank/controller --max-count 30

Repository: kosa-02team/moida

Length of output: 43


🏁 Script executed:

# Check if there's method-level security in the service layer
rg -n '@PreAuthorize|@Secured|hasRole' backend/src/main/java/back/bank/service --max-count 20

Repository: kosa-02team/moida

Length of output: 43


🏁 Script executed:

# Verify the exact changes in AdminView.tsx from the commit
git show 71337f6:frontend/src/app/components/group/admin/AdminView.tsx 2>/dev/null | grep -n -B 5 -A 15 '통장 관리' | head -40

Repository: kosa-02team/moida

Length of output: 1198


🏁 Script executed:

# Check all endpoints in BankAccountController for authorization
rg -n -B 2 -A 15 '@PostMapping|@PutMapping|@DeleteMapping' backend/src/main/java/back/bank/controller/BankAccountController.java

Repository: kosa-02team/moida

Length of output: 2809


🏁 Script executed:

# Check if there's authorization in BankAccountController class level
cat backend/src/main/java/back/bank/controller/BankAccountController.java | head -50

Repository: kosa-02team/moida

Length of output: 2282


🏁 Script executed:

# Search for createAccount or any write operations with authorization
rg -n -B 2 -A 10 'createAccount|createBankAccount|registerAccount' backend/src/main/java/back/bank/controller/

Repository: kosa-02team/moida

Length of output: 1819


🏁 Script executed:

# Check the request mapping and controller initialization
rg -n '@RestController|@RequestMapping|@PreAuthorize' backend/src/main/java/back/bank/controller/BankAccountController.java

Repository: kosa-02team/moida

Length of output: 126


🏁 Script executed:

# Look for authorization checks in related services
rg -n '@PreAuthorize|authoriz' backend/src/main/java/back/bank/service/BankService.java

Repository: kosa-02team/moida

Length of output: 43


백엔드 권한 검증 부재로 인한 보안 위험 - 긴급 수정 필요

통장 관리 섹션의 권한 체크가 전면 제거되었습니다. 이전 버전(commit 71337f6)에서는 permissions.canWithdraw 조건으로 owner/treasurer만 접근 가능했으나, 현재는 모든 멤버에게 노출되어 있습니다.

더 심각한 문제는 백엔드 엔드포인트에 권한 검증이 없다는 점입니다:

  • POST /api/clubs/{clubId}/bank/accounts (계좌 생성) - 권한 체크 없음
  • POST /api/clubs/{clubId}/bank/sync (거래 동기화) - 권한 체크 없음
  • POST /api/clubs/{clubId}/bank/refund (환불) - 권한 체크 없음
  • GET /api/clubs/{clubId}/bank/account (계좌 조회) - 권한 체크 없음

BankAccountController의 모든 엔드포인트에서 @PreAuthorize 또는 유사한 권한 검증 장식자가 누락되어 있습니다. 이로 인해 모든 멤버가 계좌 정보 조회, 계좌 생성, 거래 동기화, 환불 등의 민감한 작업을 수행할 수 있습니다.

필수 수정 사항:

  1. 민감한 엔드포인트에 @PreAuthorize("@clubMemberService.isOwnerOrTreasurer(#clubId, principal)") 추가 (createAccount, syncTransactions, refundToMember 등)
  2. 프론트엔드에서도 permissions.canWithdraw 조건 복구
  3. 조회 권한 정책 재검토 (계좌 정보 조회도 제한 필요한지 확인)
🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/admin/AdminView.tsx` around lines 489 -
526, Restore frontend restriction in AdminView.tsx by showing the "통장 관리" /
account button only when permissions.canWithdraw is true (wrap the Link/button
render or conditionally render the Account block around handleShowAccount and
the account view); additionally, update backend BankAccountController endpoints
(createAccount, syncTransactions, refundToMember, getAccount) to include
`@PreAuthorize`("@clubMemberService.isOwnerOrTreasurer(`#clubId`, principal)") (or
equivalent) on each method to enforce owner/treasurer-only access, and
re-evaluate whether getAccount should remain restricted. Ensure the unique
symbols referenced: AdminView.tsx, handleShowAccount, permissions.canWithdraw,
BankAccountController, and the four endpoints POST
/api/clubs/{clubId}/bank/accounts, POST .../bank/sync, POST .../bank/refund, GET
.../bank/account are all updated accordingly.

Comment on lines +47 to 54

// 서버 이미지 URL 사용
if (club.coverImageUrl) {
const imageUrl = club.coverImageUrl.startsWith('http')
? club.coverImageUrl
: `http://localhost:8080${club.coverImageUrl}`;
setImage(imageUrl);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

동일한 하드코딩 localhost URL 문제가 있습니다.

GroupMainView.tsx에서 언급한 것과 동일하게 http://localhost:8080이 하드코딩되어 있습니다. 공통 유틸리티로 추출하여 사용하세요.

🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/admin/EditGroupView.tsx` around lines 47 -
54, The code in EditGroupView.tsx constructs a server image URL by hardcoding
"http://localhost:8080"; replace this with the shared utility used in
GroupMainView.tsx (e.g., a common function like getApiBaseUrl or buildServerUrl
in a utils module) so that the club.coverImageUrl logic (the conditional using
club.coverImageUrl.startsWith('http')) uses that utility to prepend the base URL
instead of the hardcoded string; update the setImage call to use the result from
that utility.

Comment on lines 114 to +120
try {
const request: ClubUpdateRequest = {
clubName: name.trim(),
coverImage: image || undefined,
};
await updateClub(Number(groupId), request);

// 모임 이미지를 로컬 스토리지에 저장
if (image && groupId) {
localStorage.setItem(`club_image_${groupId}`, image);
} else if (!image && groupId) {
// 이미지가 제거된 경우 localStorage에서도 삭제
localStorage.removeItem(`club_image_${groupId}`);
}


Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

서버에서 받은 이미지 URL을 coverImage로 다시 전송하면 문제가 발생할 수 있습니다.

image 상태가 서버에서 받은 URL(예: /uploads/...)일 때 이를 coverImage로 전송하면, 백엔드에서 base64가 아닌 URL 문자열을 받게 됩니다. ImageService.saveBase64Image()에서 파싱 오류가 발생할 수 있습니다.

이미지가 변경되지 않았을 때는 coverImage를 전송하지 않거나, 새로 업로드된 base64 이미지만 전송하도록 수정이 필요합니다.

🔧 이미지 변경 여부 추적 제안
 const [image, setImage] = useState<string | null>(null);
+const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null);
+const [imageChanged, setImageChanged] = useState(false);

 // useEffect에서 서버 이미지 로드 시
 if (club.coverImageUrl) {
   const imageUrl = ...;
   setImage(imageUrl);
+  setOriginalImageUrl(imageUrl);
 }

 // handleImageUpload에서
 reader.onload = (event) => {
   const result = event.target?.result as string;
   if (result) {
     setImage(result);
+    setImageChanged(true);
   }
 };

 // handleSubmit에서
 const request: ClubUpdateRequest = {
   clubName: name.trim(),
-  coverImage: image || undefined,
+  coverImage: imageChanged ? image || undefined : undefined,
 };
🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/admin/EditGroupView.tsx` around lines 114 -
120, The bug is that EditGroupView is sending the server-provided image URL back
as coverImage which breaks ImageService.saveBase64Image; modify the request
construction around updateClub to only include coverImage when the local image
state is a newly uploaded base64 data URL (e.g., starts with "data:" or matches
a base64 pattern) and omit coverImage when image is a server URL (e.g., starts
with "/" or "http"), by building ClubUpdateRequest conditionally from the image
state before calling updateClub; reference the image state, ClubUpdateRequest,
and updateClub to locate the change.

Comment on lines +177 to +195
{/* 장부 관리 Link (모든 멤버 가능) */}
<Link to="ledger">
<Card className="border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition-colors cursor-pointer">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
<History className="w-5 h-5 text-indigo-600" />
</div>
<div>
<p className="font-medium text-indigo-900">장부 관리</p>
<p className="text-xs text-indigo-700">모임통장 사용 내역 조회</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-indigo-600" />
</div>
</CardContent>
</Card>
</Link>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 장부(ledger) 관련 백엔드 엔드포인트의 권한 검사 로직 확인

echo "=== Step 1: Ledger 관련 Java 파일 검색 ==="
fd -e java -i ledger

echo ""
echo "=== Step 2: Ledger 관련 파일의 권한 검사 어노테이션 확인 ==="
rg -n "@PreAuthorize|@Secured|@RequiredRole|hasRole|hasAuthority|권한|permission|Permission" --type=java -i ledger -A 3

echo ""
echo "=== Step 3: Ledger 컨트롤러 GET 메서드 검색 ==="
rg -n "class.*Ledger|@GetMapping|@RequestMapping" --type=java -i ledger -A 2

echo ""
echo "=== Step 4: Spring Security 관련 설정 파일 확인 ==="
fd -e java -e yml -e yaml -i security | head -20

Repository: kosa-02team/moida

Length of output: 530


🏁 Script executed:

#!/bin/bash
# Check LedgerController implementation
echo "=== LedgerController.java ==="
cat backend/src/main/java/back/controller/ledger/LedgerController.java

echo ""
echo "=== LedgerService.java ==="
cat backend/src/main/java/back/service/ledger/LedgerService.java

Repository: kosa-02team/moida

Length of output: 12513


🏁 Script executed:

#!/bin/bash
# Check SecurityConfig for URL-level authorization rules
echo "=== SecurityConfig.java ==="
cat backend/src/main/java/back/config/security/SecurityConfig.java

Repository: kosa-02team/moida

Length of output: 3679


백엔드 권한 검증 로직 누락 - 긴급 수정 필요

장부 조회 엔드포인트에서 심각한 권한 검증 누락이 발견되었습니다:

  1. SecurityConfig의 권한 설정: GET /api/clubs/**permitAll()로 설정되어 있어 인증되지 않은 모든 사용자가 어떤 모임의 장부에도 접근할 수 있습니다.

  2. LedgerController 검증 부재: @GetMapping 메서드에서 사용자가 해당 모임의 멤버인지 확인하는 로직이 없습니다. @PostMapping("/manual")@PatchMapping 메서드도 마찬가지입니다.

프론트엔드에서 "모든 멤버 접근 가능"으로 변경했지만, 백엔드는 실제로 멤버십을 검증하지 않아 모든 인증된 사용자가 모든 모임의 장부에 접근할 수 있는 상태입니다.

필수 조치:

  • LedgerController의 모든 엔드포인트에서 clubId 파라미터로 접근하는 사용자가 해당 모임의 멤버인지 검증 추가
  • 멤버십 검증 로직 구현 또는 @PreAuthorize 어노테이션을 통한 권한 제어 추가
🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/dues/DuesView.tsx` around lines 177 - 195,
SecurityConfig currently permits GET /api/clubs/** broadly and
LedgerController's endpoints (the `@GetMapping` that returns ledger data,
`@PostMapping`("/manual"), and `@PatchMapping` methods) lack membership checks;
update SecurityConfig to require authentication for club endpoints and add
membership verification in LedgerController by validating that the authenticated
user is a member of the club identified by the clubId parameter before
performing any read or write (either call an existing membership
service/repository method inside each controller method or apply a `@PreAuthorize`
expression that invokes a membership-checking bean), and ensure all controller
methods that accept clubId perform this check and return 403 if the user is not
a member.

Comment on lines +116 to +121
return club.coverImageUrl ? (
<img
src={club.coverImageUrl.startsWith('http') ? club.coverImageUrl : `http://localhost:8080${club.coverImageUrl}`}
alt={club.clubName}
className="w-full h-full object-cover rounded-2xl"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

하드코딩된 localhost URL은 프로덕션 환경에서 문제가 됩니다.

http://localhost:8080이 하드코딩되어 있어 프로덕션 배포 시 이미지가 로드되지 않습니다. 환경 변수나 설정 파일에서 API base URL을 가져오도록 수정해야 합니다.

🔧 환경 변수 사용 제안
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
+
+const normalizeImageUrl = (url: string): string => {
+  return url.startsWith('http') ? url : `${API_BASE_URL}${url}`;
+};
+
 return club.coverImageUrl ? (
   <img
-    src={club.coverImageUrl.startsWith('http') ? club.coverImageUrl : `http://localhost:8080${club.coverImageUrl}`}
+    src={normalizeImageUrl(club.coverImageUrl)}
     alt={club.clubName}
     className="w-full h-full object-cover rounded-2xl"
   />
🤖 Prompt for AI Agents
In `@frontend/src/app/components/group/GroupMainView.tsx` around lines 116 - 121,
The image src construction in GroupMainView.tsx currently hardcodes
"http://localhost:8080" when club.coverImageUrl is not absolute; change this to
read the API base URL from an environment/config variable (e.g.,
process.env.REACT_APP_API_BASE_URL or import from a central config) and use that
instead of the literal string when building the src for club.coverImageUrl (the
JSX where src={club.coverImageUrl.startsWith('http') ? ...}). Also ensure a
sensible fallback if the env var is missing and normalize joining (avoid double
slashes) so <img src=... alt={club.clubName} /> works in production.

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.

2 participants