From a0f321ca22fb58162fb458ca972a62d7dfce71d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:36 +0900 Subject: [PATCH 01/14] =?UTF-8?q?chore:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V77__create_club_registration_request.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/resources/db/migration/V77__create_club_registration_request.sql diff --git a/src/main/resources/db/migration/V77__create_club_registration_request.sql b/src/main/resources/db/migration/V77__create_club_registration_request.sql new file mode 100644 index 000000000..84bd44b72 --- /dev/null +++ b/src/main/resources/db/migration/V77__create_club_registration_request.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS club_registration_request +( + id INT AUTO_INCREMENT PRIMARY KEY, + university_name VARCHAR(255) NOT NULL COMMENT '대학교 명', + club_name VARCHAR(50) NOT NULL COMMENT '동아리 명', + club_category VARCHAR(255) NOT NULL COMMENT '동아리 분과', + club_topic VARCHAR(20) NOT NULL COMMENT '동아리 주제', + club_emoji VARCHAR(10) NOT NULL COMMENT '동아리 이모지', + short_description VARCHAR(30) NOT NULL COMMENT '한 줄 소개', + full_introduction TEXT NOT NULL COMMENT '동아리 소개 (2000자)', + image_urls JSON NULL COMMENT '사진 및 영상 URL 목록 (최대 5개)', + status VARCHAR(20) DEFAULT 'PENDING' NOT NULL COMMENT '요청 상태 (PENDING, APPROVED, REJECTED)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL +) COMMENT '동아리 등록 요청'; From 9a1c24c98d4d31d56b0ac7a39da84b456ae3cf99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:37 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EB=B0=8F=20=EB=A6=AC=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/model/ClubRegistrationRequest.java | 102 ++++++++++++++++++ .../ClubRegistrationRequestRepository.java | 10 ++ 2 files changed, 112 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java create mode 100644 src/main/java/gg/agit/konect/domain/club/repository/ClubRegistrationRequestRepository.java diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java new file mode 100644 index 000000000..af125d1dd --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java @@ -0,0 +1,102 @@ +package gg.agit.konect.domain.club.model; + +import static jakarta.persistence.EnumType.STRING; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import java.util.List; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import gg.agit.konect.domain.club.enums.ClubCategory; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "club_registration_request") +@NoArgsConstructor(access = PROTECTED) +public class ClubRegistrationRequest extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @NotNull + @Column(name = "university_name", nullable = false) + private String universityName; + + @NotNull + @Column(name = "club_name", length = 50, nullable = false) + private String clubName; + + @NotNull + @Enumerated(value = STRING) + @Column(name = "club_category", nullable = false) + private ClubCategory clubCategory; + + @NotNull + @Column(name = "club_topic", length = 20, nullable = false) + private String clubTopic; + + @NotNull + @Column(name = "club_emoji", length = 10, nullable = false) + private String clubEmoji; + + @NotNull + @Column(name = "short_description", length = 30, nullable = false) + private String shortDescription; + + @NotNull + @Column(name = "full_introduction", columnDefinition = "TEXT", nullable = false) + private String fullIntroduction; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "image_urls", columnDefinition = "JSON") + private List imageUrls; + + @NotNull + @Enumerated(value = STRING) + @Column(name = "status", length = 20, nullable = false) + private RegistrationStatus status; + + @Builder + private ClubRegistrationRequest( + Integer id, + String universityName, + String clubName, + ClubCategory clubCategory, + String clubTopic, + String clubEmoji, + String shortDescription, + String fullIntroduction, + List imageUrls, + RegistrationStatus status + ) { + this.id = id; + this.universityName = universityName; + this.clubName = clubName; + this.clubCategory = clubCategory; + this.clubTopic = clubTopic; + this.clubEmoji = clubEmoji; + this.shortDescription = shortDescription; + this.fullIntroduction = fullIntroduction; + this.imageUrls = imageUrls; + this.status = status != null ? status : RegistrationStatus.PENDING; + } + + public enum RegistrationStatus { + PENDING, APPROVED, REJECTED + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/repository/ClubRegistrationRequestRepository.java b/src/main/java/gg/agit/konect/domain/club/repository/ClubRegistrationRequestRepository.java new file mode 100644 index 000000000..86e029c0f --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/repository/ClubRegistrationRequestRepository.java @@ -0,0 +1,10 @@ +package gg.agit.konect.domain.club.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import gg.agit.konect.domain.club.model.ClubRegistrationRequest; + +@Repository +public interface ClubRegistrationRequestRepository extends JpaRepository { +} From 821424f8b1d0be857a2bc7dc7591af4772df439d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:37 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/dto/ClubRegistrationRequestDto.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java new file mode 100644 index 000000000..7364039e4 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java @@ -0,0 +1,54 @@ +package gg.agit.konect.domain.club.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import gg.agit.konect.domain.club.enums.ClubCategory; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Schema(name = "ClubRegistrationRequest", description = "동아리 등록 요청") +public record ClubRegistrationRequestDto( + + @Schema(description = "대학교 명", example = "한국기술교육대학교", requiredMode = REQUIRED) + @NotBlank(message = "대학교 명은 필수입니다.") + String universityName, + + @Schema(description = "동아리 명", example = "BCSD Lab", requiredMode = REQUIRED) + @NotBlank(message = "동아리 명은 필수입니다.") + @Size(max = 50, message = "동아리 명은 최대 50자입니다.") + String clubName, + + @Schema(description = "동아리 분과", example = "ACADEMIC", requiredMode = REQUIRED) + @NotNull(message = "동아리 분과는 필수입니다.") + ClubCategory clubCategory, + + @Schema(description = "동아리 주제", example = "코딩", requiredMode = REQUIRED) + @NotBlank(message = "동아리 주제는 필수입니다.") + @Size(max = 20, message = "동아리 주제는 최대 20자입니다.") + String clubTopic, + + @Schema(description = "동아리 이모지", example = "💻", requiredMode = REQUIRED) + @NotBlank(message = "동아리 이모지는 필수입니다.") + @Size(max = 10, message = "동아리 이모지는 최대 10자입니다.") + String clubEmoji, + + @Schema(description = "한 줄 소개 (최대 30자)", example = "코딩 동아리입니다.", requiredMode = REQUIRED) + @NotBlank(message = "한 줄 소개는 필수입니다.") + @Size(max = 30, message = "한 줄 소개는 최대 30자입니다.") + String shortDescription, + + @Schema(description = "동아리 소개 (최대 2000자)", example = "상세한 동아리 소개 내용...", requiredMode = REQUIRED) + @NotBlank(message = "동아리 소개는 필수입니다.") + @Size(max = 2000, message = "동아리 소개는 최대 2000자입니다.") + String fullIntroduction, + + @Schema(description = "사진 및 영상 URL 목록 (최대 5개)", example = "[\"https://example.com/image1.jpg\"]", requiredMode = REQUIRED) + @NotNull(message = "사진 및 영상은 필수입니다.") + @Size(max = 5, message = "사진 및 영상은 최대 5개까지 업로드 가능합니다.") + List imageUrls +) { +} From d4d7e453201d0f0253c1e1e0ba8e6c8a8266cc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:37 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=8F=20=EC=8A=AC=EB=9E=99=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClubRegistrationRequestService.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java new file mode 100644 index 000000000..cafe0abc8 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java @@ -0,0 +1,46 @@ +package gg.agit.konect.domain.club.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.club.dto.ClubRegistrationRequestDto; +import gg.agit.konect.domain.club.model.ClubRegistrationRequest; +import gg.agit.konect.domain.club.repository.ClubRegistrationRequestRepository; +import gg.agit.konect.infrastructure.slack.service.SlackNotificationService; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class ClubRegistrationRequestService { + + private final ClubRegistrationRequestRepository clubRegistrationRequestRepository; + private final SlackNotificationService slackNotificationService; + + public void register(ClubRegistrationRequestDto request) { + ClubRegistrationRequest entity = ClubRegistrationRequest.builder() + .universityName(request.universityName()) + .clubName(request.clubName()) + .clubCategory(request.clubCategory()) + .clubTopic(request.clubTopic()) + .clubEmoji(request.clubEmoji()) + .shortDescription(request.shortDescription()) + .fullIntroduction(request.fullIntroduction()) + .imageUrls(request.imageUrls()) + .status(ClubRegistrationRequest.RegistrationStatus.PENDING) + .build(); + + ClubRegistrationRequest saved = clubRegistrationRequestRepository.save(entity); + + slackNotificationService.notifyClubRegistrationRequest( + saved.getId(), + request.universityName(), + request.clubName(), + request.clubCategory().getDescription(), + request.clubTopic(), + request.clubEmoji(), + request.shortDescription(), + request.imageUrls() != null ? request.imageUrls().size() : 0 + ); + } +} From 9ef431031faa37efac23031716e9b59407318896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:38 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20API=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClubRegistrationRequestApi.java | 13 ++++++++ .../ClubRegistrationRequestController.java | 33 +++++++++++++++++++ .../konect/global/config/SecurityPaths.java | 3 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestApi.java create mode 100644 src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestApi.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestApi.java new file mode 100644 index 000000000..2aab33338 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestApi.java @@ -0,0 +1,13 @@ +package gg.agit.konect.domain.club.controller; + +import org.springframework.http.ResponseEntity; + +import gg.agit.konect.domain.club.dto.ClubRegistrationRequestDto; +import io.swagger.v3.oas.annotations.parameters.RequestBody; + +public interface ClubRegistrationRequestApi { + + ResponseEntity registerClub( + @RequestBody ClubRegistrationRequestDto request + ); +} diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java new file mode 100644 index 000000000..146ab242d --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java @@ -0,0 +1,33 @@ +package gg.agit.konect.domain.club.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +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.ClubRegistrationRequestDto; +import gg.agit.konect.domain.club.service.ClubRegistrationRequestService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@Tag(name = "Club Registration", description = "동아리 등록 요청 API") +@RestController +@RequestMapping("/clubs") +@RequiredArgsConstructor +public class ClubRegistrationRequestController implements ClubRegistrationRequestApi { + + private final ClubRegistrationRequestService clubRegistrationRequestService; + + @Override + @Operation(summary = "동아리 등록 요청", description = "비로그인 사용자가 새 동아리 등록을 요청합니다.") + @PostMapping("/registration-requests") + public ResponseEntity registerClub( + @Valid @RequestBody ClubRegistrationRequestDto request + ) { + clubRegistrationRequestService.register(request); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/gg/agit/konect/global/config/SecurityPaths.java b/src/main/java/gg/agit/konect/global/config/SecurityPaths.java index c6c154e5f..0c0981b72 100644 --- a/src/main/java/gg/agit/konect/global/config/SecurityPaths.java +++ b/src/main/java/gg/agit/konect/global/config/SecurityPaths.java @@ -12,7 +12,8 @@ public final class SecurityPaths { "/error", "/slack/events", "/auth/oauth/google/drive/callback", - "/konect/**" + "/konect/**", + "/clubs/registration-requests" }; public static final String[] DENY_PATHS = {}; From 85e5fe94221c505576c3f7f60bffe579df8d31d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:38 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9E=99=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=85=9C=ED=94=8C=EB=A6=BF=EC=97=90=20=EB=8F=99?= =?UTF-8?q?=EC=95=84=EB=A6=AC=20=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../slack/enums/SlackMessageTemplate.java | 12 ++++++++++ .../service/SlackNotificationService.java | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java b/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java index 76711b27a..7515954b6 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java @@ -38,6 +38,18 @@ public enum SlackMessageTemplate { > %s """ ), + CLUB_REGISTRATION_REQUEST( + """ + *:new: 새로운 동아리 등록 요청이 들어왔습니다.* + 요청 ID: %s + 대학교: %s + 동아리명: %s %s + 분과: %s + 주제: %s + 한 줄 소개: %s + 첨부 이미지 수: %d개 + """ + ), ; private final String template; diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/service/SlackNotificationService.java b/src/main/java/gg/agit/konect/infrastructure/slack/service/SlackNotificationService.java index 86db2f7d1..708d895a6 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/service/SlackNotificationService.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/service/SlackNotificationService.java @@ -1,6 +1,7 @@ package gg.agit.konect.infrastructure.slack.service; import static gg.agit.konect.infrastructure.slack.enums.SlackMessageTemplate.ADMIN_CHAT_RECEIVED; +import static gg.agit.konect.infrastructure.slack.enums.SlackMessageTemplate.CLUB_REGISTRATION_REQUEST; import static gg.agit.konect.infrastructure.slack.enums.SlackMessageTemplate.INQUIRY; import static gg.agit.konect.infrastructure.slack.enums.SlackMessageTemplate.SHEET_SYNC_FAILED; import static gg.agit.konect.infrastructure.slack.enums.SlackMessageTemplate.USER_REGISTER; @@ -50,4 +51,27 @@ public void notifySheetSyncFailed(SheetSyncFailedEvent event) { ); slackClient.sendMessage(message, slackProperties.webhooks().error()); } + + public void notifyClubRegistrationRequest( + Integer requestId, + String universityName, + String clubName, + String category, + String topic, + String emoji, + String description, + int imageCount + ) { + String message = CLUB_REGISTRATION_REQUEST.format( + requestId, + universityName, + clubName, + category, + topic, + emoji, + description, + imageCount + ); + slackClient.sendMessage(message, slackProperties.webhooks().event()); + } } From dfe0f2eb7cf90ce7b35bfa6958fa4685bdde2cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:25:38 +0900 Subject: [PATCH 07/14] =?UTF-8?q?test:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/ClubRegistrationRequestApiTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java diff --git a/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java b/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java new file mode 100644 index 000000000..cced8aba8 --- /dev/null +++ b/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java @@ -0,0 +1,103 @@ +package gg.agit.konect.integration.domain.club; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import gg.agit.konect.domain.club.dto.ClubRegistrationRequestDto; +import gg.agit.konect.domain.club.enums.ClubCategory; +import gg.agit.konect.support.IntegrationTestSupport; + +class ClubRegistrationRequestApiTest extends IntegrationTestSupport { + + @Test + @DisplayName("비로그인 사용자도 동아리 등록 요청을 보낼 수 있다") + void registerClubWithoutLogin() throws Exception { + // given + ClubRegistrationRequestDto request = new ClubRegistrationRequestDto( + "한국기술교육대학교", + "BCSD Lab", + ClubCategory.ACADEMIC, + "코딩", + "💻", + "코딩 동아리입니다.", + "상세한 동아리 소개 내용입니다.", + List.of("https://example.com/image1.jpg") + ); + + // when & then + performPost("/clubs/registration-requests", request) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("필수값이 없으면 400을 반환한다") + void registerClubWithMissingFields() throws Exception { + // given + ClubRegistrationRequestDto request = new ClubRegistrationRequestDto( + "", + "BCSD Lab", + ClubCategory.ACADEMIC, + "코딩", + "💻", + "코딩 동아리입니다.", + "상세한 동아리 소개 내용입니다.", + List.of() + ); + + // when & then + performPost("/clubs/registration-requests", request) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("이미지가 5개를 초과하면 400을 반환한다") + void registerClubWithTooManyImages() throws Exception { + // given + ClubRegistrationRequestDto request = new ClubRegistrationRequestDto( + "한국기술교육대학교", + "BCSD Lab", + ClubCategory.ACADEMIC, + "코딩", + "💻", + "코딩 동아리입니다.", + "상세한 동아리 소개 내용입니다.", + List.of( + "https://example.com/image1.jpg", + "https://example.com/image2.jpg", + "https://example.com/image3.jpg", + "https://example.com/image4.jpg", + "https://example.com/image5.jpg", + "https://example.com/image6.jpg" + ) + ); + + // when & then + performPost("/clubs/registration-requests", request) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("동아리 소개가 2000자를 초과하면 400을 반환한다") + void registerClubWithLongIntroduction() throws Exception { + // given + String longIntroduction = "a".repeat(2001); + ClubRegistrationRequestDto request = new ClubRegistrationRequestDto( + "한국기술교육대학교", + "BCSD Lab", + ClubCategory.ACADEMIC, + "코딩", + "💻", + "코딩 동아리입니다.", + longIntroduction, + List.of() + ); + + // when & then + performPost("/clubs/registration-requests", request) + .andExpect(status().isBadRequest()); + } +} From 7eb9a1ee64bcfdf266b9fb5cdc9f112424f52583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:56:46 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor:=20=EC=9B=B9=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A5=BC=20website=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 웹사이트 전용 엔티티가 사용하는 서비스와 DTO의 도메인 경계에 놓이도록 패키지를 이동 - QueryDSL static import와 테스트 fixture import를 함께 정리해 생성 클래스 경로 불일치를 방지 - 테이블명은 유지해 DB 스키마 변경 없이 Java 패키지 경계만 정리 --- .../domain/website/dto/WebsiteClubDetailResponse.java | 4 ++-- .../konect/domain/website/dto/WebsiteClubsResponse.java | 4 ++-- .../konect/domain/{web => website}/model/WebClub.java | 2 +- .../domain/{web => website}/model/WebUniversity.java | 2 +- .../domain/website/repository/WebsiteQueryRepository.java | 8 ++++---- .../konect/domain/website/service/WebsiteService.java | 4 ++-- .../konect/integration/domain/website/WebsiteApiTest.java | 4 ++-- .../gg/agit/konect/support/fixture/WebClubFixture.java | 4 ++-- .../agit/konect/support/fixture/WebUniversityFixture.java | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) rename src/main/java/gg/agit/konect/domain/{web => website}/model/WebClub.java (98%) rename src/main/java/gg/agit/konect/domain/{web => website}/model/WebUniversity.java (97%) diff --git a/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubDetailResponse.java b/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubDetailResponse.java index 340362a1d..7bc7f56ff 100644 --- a/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubDetailResponse.java +++ b/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubDetailResponse.java @@ -4,8 +4,8 @@ import gg.agit.konect.domain.club.enums.ClubCategory; import gg.agit.konect.domain.university.enums.UniversityRegion; -import gg.agit.konect.domain.web.model.WebClub; -import gg.agit.konect.domain.web.model.WebUniversity; +import gg.agit.konect.domain.website.model.WebClub; +import gg.agit.konect.domain.website.model.WebUniversity; import io.swagger.v3.oas.annotations.media.Schema; public record WebsiteClubDetailResponse( diff --git a/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java b/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java index f72e56f37..9b87c9965 100644 --- a/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java +++ b/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java @@ -9,8 +9,8 @@ import gg.agit.konect.domain.club.enums.ClubCategory; import gg.agit.konect.domain.university.enums.UniversityRegion; -import gg.agit.konect.domain.web.model.WebClub; -import gg.agit.konect.domain.web.model.WebUniversity; +import gg.agit.konect.domain.website.model.WebClub; +import gg.agit.konect.domain.website.model.WebUniversity; import io.swagger.v3.oas.annotations.media.Schema; public record WebsiteClubsResponse( diff --git a/src/main/java/gg/agit/konect/domain/web/model/WebClub.java b/src/main/java/gg/agit/konect/domain/website/model/WebClub.java similarity index 98% rename from src/main/java/gg/agit/konect/domain/web/model/WebClub.java rename to src/main/java/gg/agit/konect/domain/website/model/WebClub.java index 73013859e..19165d813 100644 --- a/src/main/java/gg/agit/konect/domain/web/model/WebClub.java +++ b/src/main/java/gg/agit/konect/domain/website/model/WebClub.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.web.model; +package gg.agit.konect.domain.website.model; import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.FetchType.LAZY; diff --git a/src/main/java/gg/agit/konect/domain/web/model/WebUniversity.java b/src/main/java/gg/agit/konect/domain/website/model/WebUniversity.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/web/model/WebUniversity.java rename to src/main/java/gg/agit/konect/domain/website/model/WebUniversity.java index 819b9857d..48eb8e9e0 100644 --- a/src/main/java/gg/agit/konect/domain/web/model/WebUniversity.java +++ b/src/main/java/gg/agit/konect/domain/website/model/WebUniversity.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.web.model; +package gg.agit.konect.domain.website.model; import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.GenerationType.IDENTITY; diff --git a/src/main/java/gg/agit/konect/domain/website/repository/WebsiteQueryRepository.java b/src/main/java/gg/agit/konect/domain/website/repository/WebsiteQueryRepository.java index e5027783e..d51432830 100644 --- a/src/main/java/gg/agit/konect/domain/website/repository/WebsiteQueryRepository.java +++ b/src/main/java/gg/agit/konect/domain/website/repository/WebsiteQueryRepository.java @@ -1,7 +1,7 @@ package gg.agit.konect.domain.website.repository; -import static gg.agit.konect.domain.web.model.QWebClub.webClub; -import static gg.agit.konect.domain.web.model.QWebUniversity.webUniversity; +import static gg.agit.konect.domain.website.model.QWebClub.webClub; +import static gg.agit.konect.domain.website.model.QWebUniversity.webUniversity; import java.util.LinkedHashMap; import java.util.List; @@ -21,8 +21,8 @@ import gg.agit.konect.domain.club.enums.ClubCategory; import gg.agit.konect.domain.university.enums.UniversityRegion; -import gg.agit.konect.domain.web.model.WebClub; -import gg.agit.konect.domain.web.model.WebUniversity; +import gg.agit.konect.domain.website.model.WebClub; +import gg.agit.konect.domain.website.model.WebUniversity; import gg.agit.konect.domain.website.model.WebsiteUniversitySummary; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/gg/agit/konect/domain/website/service/WebsiteService.java b/src/main/java/gg/agit/konect/domain/website/service/WebsiteService.java index eb5f3b09d..d74bcc032 100644 --- a/src/main/java/gg/agit/konect/domain/website/service/WebsiteService.java +++ b/src/main/java/gg/agit/konect/domain/website/service/WebsiteService.java @@ -13,12 +13,12 @@ import org.springframework.transaction.annotation.Transactional; import gg.agit.konect.domain.university.enums.UniversityRegion; -import gg.agit.konect.domain.web.model.WebClub; -import gg.agit.konect.domain.web.model.WebUniversity; import gg.agit.konect.domain.website.dto.WebsiteClubDetailResponse; import gg.agit.konect.domain.website.dto.WebsiteClubListCondition; import gg.agit.konect.domain.website.dto.WebsiteClubsResponse; import gg.agit.konect.domain.website.dto.WebsiteHomeResponse; +import gg.agit.konect.domain.website.model.WebClub; +import gg.agit.konect.domain.website.model.WebUniversity; import gg.agit.konect.domain.website.model.WebsiteUniversitySummary; import gg.agit.konect.domain.website.repository.WebsiteQueryRepository; import gg.agit.konect.global.exception.CustomException; diff --git a/src/test/java/gg/agit/konect/integration/domain/website/WebsiteApiTest.java b/src/test/java/gg/agit/konect/integration/domain/website/WebsiteApiTest.java index edd13ae06..acdebe42b 100644 --- a/src/test/java/gg/agit/konect/integration/domain/website/WebsiteApiTest.java +++ b/src/test/java/gg/agit/konect/integration/domain/website/WebsiteApiTest.java @@ -17,8 +17,8 @@ import gg.agit.konect.domain.club.enums.ClubCategory; import gg.agit.konect.domain.university.enums.Campus; import gg.agit.konect.domain.university.enums.UniversityRegion; -import gg.agit.konect.domain.web.model.WebClub; -import gg.agit.konect.domain.web.model.WebUniversity; +import gg.agit.konect.domain.website.model.WebClub; +import gg.agit.konect.domain.website.model.WebUniversity; import gg.agit.konect.support.IntegrationTestSupport; import gg.agit.konect.support.fixture.WebClubFixture; import gg.agit.konect.support.fixture.WebUniversityFixture; diff --git a/src/test/java/gg/agit/konect/support/fixture/WebClubFixture.java b/src/test/java/gg/agit/konect/support/fixture/WebClubFixture.java index 05e0f16bc..3cc983142 100644 --- a/src/test/java/gg/agit/konect/support/fixture/WebClubFixture.java +++ b/src/test/java/gg/agit/konect/support/fixture/WebClubFixture.java @@ -3,8 +3,8 @@ import org.springframework.test.util.ReflectionTestUtils; import gg.agit.konect.domain.club.enums.ClubCategory; -import gg.agit.konect.domain.web.model.WebClub; -import gg.agit.konect.domain.web.model.WebUniversity; +import gg.agit.konect.domain.website.model.WebClub; +import gg.agit.konect.domain.website.model.WebUniversity; public class WebClubFixture { diff --git a/src/test/java/gg/agit/konect/support/fixture/WebUniversityFixture.java b/src/test/java/gg/agit/konect/support/fixture/WebUniversityFixture.java index 9cfdead7e..a822666c9 100644 --- a/src/test/java/gg/agit/konect/support/fixture/WebUniversityFixture.java +++ b/src/test/java/gg/agit/konect/support/fixture/WebUniversityFixture.java @@ -4,7 +4,7 @@ import gg.agit.konect.domain.university.enums.Campus; import gg.agit.konect.domain.university.enums.UniversityRegion; -import gg.agit.konect.domain.web.model.WebUniversity; +import gg.agit.konect.domain.website.model.WebUniversity; public class WebUniversityFixture { From 9778edba23353ad55281ea8cebb61d079974beda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 15:56:56 +0900 Subject: [PATCH 09/14] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../slack/enums/SlackMessageTemplate.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java b/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java index 7515954b6..663907143 100644 --- a/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java +++ b/src/main/java/gg/agit/konect/infrastructure/slack/enums/SlackMessageTemplate.java @@ -7,48 +7,48 @@ public enum SlackMessageTemplate { USER_REGISTER( """ - `%s님이 가입하셨습니다. Provider : %s` - """ + `%s님이 가입하셨습니다. Provider : %s` + """ ), USER_WITHDRAWAL( """ - `%s님이 탈퇴하셨습니다. Provider : %s` - """ + `%s님이 탈퇴하셨습니다. Provider : %s` + """ ), INQUIRY( """ - *:incoming_envelope: 사용자로부터 문의가 도착했습니다.* - > %s - """ + *:incoming_envelope: 사용자로부터 문의가 도착했습니다.* + > %s + """ ), ADMIN_CHAT_RECEIVED( """ - *:speech_balloon: 새로운 채팅이 도착했습니다.* - 보낸 사람: %s - > %s - """ + *:speech_balloon: 새로운 채팅이 도착했습니다.* + 보낸 사람: %s + > %s + """ ), SHEET_SYNC_FAILED( """ - *:warning: 시트 동기화 실패* - 동아리 ID: %s - 스프레드시트 ID: `%s` - 유형: %s - 발생 시각: %s - > %s - """ + *:warning: 시트 동기화 실패* + 동아리 ID: %s + 스프레드시트 ID: `%s` + 유형: %s + 발생 시각: %s + > %s + """ ), CLUB_REGISTRATION_REQUEST( """ - *:new: 새로운 동아리 등록 요청이 들어왔습니다.* - 요청 ID: %s - 대학교: %s - 동아리명: %s %s - 분과: %s - 주제: %s - 한 줄 소개: %s - 첨부 이미지 수: %d개 - """ + *:new: 새로운 동아리 등록 요청이 들어왔습니다.* + 요청 ID: %s + 대학교: %s + 동아리명: %s %s + 분과: %s + 주제: %s + 한 줄 소개: %s + 첨부 이미지 수: %d개 + """ ), ; From bad19c4f9293b6eb01847d0537cdf412678824ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 16:04:14 +0900 Subject: [PATCH 10/14] =?UTF-8?q?chore:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V77__create_club_registration_request.sql | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/resources/db/migration/V77__create_club_registration_request.sql b/src/main/resources/db/migration/V77__create_club_registration_request.sql index 84bd44b72..750aa39cc 100644 --- a/src/main/resources/db/migration/V77__create_club_registration_request.sql +++ b/src/main/resources/db/migration/V77__create_club_registration_request.sql @@ -8,8 +8,18 @@ CREATE TABLE IF NOT EXISTS club_registration_request club_emoji VARCHAR(10) NOT NULL COMMENT '동아리 이모지', short_description VARCHAR(30) NOT NULL COMMENT '한 줄 소개', full_introduction TEXT NOT NULL COMMENT '동아리 소개 (2000자)', - image_urls JSON NULL COMMENT '사진 및 영상 URL 목록 (최대 5개)', status VARCHAR(20) DEFAULT 'PENDING' NOT NULL COMMENT '요청 상태 (PENDING, APPROVED, REJECTED)', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL ) COMMENT '동아리 등록 요청'; + +CREATE TABLE IF NOT EXISTS club_registration_request_image +( + id INT AUTO_INCREMENT PRIMARY KEY, + request_id INT NOT NULL COMMENT '동아리 등록 요청 ID', + image_url VARCHAR(500) NOT NULL COMMENT '이미지 URL', + display_order INT DEFAULT 0 NOT NULL COMMENT '표시 순서', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + + FOREIGN KEY (request_id) REFERENCES club_registration_request (id) ON DELETE CASCADE +) COMMENT '동아리 등록 요청 이미지'; From e7e6fb05d19de2c65cd75f6fbae3e4221d12343c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 16:04:15 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/model/ClubRegistrationRequest.java | 26 ++++++--- .../model/ClubRegistrationRequestImage.java | 56 +++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java index af125d1dd..154480ebb 100644 --- a/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequest.java @@ -4,18 +4,19 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; +import java.util.ArrayList; import java.util.List; -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.type.SqlTypes; - import gg.agit.konect.domain.club.enums.ClubCategory; import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import lombok.Builder; @@ -62,9 +63,9 @@ public class ClubRegistrationRequest extends BaseEntity { @Column(name = "full_introduction", columnDefinition = "TEXT", nullable = false) private String fullIntroduction; - @JdbcTypeCode(SqlTypes.JSON) - @Column(name = "image_urls", columnDefinition = "JSON") - private List imageUrls; + @OneToMany(mappedBy = "request", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("displayOrder ASC") + private List images = new ArrayList<>(); @NotNull @Enumerated(value = STRING) @@ -81,7 +82,6 @@ private ClubRegistrationRequest( String clubEmoji, String shortDescription, String fullIntroduction, - List imageUrls, RegistrationStatus status ) { this.id = id; @@ -92,10 +92,20 @@ private ClubRegistrationRequest( this.clubEmoji = clubEmoji; this.shortDescription = shortDescription; this.fullIntroduction = fullIntroduction; - this.imageUrls = imageUrls; this.status = status != null ? status : RegistrationStatus.PENDING; } + public void addImages(List imageUrls) { + for (int i = 0; i < imageUrls.size(); i++) { + ClubRegistrationRequestImage image = ClubRegistrationRequestImage.builder() + .request(this) + .imageUrl(imageUrls.get(i)) + .displayOrder(i) + .build(); + this.images.add(image); + } + } + public enum RegistrationStatus { PENDING, APPROVED, REJECTED } diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java new file mode 100644 index 000000000..380dd1298 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java @@ -0,0 +1,56 @@ +package gg.agit.konect.domain.club.model; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "club_registration_request_image") +@NoArgsConstructor(access = PROTECTED) +public class ClubRegistrationRequestImage extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; + + @NotNull + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "request_id", nullable = false) + private ClubRegistrationRequest request; + + @NotNull + @Column(name = "image_url", length = 500, nullable = false) + private String imageUrl; + + @NotNull + @Column(name = "display_order", nullable = false) + private Integer displayOrder; + + @Builder + private ClubRegistrationRequestImage( + Integer id, + ClubRegistrationRequest request, + String imageUrl, + Integer displayOrder + ) { + this.id = id; + this.request = request; + this.imageUrl = imageUrl; + this.displayOrder = displayOrder; + } +} From ef3da7900726ddd0f735761bbe619eab808b7f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 16:04:15 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/club/service/ClubRegistrationRequestService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java index cafe0abc8..8a005f11f 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java @@ -26,10 +26,14 @@ public void register(ClubRegistrationRequestDto request) { .clubEmoji(request.clubEmoji()) .shortDescription(request.shortDescription()) .fullIntroduction(request.fullIntroduction()) - .imageUrls(request.imageUrls()) .status(ClubRegistrationRequest.RegistrationStatus.PENDING) .build(); + // 이미지 추가 + if (request.imageUrls() != null && !request.imageUrls().isEmpty()) { + entity.addImages(request.imageUrls()); + } + ClubRegistrationRequest saved = clubRegistrationRequestRepository.save(entity); slackNotificationService.notifyClubRegistrationRequest( From 0ff07c576088b78179ee196bc8d93a55fc102eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 16:41:27 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fix:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생성 요청은 201 응답으로 맞춰 클라이언트가 리소스 생성 성공을 일관되게 판단할 수 있게 정리 - 사진 및 영상은 최대 개수 제한만 유지해 무첨부 요청을 허용하는 API 계약과 저장 로직을 일치 - Slack 알림은 커밋 이후 이벤트 리스너에서 처리해 외부 연동 실패가 등록 저장을 롤백하지 않도록 분리 - 공개 접근은 경로 전체 예외 대신 @PublicApi 메서드 단위로 제한해 같은 경로의 향후 메서드가 자동 공개되지 않도록 정리 - 실제 null 반환 가능성이 있는 웹사이트 동아리 응답의 대학 정보 문서화를 선택값으로 맞춤 --- .../ClubRegistrationRequestController.java | 5 +- .../club/dto/ClubRegistrationRequestDto.java | 8 +- .../event/ClubRegistrationRequestedEvent.java | 28 +++++++ .../ClubRegistrationRequestService.java | 17 +--- .../website/dto/WebsiteClubsResponse.java | 3 +- .../konect/global/config/SecurityPaths.java | 3 +- .../ClubRegistrationRequestSlackListener.java | 39 +++++++++ .../club/ClubRegistrationRequestApiTest.java | 22 ++++- .../ClubRegistrationRequestServiceTest.java | 79 ++++++++++++++++++ ...bRegistrationRequestSlackListenerTest.java | 82 +++++++++++++++++++ 10 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/club/event/ClubRegistrationRequestedEvent.java create mode 100644 src/main/java/gg/agit/konect/infrastructure/slack/listener/ClubRegistrationRequestSlackListener.java create mode 100644 src/test/java/gg/agit/konect/unit/domain/club/service/ClubRegistrationRequestServiceTest.java create mode 100644 src/test/java/gg/agit/konect/unit/infrastructure/slack/listener/ClubRegistrationRequestSlackListenerTest.java diff --git a/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java b/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java index 146ab242d..fdc00b45c 100644 --- a/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java +++ b/src/main/java/gg/agit/konect/domain/club/controller/ClubRegistrationRequestController.java @@ -1,5 +1,6 @@ package gg.agit.konect.domain.club.controller; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -8,6 +9,7 @@ import gg.agit.konect.domain.club.dto.ClubRegistrationRequestDto; import gg.agit.konect.domain.club.service.ClubRegistrationRequestService; +import gg.agit.konect.global.auth.annotation.PublicApi; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -24,10 +26,11 @@ public class ClubRegistrationRequestController implements ClubRegistrationReques @Override @Operation(summary = "동아리 등록 요청", description = "비로그인 사용자가 새 동아리 등록을 요청합니다.") @PostMapping("/registration-requests") + @PublicApi public ResponseEntity registerClub( @Valid @RequestBody ClubRegistrationRequestDto request ) { clubRegistrationRequestService.register(request); - return ResponseEntity.ok().build(); + return ResponseEntity.status(HttpStatus.CREATED).build(); } } diff --git a/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java b/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java index 7364039e4..d3f662747 100644 --- a/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java +++ b/src/main/java/gg/agit/konect/domain/club/dto/ClubRegistrationRequestDto.java @@ -1,5 +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 java.util.List; @@ -46,8 +47,11 @@ public record ClubRegistrationRequestDto( @Size(max = 2000, message = "동아리 소개는 최대 2000자입니다.") String fullIntroduction, - @Schema(description = "사진 및 영상 URL 목록 (최대 5개)", example = "[\"https://example.com/image1.jpg\"]", requiredMode = REQUIRED) - @NotNull(message = "사진 및 영상은 필수입니다.") + @Schema( + description = "사진 및 영상 URL 목록 (최대 5개)", + example = "[\"https://example.com/image1.jpg\"]", + requiredMode = NOT_REQUIRED + ) @Size(max = 5, message = "사진 및 영상은 최대 5개까지 업로드 가능합니다.") List imageUrls ) { diff --git a/src/main/java/gg/agit/konect/domain/club/event/ClubRegistrationRequestedEvent.java b/src/main/java/gg/agit/konect/domain/club/event/ClubRegistrationRequestedEvent.java new file mode 100644 index 000000000..e40bb6b47 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/club/event/ClubRegistrationRequestedEvent.java @@ -0,0 +1,28 @@ +package gg.agit.konect.domain.club.event; + +import gg.agit.konect.domain.club.model.ClubRegistrationRequest; + +public record ClubRegistrationRequestedEvent( + Integer requestId, + String universityName, + String clubName, + String category, + String topic, + String emoji, + String description, + int imageCount +) { + + public static ClubRegistrationRequestedEvent from(ClubRegistrationRequest request) { + return new ClubRegistrationRequestedEvent( + request.getId(), + request.getUniversityName(), + request.getClubName(), + request.getClubCategory().getDescription(), + request.getClubTopic(), + request.getClubEmoji(), + request.getShortDescription(), + request.getImages().size() + ); + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java index 8a005f11f..aa812da2b 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubRegistrationRequestService.java @@ -1,12 +1,13 @@ package gg.agit.konect.domain.club.service; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import gg.agit.konect.domain.club.dto.ClubRegistrationRequestDto; +import gg.agit.konect.domain.club.event.ClubRegistrationRequestedEvent; import gg.agit.konect.domain.club.model.ClubRegistrationRequest; import gg.agit.konect.domain.club.repository.ClubRegistrationRequestRepository; -import gg.agit.konect.infrastructure.slack.service.SlackNotificationService; import lombok.RequiredArgsConstructor; @Service @@ -15,7 +16,7 @@ public class ClubRegistrationRequestService { private final ClubRegistrationRequestRepository clubRegistrationRequestRepository; - private final SlackNotificationService slackNotificationService; + private final ApplicationEventPublisher applicationEventPublisher; public void register(ClubRegistrationRequestDto request) { ClubRegistrationRequest entity = ClubRegistrationRequest.builder() @@ -35,16 +36,6 @@ public void register(ClubRegistrationRequestDto request) { } ClubRegistrationRequest saved = clubRegistrationRequestRepository.save(entity); - - slackNotificationService.notifyClubRegistrationRequest( - saved.getId(), - request.universityName(), - request.clubName(), - request.clubCategory().getDescription(), - request.clubTopic(), - request.clubEmoji(), - request.shortDescription(), - request.imageUrls() != null ? request.imageUrls().size() : 0 - ); + applicationEventPublisher.publishEvent(ClubRegistrationRequestedEvent.from(saved)); } } diff --git a/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java b/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java index 9b87c9965..4ef2b5962 100644 --- a/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java +++ b/src/main/java/gg/agit/konect/domain/website/dto/WebsiteClubsResponse.java @@ -1,5 +1,6 @@ package gg.agit.konect.domain.website.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.util.Arrays; @@ -14,7 +15,7 @@ import io.swagger.v3.oas.annotations.media.Schema; public record WebsiteClubsResponse( - @Schema(description = "대학 정보", requiredMode = REQUIRED) + @Schema(description = "대학 정보", requiredMode = NOT_REQUIRED) UniversityResponse university, @Schema(description = "전체 동아리 수", example = "28", requiredMode = REQUIRED) diff --git a/src/main/java/gg/agit/konect/global/config/SecurityPaths.java b/src/main/java/gg/agit/konect/global/config/SecurityPaths.java index 0c0981b72..c6c154e5f 100644 --- a/src/main/java/gg/agit/konect/global/config/SecurityPaths.java +++ b/src/main/java/gg/agit/konect/global/config/SecurityPaths.java @@ -12,8 +12,7 @@ public final class SecurityPaths { "/error", "/slack/events", "/auth/oauth/google/drive/callback", - "/konect/**", - "/clubs/registration-requests" + "/konect/**" }; public static final String[] DENY_PATHS = {}; diff --git a/src/main/java/gg/agit/konect/infrastructure/slack/listener/ClubRegistrationRequestSlackListener.java b/src/main/java/gg/agit/konect/infrastructure/slack/listener/ClubRegistrationRequestSlackListener.java new file mode 100644 index 000000000..5f21566fa --- /dev/null +++ b/src/main/java/gg/agit/konect/infrastructure/slack/listener/ClubRegistrationRequestSlackListener.java @@ -0,0 +1,39 @@ +package gg.agit.konect.infrastructure.slack.listener; + +import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +import gg.agit.konect.domain.club.event.ClubRegistrationRequestedEvent; +import gg.agit.konect.infrastructure.slack.service.SlackNotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ClubRegistrationRequestSlackListener { + + private final SlackNotificationService slackNotificationService; + + @Async("slackTaskExecutor") + @TransactionalEventListener(phase = AFTER_COMMIT) + public void handleClubRegistrationRequested(ClubRegistrationRequestedEvent event) { + try { + slackNotificationService.notifyClubRegistrationRequest( + event.requestId(), + event.universityName(), + event.clubName(), + event.category(), + event.topic(), + event.emoji(), + event.description(), + event.imageCount() + ); + } catch (RuntimeException e) { + log.warn("Failed to send club registration request Slack notification. requestId={}", event.requestId(), e); + } + } +} diff --git a/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java b/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java index cced8aba8..808bfd630 100644 --- a/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java +++ b/src/test/java/gg/agit/konect/integration/domain/club/ClubRegistrationRequestApiTest.java @@ -30,7 +30,27 @@ void registerClubWithoutLogin() throws Exception { // when & then performPost("/clubs/registration-requests", request) - .andExpect(status().isOk()); + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("사진 및 영상이 없어도 동아리 등록 요청을 보낼 수 있다") + void registerClubWithoutImages() throws Exception { + // given + ClubRegistrationRequestDto request = new ClubRegistrationRequestDto( + "한국기술교육대학교", + "BCSD Lab", + ClubCategory.ACADEMIC, + "코딩", + "💻", + "코딩 동아리입니다.", + "상세한 동아리 소개 내용입니다.", + null + ); + + // when & then + performPost("/clubs/registration-requests", request) + .andExpect(status().isCreated()); } @Test diff --git a/src/test/java/gg/agit/konect/unit/domain/club/service/ClubRegistrationRequestServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/club/service/ClubRegistrationRequestServiceTest.java new file mode 100644 index 000000000..63672c18f --- /dev/null +++ b/src/test/java/gg/agit/konect/unit/domain/club/service/ClubRegistrationRequestServiceTest.java @@ -0,0 +1,79 @@ +package gg.agit.konect.unit.domain.club.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.context.ApplicationEventPublisher; + +import gg.agit.konect.domain.club.dto.ClubRegistrationRequestDto; +import gg.agit.konect.domain.club.enums.ClubCategory; +import gg.agit.konect.domain.club.event.ClubRegistrationRequestedEvent; +import gg.agit.konect.domain.club.model.ClubRegistrationRequest; +import gg.agit.konect.domain.club.repository.ClubRegistrationRequestRepository; +import gg.agit.konect.domain.club.service.ClubRegistrationRequestService; +import gg.agit.konect.support.ServiceTestSupport; + +class ClubRegistrationRequestServiceTest extends ServiceTestSupport { + + @Mock + private ClubRegistrationRequestRepository clubRegistrationRequestRepository; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + @InjectMocks + private ClubRegistrationRequestService clubRegistrationRequestService; + + @Test + @DisplayName("동아리 등록 요청 저장 후 Slack 알림 이벤트를 발행한다") + void registerPublishesClubRegistrationRequestedEvent() { + // given + ClubRegistrationRequestDto request = new ClubRegistrationRequestDto( + "한국기술교육대학교", + "BCSD Lab", + ClubCategory.ACADEMIC, + "코딩", + "💻", + "코딩 동아리입니다.", + "상세한 동아리 소개 내용입니다.", + List.of("https://example.com/image1.jpg") + ); + ClubRegistrationRequest saved = ClubRegistrationRequest.builder() + .id(1) + .universityName(request.universityName()) + .clubName(request.clubName()) + .clubCategory(request.clubCategory()) + .clubTopic(request.clubTopic()) + .clubEmoji(request.clubEmoji()) + .shortDescription(request.shortDescription()) + .fullIntroduction(request.fullIntroduction()) + .build(); + saved.addImages(request.imageUrls()); + given(clubRegistrationRequestRepository.save(any(ClubRegistrationRequest.class))).willReturn(saved); + + // when + clubRegistrationRequestService.register(request); + + // then + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass( + ClubRegistrationRequestedEvent.class + ); + verify(applicationEventPublisher).publishEvent(eventCaptor.capture()); + + ClubRegistrationRequestedEvent event = eventCaptor.getValue(); + assertThat(event.requestId()).isEqualTo(saved.getId()); + assertThat(event.universityName()).isEqualTo(request.universityName()); + assertThat(event.clubName()).isEqualTo(request.clubName()); + assertThat(event.category()).isEqualTo(request.clubCategory().getDescription()); + assertThat(event.imageCount()).isEqualTo(1); + } +} diff --git a/src/test/java/gg/agit/konect/unit/infrastructure/slack/listener/ClubRegistrationRequestSlackListenerTest.java b/src/test/java/gg/agit/konect/unit/infrastructure/slack/listener/ClubRegistrationRequestSlackListenerTest.java new file mode 100644 index 000000000..1850aa6b5 --- /dev/null +++ b/src/test/java/gg/agit/konect/unit/infrastructure/slack/listener/ClubRegistrationRequestSlackListenerTest.java @@ -0,0 +1,82 @@ +package gg.agit.konect.unit.infrastructure.slack.listener; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import gg.agit.konect.domain.club.event.ClubRegistrationRequestedEvent; +import gg.agit.konect.infrastructure.slack.listener.ClubRegistrationRequestSlackListener; +import gg.agit.konect.infrastructure.slack.service.SlackNotificationService; +import gg.agit.konect.support.ServiceTestSupport; + +class ClubRegistrationRequestSlackListenerTest extends ServiceTestSupport { + + @Mock + private SlackNotificationService slackNotificationService; + + @InjectMocks + private ClubRegistrationRequestSlackListener clubRegistrationRequestSlackListener; + + @Test + @DisplayName("동아리 등록 요청 이벤트를 Slack 알림 서비스에 위임한다") + void handleClubRegistrationRequestedDelegatesToSlackService() { + // given + ClubRegistrationRequestedEvent event = createEvent(); + + // when + clubRegistrationRequestSlackListener.handleClubRegistrationRequested(event); + + // then + verify(slackNotificationService).notifyClubRegistrationRequest( + event.requestId(), + event.universityName(), + event.clubName(), + event.category(), + event.topic(), + event.emoji(), + event.description(), + event.imageCount() + ); + } + + @Test + @DisplayName("Slack 알림 실패가 이벤트 처리 밖으로 전파되지 않는다") + void handleClubRegistrationRequestedSwallowsExceptions() { + // given + ClubRegistrationRequestedEvent event = createEvent(); + doThrow(new RuntimeException("slack error")) + .when(slackNotificationService) + .notifyClubRegistrationRequest( + event.requestId(), + event.universityName(), + event.clubName(), + event.category(), + event.topic(), + event.emoji(), + event.description(), + event.imageCount() + ); + + // when & then + assertThatCode(() -> clubRegistrationRequestSlackListener.handleClubRegistrationRequested(event)) + .doesNotThrowAnyException(); + } + + private ClubRegistrationRequestedEvent createEvent() { + return new ClubRegistrationRequestedEvent( + 1, + "한국기술교육대학교", + "BCSD Lab", + "학술", + "코딩", + "💻", + "코딩 동아리입니다.", + 1 + ); + } +} From 115f00cfc8e830c9108539f119156271439d6538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sat, 23 May 2026 16:43:16 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20=EB=93=B1=EB=A1=9D=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20builder=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=20=EB=B2=94=EC=9C=84=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Builder 문자열 출력에서 요청 엔티티 연관관계를 제외해 불필요한 객체 그래프 노출을 피함 - 기능 동작은 유지하면서 정적 분석 경고만 좁게 해소 --- .../club/model/ClubRegistrationRequestImage.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java index 380dd1298..8f2055aba 100644 --- a/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java +++ b/src/main/java/gg/agit/konect/domain/club/model/ClubRegistrationRequestImage.java @@ -53,4 +53,16 @@ private ClubRegistrationRequestImage( this.imageUrl = imageUrl; this.displayOrder = displayOrder; } + + public static class ClubRegistrationRequestImageBuilder { + + @Override + public String toString() { + return "ClubRegistrationRequestImage.ClubRegistrationRequestImageBuilder(" + + "id=" + id + + ", imageUrl=" + imageUrl + + ", displayOrder=" + displayOrder + + ")"; + } + } }