Conversation
📝 Walkthrough개요클럽 엔티티에 커버 이미지 기능을 추가했습니다. 백엔드에서는 데이터베이스 스키마에 변경사항
코드 리뷰 예상 난이도🎯 3 (Moderate) | ⏱️ ~25 minutes 관련 PR
제안 레이블
제안 검토자
축하 시
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ 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: 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); }
| {/* 통장 관리 - 모든 멤버 */} | ||
| <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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for bank account related endpoints and authorization
rg -n -A 10 'getBankAccount|createBankAccount|bankAccount' --type java -g '!*Test*' | head -100Repository: 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 -50Repository: 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 -20Repository: 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.tsxRepository: 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 controllerRepository: 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.tsxRepository: 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 -30Repository: kosa-02team/moida
Length of output: 1786
🏁 Script executed:
# Find and examine BankAccountController
find backend -name '*BankAccountController*' -type fRepository: 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 50Repository: 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 30Repository: 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 20Repository: 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 -40Repository: 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.javaRepository: 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 -50Repository: 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.javaRepository: 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.javaRepository: 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 또는 유사한 권한 검증 장식자가 누락되어 있습니다. 이로 인해 모든 멤버가 계좌 정보 조회, 계좌 생성, 거래 동기화, 환불 등의 민감한 작업을 수행할 수 있습니다.
필수 수정 사항:
- 민감한 엔드포인트에
@PreAuthorize("@clubMemberService.isOwnerOrTreasurer(#clubId, principal)")추가 (createAccount, syncTransactions, refundToMember 등) - 프론트엔드에서도
permissions.canWithdraw조건 복구 - 조회 권한 정책 재검토 (계좌 정보 조회도 제한 필요한지 확인)
🤖 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.
|
|
||
| // 서버 이미지 URL 사용 | ||
| if (club.coverImageUrl) { | ||
| const imageUrl = club.coverImageUrl.startsWith('http') | ||
| ? club.coverImageUrl | ||
| : `http://localhost:8080${club.coverImageUrl}`; | ||
| setImage(imageUrl); | ||
| } |
There was a problem hiding this comment.
동일한 하드코딩 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.
| 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}`); | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
서버에서 받은 이미지 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.
| {/* 장부 관리 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> |
There was a problem hiding this comment.
🧩 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 -20Repository: 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.javaRepository: 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.javaRepository: kosa-02team/moida
Length of output: 3679
백엔드 권한 검증 로직 누락 - 긴급 수정 필요
장부 조회 엔드포인트에서 심각한 권한 검증 누락이 발견되었습니다:
-
SecurityConfig의 권한 설정:
GET /api/clubs/**는permitAll()로 설정되어 있어 인증되지 않은 모든 사용자가 어떤 모임의 장부에도 접근할 수 있습니다. -
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.
| 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" | ||
| /> |
There was a problem hiding this comment.
하드코딩된 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.
😉 연관 이슈
#120
🚀 작업 내용
💬 리뷰 중점사항
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.