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
@@ -0,0 +1,61 @@
package gg.agit.konect.domain.club.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import gg.agit.konect.domain.club.dto.ClubSettingsResponse;
import gg.agit.konect.domain.club.dto.ClubSettingsUpdateRequest;
import gg.agit.konect.global.auth.annotation.UserId;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;

@Tag(name = "(Normal) Club - Settings: 동아리 설정 관리")
@RequestMapping("/clubs")
public interface ClubSettingsApi {

@Operation(summary = "동아리 설정 정보를 조회한다.", description = """
동아리 설정 관리 화면에 필요한 정보를 조회합니다.

토글 상태(모집공고, 지원서, 회비)와 각 항목의 요약 정보를 반환합니다.
- 모집공고: 모집 기간 정보
- 지원서: 문항 개수
- 회비: 금액, 은행, 계좌번호, 예금주

요약 정보가 설정되지 않은 경우 해당 필드는 null로 반환됩니다.

## 에러
- NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다.
- NOT_FOUND_USER (404): 유저를 찾을 수 없습니다.
- FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다.
""")
@GetMapping("/{clubId}/settings")
ResponseEntity<ClubSettingsResponse> getSettings(
@PathVariable(name = "clubId") Integer clubId,
@UserId Integer userId
);

@Operation(summary = "동아리 설정을 변경한다.", description = """
동아리의 토글 설정(모집공고, 지원서, 회비 활성화 여부)을 변경합니다.

요청에 포함된 필드만 업데이트됩니다. (PATCH 방식)
- isRecruitmentEnabled: 모집공고 활성화 여부
- isApplicationEnabled: 지원서 활성화 여부
- isFeeEnabled: 회비 활성화 여부

## 에러
- NOT_FOUND_CLUB (404): 동아리를 찾을 수 없습니다.
- NOT_FOUND_USER (404): 유저를 찾을 수 없습니다.
- FORBIDDEN_CLUB_MANAGER_ACCESS (403): 동아리 매니저 권한이 없습니다.
""")
@PatchMapping("/{clubId}/settings")
ResponseEntity<ClubSettingsResponse> updateSettings(
@PathVariable(name = "clubId") Integer clubId,
@Valid @RequestBody ClubSettingsUpdateRequest request,
@UserId Integer userId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package gg.agit.konect.domain.club.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import gg.agit.konect.domain.club.dto.ClubSettingsResponse;
import gg.agit.konect.domain.club.dto.ClubSettingsUpdateRequest;
import gg.agit.konect.domain.club.service.ClubSettingsService;
import gg.agit.konect.global.auth.annotation.UserId;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/clubs")
public class ClubSettingsController implements ClubSettingsApi {

private final ClubSettingsService clubSettingsService;

@Override
public ResponseEntity<ClubSettingsResponse> getSettings(
@PathVariable(name = "clubId") Integer clubId,
@UserId Integer userId
) {
ClubSettingsResponse response = clubSettingsService.getSettings(clubId, userId);
return ResponseEntity.ok(response);
}

@Override
public ResponseEntity<ClubSettingsResponse> updateSettings(
@PathVariable(name = "clubId") Integer clubId,
@Valid @RequestBody ClubSettingsUpdateRequest request,
@UserId Integer userId
) {
ClubSettingsResponse response = clubSettingsService.updateSettings(clubId, userId, request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package gg.agit.konect.domain.club.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Expand All @@ -22,10 +21,6 @@ public record ClubFeeInfoReplaceRequest(

@Size(max = 100, message = "예금주는 최대 100자 입니다.")
@Schema(description = "예금주", example = "BCSD", requiredMode = NOT_REQUIRED)
String accountHolder,

@NotNull(message = "회비 납부 필요 여부는 필수로 입력해야 합니다.")
@Schema(description = "회비 납부 필요 여부", example = "true", requiredMode = REQUIRED)
Boolean isFeeRequired
String accountHolder
) {
Comment on lines 21 to 25
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

ClubFeeInfoReplaceRequest에서 isFeeRequired 입력이 제거되면서, 기존 클라이언트가 해당 필드를 포함해 요청할 경우 Jackson 설정에 따라 400(unknown property)로 실패할 수 있습니다. 하위 호환이 필요하면 요청 DTO에 unknown property 무시 설정(@JsonIgnoreProperties(ignoreUnknown = true))을 추가하거나, 일정 기간 구 DTO를 병행 지원하는 방안을 고려해주세요.

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,15 @@ public record ClubFeeInfoResponse(
String accountNumber,

@Schema(description = "예금주", example = "BCSD", requiredMode = REQUIRED)
String accountHolder,

@Schema(description = "회비 납부 필요 여부", example = "true", requiredMode = REQUIRED)
Boolean isFeeRequired
String accountHolder
Comment on lines 19 to +22
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

ClubFeeInfoResponse에서 isFeeRequired 필드가 제거되어 기존 API 응답 스펙을 사용하는 클라이언트가 깨질 수 있습니다. 의도된 변경이라면 API 문서/클라이언트 동시 배포가 필요하고, 호환성이 필요하면 필드를 유지한 채 deprecated 처리 또는 버전 분리(새 응답 DTO)로 전환하는 방안을 검토해주세요.

Copilot uses AI. Check for mistakes.
) {
public static ClubFeeInfoResponse of(Club club, Integer bankId, String bankName) {
return new ClubFeeInfoResponse(
club.getFeeAmount(),
bankId,
bankName,
club.getFeeAccountNumber(),
club.getFeeAccountHolder(),
club.getIsFeeRequired()
club.getFeeAccountHolder()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package gg.agit.konect.domain.club.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonFormat;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "동아리 설정 관리 응답")
public record ClubSettingsResponse(
@Schema(description = "모집공고 활성화 여부", example = "true", requiredMode = REQUIRED)
Boolean isRecruitmentEnabled,

@Schema(description = "지원서 활성화 여부", example = "true", requiredMode = REQUIRED)
Boolean isApplicationEnabled,

@Schema(description = "회비 활성화 여부", example = "false", requiredMode = REQUIRED)
Boolean isFeeEnabled,

@Schema(description = "모집공고 요약 정보", requiredMode = NOT_REQUIRED)
RecruitmentSummary recruitment,

@Schema(description = "지원서 요약 정보", requiredMode = NOT_REQUIRED)
ApplicationSummary application,

@Schema(description = "회비 요약 정보", requiredMode = NOT_REQUIRED)
FeeSummary fee
) {
@Schema(description = "모집공고 요약")
public record RecruitmentSummary(
@Schema(description = "모집 시작일", example = "2026.02.02", requiredMode = NOT_REQUIRED)
@JsonFormat(pattern = "yyyy.MM.dd")
LocalDate startDate,

@Schema(description = "모집 종료일", example = "2027.02.02", requiredMode = NOT_REQUIRED)
@JsonFormat(pattern = "yyyy.MM.dd")
LocalDate endDate,

@Schema(description = "상시 모집 여부", example = "false", requiredMode = REQUIRED)
Boolean isAlwaysRecruiting
) {
}

@Schema(description = "지원서 요약")
public record ApplicationSummary(
@Schema(description = "문항 개수", example = "3", requiredMode = REQUIRED)
Integer questionCount
) {
}

@Schema(description = "회비 요약")
public record FeeSummary(
@Schema(description = "회비 금액", example = "3만원", requiredMode = NOT_REQUIRED)
String amount,

@Schema(description = "은행 고유 ID", example = "1", requiredMode = NOT_REQUIRED)
Integer bankId,

@Schema(description = "은행명", example = "국민은행", requiredMode = NOT_REQUIRED)
String bankName,

@Schema(description = "계좌번호", example = "123-456-7890", requiredMode = NOT_REQUIRED)
String accountNumber,

@Schema(description = "예금주", example = "BCSD", requiredMode = NOT_REQUIRED)
String accountHolder
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gg.agit.konect.domain.club.dto;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "동아리 설정 변경 요청")
public record ClubSettingsUpdateRequest(
@Schema(description = "모집공고 활성화 여부", example = "true", requiredMode = NOT_REQUIRED)
Boolean isRecruitmentEnabled,

@Schema(description = "지원서 활성화 여부", example = "true", requiredMode = NOT_REQUIRED)
Boolean isApplicationEnabled,

@Schema(description = "회비 활성화 여부", example = "false", requiredMode = NOT_REQUIRED)
Boolean isFeeEnabled
) {
}
62 changes: 39 additions & 23 deletions src/main/java/gg/agit/konect/domain/club/model/Club.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import static jakarta.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

import java.time.LocalDate;

import org.springframework.util.StringUtils;

import gg.agit.konect.domain.club.dto.ClubCreateRequest;
Expand Down Expand Up @@ -77,8 +75,14 @@ public class Club extends BaseEntity {
@Column(name = "fee_account_holder", length = 100)
private String feeAccountHolder;

@Column(name = "fee_deadline")
private LocalDate feeDeadline;
@Column(name = "is_fee_required")
private Boolean isFeeRequired;

@Column(name = "is_recruitment_enabled")
private Boolean isRecruitmentEnabled;

@Column(name = "is_application_enabled")
private Boolean isApplicationEnabled;
Comment on lines +78 to +85
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -i club | head -20

Repository: BCSDLab/KONECT_BACK_END

Length of output: 1575


🏁 Script executed:

find . -type f -name "Club.java" | head -5

Repository: BCSDLab/KONECT_BACK_END

Length of output: 125


🏁 Script executed:

find . -type f -name "*ClubSettingsResponse*"

Repository: BCSDLab/KONECT_BACK_END

Length of output: 139


🏁 Script executed:

cat -n src/main/java/gg/agit/konect/domain/club/model/Club.java | head -200

Repository: BCSDLab/KONECT_BACK_END

Length of output: 7881


🏁 Script executed:

cat -n src/main/java/gg/agit/konect/domain/club/dto/ClubSettingsResponse.java

Repository: BCSDLab/KONECT_BACK_END

Length of output: 3021


🏁 Script executed:

rg -n "ClubSettingsResponse" --type java -B 3 -A 10 src/main/java/gg/agit/konect/domain/club/service/

Repository: BCSDLab/KONECT_BACK_END

Length of output: 7813


🏁 Script executed:

rg -n "getIsRecruitmentEnabled|getIsApplicationEnabled|getIsFeeRequired" --type java src/main/java/gg/agit/konect/domain/club/

Repository: BCSDLab/KONECT_BACK_END

Length of output: 1042


🏁 Script executed:

cat -n src/main/java/gg/agit/konect/domain/club/service/ClubSettingsService.java | sed -n '32,90p'

Repository: BCSDLab/KONECT_BACK_END

Length of output: 2712


🏁 Script executed:

rg -n "club\.get(IsRecruitmentEnabled|IsApplicationEnabled|IsFeeRequired)" --type java

Repository: BCSDLab/KONECT_BACK_END

Length of output: 1042


🏁 Script executed:

rg -n "\.isRecruitmentEnabled()|\.isApplicationEnabled()|\.isFeeRequired()" --type java -A 2 -B 2

Repository: BCSDLab/KONECT_BACK_END

Length of output: 2451


🏁 Script executed:

cat -n src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java | sed -n '185,200p'

Repository: BCSDLab/KONECT_BACK_END

Length of output: 824


Initialize boolean flags in the factory method or set column defaults.

The new boolean fields (isFeeRequired, isRecruitmentEnabled, isApplicationEnabled) are uninitialized in the of() factory method (lines 127-137) and will be null for newly created clubs. While the service layer safely handles null values using Boolean.TRUE.equals(), initializing these fields with sensible defaults in the factory method improves data consistency and makes the entity's state explicit:

🛠️ Proposed fix: Initialize in factory method
     public static Club of(ClubCreateRequest request, University university) {
         return Club.builder()
             .name(request.name())
             .description(request.description())
             .introduce(request.introduce())
             .imageUrl(request.imageUrl())
             .location(request.location())
             .clubCategory(request.clubCategory())
             .university(university)
+            .isFeeRequired(false)
+            .isRecruitmentEnabled(false)
+            .isApplicationEnabled(false)
             .build();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/domain/club/model/Club.java` around lines 78 -
85, The new Boolean flags in Club (isFeeRequired, isRecruitmentEnabled,
isApplicationEnabled) are left null by the Club.of(...) factory; update the
Club.of(...) method to explicitly initialize these fields to sensible defaults
(e.g., Boolean.FALSE or whatever domain default) when constructing the Club
instance so newly created clubs never have null for
isFeeRequired/isRecruitmentEnabled/isApplicationEnabled and the entity state is
explicit.


@OneToOne(mappedBy = "club", fetch = LAZY, cascade = ALL, orphanRemoval = true)
private ClubRecruitment clubRecruitment;
Expand All @@ -97,7 +101,9 @@ private Club(
String feeBank,
String feeAccountNumber,
String feeAccountHolder,
LocalDate feeDeadline,
Boolean isFeeRequired,
Boolean isRecruitmentEnabled,
Boolean isApplicationEnabled,
ClubRecruitment clubRecruitment
) {
this.id = id;
Expand All @@ -112,7 +118,9 @@ private Club(
this.feeBank = feeBank;
this.feeAccountNumber = feeAccountNumber;
this.feeAccountHolder = feeAccountHolder;
this.feeDeadline = feeDeadline;
this.isFeeRequired = isFeeRequired;
this.isRecruitmentEnabled = isRecruitmentEnabled;
this.isApplicationEnabled = isApplicationEnabled;
this.clubRecruitment = clubRecruitment;
}

Expand All @@ -132,19 +140,18 @@ public void replaceFeeInfo(
String feeAmount,
String feeBank,
String feeAccountNumber,
String feeAccountHolder,
LocalDate feeDeadline
String feeAccountHolder
) {
if (isFeeInfoEmpty(feeAmount, feeBank, feeAccountNumber, feeAccountHolder, feeDeadline)) {
if (isFeeInfoEmpty(feeAmount, feeBank, feeAccountNumber, feeAccountHolder)) {
clearFeeInfo();
return;
}

if (!isFeeInfoComplete(feeAmount, feeBank, feeAccountNumber, feeAccountHolder, feeDeadline)) {
if (!isFeeInfoComplete(feeAmount, feeBank, feeAccountNumber, feeAccountHolder)) {
throw CustomException.of(INVALID_REQUEST_BODY);
}

updateFeeInfo(feeAmount, feeBank, feeAccountNumber, feeAccountHolder, feeDeadline);
updateFeeInfo(feeAmount, feeBank, feeAccountNumber, feeAccountHolder);
}

public void updateInfo(String description, String imageUrl, String location, String introduce) {
Expand All @@ -159,53 +166,62 @@ public void updateBasicInfo(String name, ClubCategory clubCategory) { // 어드
this.clubCategory = clubCategory;
}

public void updateSettings(
Boolean isRecruitmentEnabled,
Boolean isApplicationEnabled,
Boolean isFeeRequired
) {
if (isRecruitmentEnabled != null) {
this.isRecruitmentEnabled = isRecruitmentEnabled;
}
if (isApplicationEnabled != null) {
this.isApplicationEnabled = isApplicationEnabled;
}
if (isFeeRequired != null) {
this.isFeeRequired = isFeeRequired;
}
}

private boolean isFeeInfoEmpty(
String feeAmount,
String feeBank,
String feeAccountNumber,
String feeAccountHolder,
LocalDate feeDeadline
String feeAccountHolder
) {
return !StringUtils.hasText(feeAmount)
&& !StringUtils.hasText(feeBank)
&& !StringUtils.hasText(feeAccountNumber)
&& !StringUtils.hasText(feeAccountHolder)
&& feeDeadline == null;
&& !StringUtils.hasText(feeAccountHolder);
}

private boolean isFeeInfoComplete(
String feeAmount,
String feeBank,
String feeAccountNumber,
String feeAccountHolder,
LocalDate feeDeadline
String feeAccountHolder
) {
return StringUtils.hasText(feeAmount)
&& StringUtils.hasText(feeBank)
&& StringUtils.hasText(feeAccountNumber)
&& StringUtils.hasText(feeAccountHolder)
&& feeDeadline != null;
&& StringUtils.hasText(feeAccountHolder);
}

private void updateFeeInfo(
String feeAmount,
String feeBank,
String feeAccountNumber,
String feeAccountHolder,
LocalDate feeDeadline
String feeAccountHolder
) {
this.feeAmount = feeAmount;
this.feeBank = feeBank;
this.feeAccountNumber = feeAccountNumber;
this.feeAccountHolder = feeAccountHolder;
this.feeDeadline = feeDeadline;
}

private void clearFeeInfo() {
this.feeAmount = null;
this.feeBank = null;
this.feeAccountNumber = null;
this.feeAccountHolder = null;
this.feeDeadline = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,7 @@ public ClubFeeInfoResponse replaceFeeInfo(Integer clubId, Integer userId, ClubFe
request.amount(),
bankName,
request.accountNumber(),
request.accountHolder(),
request.isFeeRequired()
request.accountHolder()
);

return ClubFeeInfoResponse.of(club, request.bankId(), bankName);
Expand Down
Loading