Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.be.sportizebe.domain.notification.controller;

import com.be.sportizebe.domain.notification.dto.request.JoinClubApplicationRequest;
import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestResponse;
import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestSummaryResponse;
import com.be.sportizebe.domain.notification.service.JoinClubRequestService;
import com.be.sportizebe.global.cache.dto.UserAuthInfo;
import com.be.sportizebe.global.response.BaseResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -27,12 +31,22 @@ public class JoinClubRequestController {
@Operation(summary = "가입 신청", description = "동호회에 가입을 신청합니다. 동호회장에게 알림이 전송됩니다.")
public ResponseEntity<BaseResponse<JoinClubRequestResponse>> requestJoin(
@Parameter(description = "동호회 ID") @PathVariable Long clubId,
@Valid @RequestBody JoinClubApplicationRequest request,
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
JoinClubRequestResponse response = joinClubRequestService.requestJoin(clubId, userAuthInfo.getId());
JoinClubRequestResponse response = joinClubRequestService.requestJoin(clubId, userAuthInfo.getId(), request.message());
return ResponseEntity.status(HttpStatus.CREATED)
.body(BaseResponse.success("가입 신청 완료", response));
}

@GetMapping("/join-requests/{requestId}")
@Operation(summary = "가입 신청 단건 조회", description = "가입 신청 상세 정보를 조회합니다. 동호회장 또는 신청자 본인만 가능합니다.")
public ResponseEntity<BaseResponse<JoinClubRequestResponse>> getJoinRequest(
@Parameter(description = "가입 신청 ID") @PathVariable Long requestId,
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
JoinClubRequestResponse response = joinClubRequestService.getJoinRequest(requestId, userAuthInfo.getId());
return ResponseEntity.ok(BaseResponse.success("가입 신청 조회 성공", response));
}

@DeleteMapping("/join-requests/{requestId}")
@Operation(summary = "가입 신청 취소", description = "본인의 가입 신청을 취소합니다.")
public ResponseEntity<BaseResponse<Void>> cancelRequest(
Expand Down Expand Up @@ -62,10 +76,10 @@ public ResponseEntity<BaseResponse<JoinClubRequestResponse>> rejectRequest(

@GetMapping("/{clubId}/join-requests")
@Operation(summary = "대기 중인 가입 신청 목록", description = "동호회의 대기 중인 가입 신청 목록을 조회합니다. 동호회장만 가능합니다.")
public ResponseEntity<BaseResponse<List<JoinClubRequestResponse>>> getPendingRequests(
public ResponseEntity<BaseResponse<List<JoinClubRequestSummaryResponse>>> getPendingRequests(
@Parameter(description = "동호회 ID") @PathVariable Long clubId,
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
List<JoinClubRequestResponse> response = joinClubRequestService.getPendingRequests(clubId, userAuthInfo.getId());
List<JoinClubRequestSummaryResponse> response = joinClubRequestService.getPendingRequests(clubId, userAuthInfo.getId());
return ResponseEntity.ok(BaseResponse.success("가입 신청 목록 조회 성공", response));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.be.sportizebe.domain.notification.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

@Schema(title = "JoinClubApplicationRequest DTO", description = "가입 신청 요청")
public record JoinClubApplicationRequest(
@NotBlank(message = "가입 신청서를 작성해주세요.")
@Schema(description = "가입 신청서", example = "안녕하세요! 열심히 활동하겠습니다.")
@Size(max = 200, message = "최대 200자 이하로 작성해주세요.")
String message
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ public record JoinClubRequestResponse(
@Schema(description = "가입 신청 ID") Long id,
@Schema(description = "신청자 ID") Long userId,
@Schema(description = "신청자 닉네임") String userNickname,
@Schema(description = "신청자 한줄 소개") String userIntroduce,
@Schema(description = "신청자 프로필 이미지") String userProfileImage,
@Schema(description = "동호회 ID") Long clubId,
@Schema(description = "동호회 이름") String clubName,
@Schema(description = "가입 신청서") String message,
@Schema(description = "신청 상태") JoinClubRequest.JoinClubRequestStatus status,
@Schema(description = "신청 일시") LocalDateTime createdAt
) {
Expand All @@ -23,9 +25,11 @@ public static JoinClubRequestResponse from(JoinClubRequest request) {
.id(request.getId())
.userId(request.getUser().getId())
.userNickname(request.getUser().getNickname())
.userIntroduce(request.getUser().getIntroduce())
.userProfileImage(request.getUser().getProfileImage())
.clubId(request.getClub().getId())
.clubName(request.getClub().getName())
.message(request.getMessage())
.status(request.getStatus())
.createdAt(request.getCreatedAt())
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.be.sportizebe.domain.notification.dto.response;

import com.be.sportizebe.domain.notification.entity.JoinClubRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
@Schema(title = "JoinClubRequestSummaryResponse DTO", description = "가입 신청 목록 응답 (요약)")
public record JoinClubRequestSummaryResponse(
@Schema(description = "가입 신청 ID") Long id,
@Schema(description = "신청자 ID") Long userId,
@Schema(description = "신청자 닉네임") String userNickname,
@Schema(description = "동호회 ID") Long clubId,
@Schema(description = "동호회 이름") String clubName
) {
public static JoinClubRequestSummaryResponse from(JoinClubRequest request) {
return JoinClubRequestSummaryResponse.builder()
.id(request.getId())
.userId(request.getUser().getId())
.userNickname(request.getUser().getNickname())
.clubId(request.getClub().getId())
.clubName(request.getClub().getName())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public enum JoinClubRequestStatus {
@JoinColumn(name = "club_id", nullable = false)
private Club club; // 가입 신청 대상 동호회

@Column(nullable = false, columnDefinition = "TEXT")
private String message; // 가입 신청서
Comment on lines +40 to +41
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

🧩 Analysis chain

🏁 Script executed:

# Find migration-related directories and files
find . -type d \( -name migrations -o -name db -o -name liquibase -o -name flyway -o -name sql \) 2>/dev/null | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Look for migration file patterns in common locations
find . -name "*.sql" -o -name "*.xml" | grep -i -E "(migration|flyway|liquibase|db)" | head -30

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check for Flyway or Liquibase configuration in build files and properties
find . -name "*.gradle" -o -name "*.gradle.kts" -o -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" | xargs grep -l "flyway\|liquibase" 2>/dev/null

Repository: Sportize/be

Length of output: 39


🏁 Script executed:

# Search for join_club_request related code and migrations
rg -i "join_club_request" --type-list | head -20

Repository: Sportize/be

Length of output: 444


🏁 Script executed:

# Get repository structure overview
ls -la

Repository: Sportize/be

Length of output: 1171


🏁 Script executed:

# Explore src directory structure
find src -type f -name "*.sql" -o -name "*.xml" | head -30

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check for Flyway or Liquibase configuration in gradle and properties files
cat build.gradle

Repository: Sportize/be

Length of output: 1969


🏁 Script executed:

# Look for application properties/yml files
find src -name "application*.yml" -o -name "application*.properties" | head -10

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Search for join_club_request in the codebase
rg "join_club_request" -i

Repository: Sportize/be

Length of output: 399


🏁 Script executed:

# Check the JoinClubRequest entity file to understand the current state
cat -n src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java

Repository: Sportize/be

Length of output: 1917


🏁 Script executed:

# Check if there's a migrations directory or database schema directory
find src -type d -name "*migration*" -o -type d -name "*db*" -o -type d -name "*sql*" -o -type d -name "*schema*"

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Look for all SQL files in the project
find . -name "*.sql" -type f

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check application.yml/properties in resources directory
find src -path "*/resources/*" -name "application*" -type f

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check for database configuration in resources
ls -la src/main/resources/ 2>/dev/null || echo "Resources directory not found or empty"

Repository: Sportize/be

Length of output: 161


🏁 Script executed:

# Search for any references to Flyway, Liquibase, or migration in all files
rg -i "flyway|liquibase|migration" --type-list

Repository: Sportize/be

Length of output: 444


🏁 Script executed:

# Try a simpler search for migration configuration
rg "flyway|liquibase|migration" -i

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check complete src directory structure
tree src -L 5 --dirsfirst

Repository: Sportize/be

Length of output: 277


🏁 Script executed:

# Check if there's a resources directory with application properties
find src/main -type f -name "*" | grep -i -E "(application|config|properties|yml|yaml)" | head -20

Repository: Sportize/be

Length of output: 690


🏁 Script executed:

# Look for any database initialization or schema files
find . -type f \( -name "*.sql" -o -name "schema.*" -o -name "data.*" \) 2>/dev/null

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check git history for any migration-related commits or files
git log --name-only --oneline | grep -i -E "(migration|flyway|liquibase|\.sql)" | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Verify if there's any Spring Boot auto-ddl or hibernate configuration
rg "spring.jpa.hibernate|spring.jpa.database|ddl-auto" -i

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check if Hibernate is configured to auto-create tables
find src/main -name "*.java" | xargs rg "spring.jpa|hibernate|ddl-auto" 2>/dev/null | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Verify if application properties exist anywhere in the project
find . -name "application*" -o -name "application*.yml" -o -name "application*.properties" 2>/dev/null

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check if there's any database configuration in the codebase
rg "@Configuration|@EnableAutoConfiguration" -A 10 | grep -i "database\|datasource\|jpa\|hibernate" | head -20

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Final comprehensive check - list all directories in src/main
find src/main -type d | sort

Repository: Sportize/be

Length of output: 6104


🏁 Script executed:

# Check git history to see if message field is newly added
git log -p -- src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java | head -100

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check the current status of the file in git
git show HEAD:src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java

Repository: Sportize/be

Length of output: 1518


🏁 Script executed:

# Check if there are any unstaged changes to this file
git diff src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java

Repository: Sportize/be

Length of output: 37


데이터베이스 마이그레이션 프레임워크 및 스크립트가 누락되었습니다.

코드상 message 필드는 nullable = false로 정의되어 있으나, 프로젝트에 Flyway나 Liquibase 같은 마이그레이션 도구가 설정되어 있지 않으며 마이그레이션 스크립트도 없습니다.

운영 데이터베이스에 기존 join_club_request 데이터가 있을 경우, 스키마 변경 시 다음 순서로 진행해야 합니다:

  1. 마이그레이션 프레임워크(Flyway/Liquibase) 설정
  2. 마이그레이션 스크립트 작성: message 컬럼 추가 → 기존 행 백필(DEFAULT '' 등) → NOT NULL 제약 적용
  3. 마이그레이션 파일을 통해 안전하게 배포
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/be/sportizebe/domain/notification/entity/JoinClubRequest.java`
around lines 40 - 41, The JoinClubRequest entity declares the field message as
`@Column`(nullable = false) but there is no DB migration framework or scripts; set
up a migration tool (Flyway or Liquibase), then add a migration that (1) adds
the message column to the join_club_request table (allow NULL initially or add
with DEFAULT ''), (2) backfill existing rows with a safe default (e.g. update
message = '' where message IS NULL), and (3) alter the column to NOT NULL (or
create it with NOT NULL only after backfill); ensure the migration file is
committed and the application configured to run migrations on deploy so
JoinClubRequest.message matches the DB schema.


@Enumerated(EnumType.STRING)
@Column(nullable = false)
@Builder.Default
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.be.sportizebe.domain.notification.service;

import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestResponse;
import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestSummaryResponse;

import java.util.List;

public interface JoinClubRequestService {

// 가입 신청
JoinClubRequestResponse requestJoin(Long clubId, Long userId);
JoinClubRequestResponse requestJoin(Long clubId, Long userId, String message);

// 가입 신청 단건 조회 (동호회장 또는 신청자 본인)
JoinClubRequestResponse getJoinRequest(Long requestId, Long userId);

// 가입 신청 취소
void cancelRequest(Long requestId, Long userId);
Expand All @@ -19,7 +23,7 @@ public interface JoinClubRequestService {
JoinClubRequestResponse rejectRequest(Long requestId, Long leaderId);

// 동호회의 대기 중인 가입 신청 목록 조회 (동호회장만)
List<JoinClubRequestResponse> getPendingRequests(Long clubId, Long leaderId);
List<JoinClubRequestSummaryResponse> getPendingRequests(Long clubId, Long leaderId);

// 내 가입 신청 목록 조회
List<JoinClubRequestResponse> getMyRequests(Long userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.be.sportizebe.domain.club.repository.ClubMemberRepository;
import com.be.sportizebe.domain.club.repository.ClubRepository;
import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestResponse;
import com.be.sportizebe.domain.notification.dto.response.JoinClubRequestSummaryResponse;
import com.be.sportizebe.domain.notification.entity.JoinClubRequest;
import com.be.sportizebe.domain.notification.exception.JoinClubRequestErrorCode;
import com.be.sportizebe.domain.notification.repository.JoinClubRequestRepository;
Expand Down Expand Up @@ -34,7 +35,7 @@ public class JoinClubRequestServiceImpl implements JoinClubRequestService {

@Override
@Transactional
public JoinClubRequestResponse requestJoin(Long clubId, Long userId) {
public JoinClubRequestResponse requestJoin(Long clubId, Long userId, String message) {
User user = findUserById(userId);
Club club = findClubById(clubId);

Expand Down Expand Up @@ -62,6 +63,7 @@ public JoinClubRequestResponse requestJoin(Long clubId, Long userId) {
JoinClubRequest joinRequest = JoinClubRequest.builder()
.user(user)
.club(club)
.message(message)
.build();
joinClubRequestRepository.save(joinRequest);

Expand All @@ -75,6 +77,20 @@ public JoinClubRequestResponse requestJoin(Long clubId, Long userId) {
return JoinClubRequestResponse.from(joinRequest);
}

@Override
public JoinClubRequestResponse getJoinRequest(Long requestId, Long userId) {
JoinClubRequest request = findRequestById(requestId);

// 동호회장 또는 신청자 본인만 조회 가능
boolean isLeader = request.getClub().isLeader(userId);
boolean isApplicant = userId.equals(request.getUser().getId());
if (!isLeader && !isApplicant) {
throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED);
}

return JoinClubRequestResponse.from(request);
}

@Override
@Transactional
public void cancelRequest(Long requestId, Long userId) {
Expand Down Expand Up @@ -160,7 +176,7 @@ public JoinClubRequestResponse rejectRequest(Long requestId, Long leaderId) {
}

@Override
public List<JoinClubRequestResponse> getPendingRequests(Long clubId, Long leaderId) {
public List<JoinClubRequestSummaryResponse> getPendingRequests(Long clubId, Long leaderId) {
Club club = findClubById(clubId);

// 동호회장만 조회 가능
Expand All @@ -171,7 +187,7 @@ public List<JoinClubRequestResponse> getPendingRequests(Long clubId, Long leader
return joinClubRequestRepository
.findByClubAndStatus(club, JoinClubRequest.JoinClubRequestStatus.PENDING)
.stream()
.map(JoinClubRequestResponse::from)
.map(JoinClubRequestSummaryResponse::from)
.toList();
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources