From 426cf0b722b39b224e25f101f5a7f11d88e6b82a Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 16 Feb 2026 16:30:45 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20@=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EB=B0=9B=EB=8A=94=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../moadong/club/controller/ClubProfileController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/src/main/java/moadong/club/controller/ClubProfileController.java b/backend/src/main/java/moadong/club/controller/ClubProfileController.java index d0c97c9cf..cc5f20f1c 100644 --- a/backend/src/main/java/moadong/club/controller/ClubProfileController.java +++ b/backend/src/main/java/moadong/club/controller/ClubProfileController.java @@ -31,6 +31,14 @@ public ResponseEntity getClubDetail(@PathVariable String clubId) { return Response.ok(clubDetailedPageResponse); } + @GetMapping("/{clubName:@[^/]+}") + @Operation(summary = "클럽 상세 정보 조회", description = "클럽 이름을 이용해 상세 정보를 조회합니다.") + public ResponseEntity getClubDetailByClubName(@PathVariable String clubName) { + ClubDetailedResponse clubDetailedResponse = clubProfileService.getClubDetailByClubName(clubName); + return Response.ok(clubDetailedResponse); + } + + @PutMapping("/info") @Operation(summary = "클럽 약력 수정", description = "클럽 약력을 수정합니다.
" + "tags는 최대 2개, 5글자 이내로 입력해야 합니다.
" From acc2d9c16fd8b6652d4c91e2122fa2a4e17d7529 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 16 Feb 2026 16:31:18 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8F=99=EC=95=84=EB=A6=AC=20=EC=83=81=EC=84=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=EA=B8=B0=EB=8A=A5=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 --- .../java/moadong/club/repository/ClubRepository.java | 3 +++ .../java/moadong/club/service/ClubProfileService.java | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/backend/src/main/java/moadong/club/repository/ClubRepository.java b/backend/src/main/java/moadong/club/repository/ClubRepository.java index aa4e47003..c558bfa10 100644 --- a/backend/src/main/java/moadong/club/repository/ClubRepository.java +++ b/backend/src/main/java/moadong/club/repository/ClubRepository.java @@ -20,4 +20,7 @@ public interface ClubRepository extends MongoRepository { Long countByIdIn(List id); List findAllByClubRecruitmentInformation_ClubRecruitmentStatus(ClubRecruitmentStatus status); + + Optional findClubByName(String name); + } diff --git a/backend/src/main/java/moadong/club/service/ClubProfileService.java b/backend/src/main/java/moadong/club/service/ClubProfileService.java index 561babfbd..98349b5f0 100644 --- a/backend/src/main/java/moadong/club/service/ClubProfileService.java +++ b/backend/src/main/java/moadong/club/service/ClubProfileService.java @@ -94,6 +94,16 @@ public ClubDetailedResponse getClubDetail(String clubId) { return new ClubDetailedResponse(clubDetailedResult); } + public ClubDetailedResponse getClubDetailByClubName(String clubName) { + Club club = clubRepository.findClubByName(clubName) + .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); + + ClubDetailedResult clubDetailedResult = ClubDetailedResult.of( + club + ); + return new ClubDetailedResponse(clubDetailedResult); + } + @Transactional public void updateClubInfoByClubId(String clubId, ClubInfoRequest request, CustomUserDetails user) { ObjectId objectId = ObjectIdConverter.convertString(clubId); From 6cf83972c7e17b69ef266b6ca0e4e4c86e59eec2 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 16 Feb 2026 16:44:15 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=A4=91=EB=B3=B5=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/moadong/club/entity/Club.java | 3 +++ .../club/repository/ClubRepository.java | 2 ++ .../club/service/ClubProfileService.java | 26 +++++++++++++++++-- .../moadong/global/exception/ErrorCode.java | 1 + 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/moadong/club/entity/Club.java b/backend/src/main/java/moadong/club/entity/Club.java index bac93a549..c2826911f 100644 --- a/backend/src/main/java/moadong/club/entity/Club.java +++ b/backend/src/main/java/moadong/club/entity/Club.java @@ -13,6 +13,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Version; import org.springframework.data.domain.Persistable; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @@ -27,6 +28,8 @@ public class Club implements Persistable { @Id private String id; + @Indexed(name = "uk_club_name_not_blank", unique = true, + partialFilter = "{ \"name\": { \"$exists\": true, \"$nin\": [null, \"\"] } }") private String name; private String category; diff --git a/backend/src/main/java/moadong/club/repository/ClubRepository.java b/backend/src/main/java/moadong/club/repository/ClubRepository.java index c558bfa10..f371fa5da 100644 --- a/backend/src/main/java/moadong/club/repository/ClubRepository.java +++ b/backend/src/main/java/moadong/club/repository/ClubRepository.java @@ -23,4 +23,6 @@ public interface ClubRepository extends MongoRepository { Optional findClubByName(String name); + boolean existsByNameAndIdNot(String name, String id); + } diff --git a/backend/src/main/java/moadong/club/service/ClubProfileService.java b/backend/src/main/java/moadong/club/service/ClubProfileService.java index 98349b5f0..00a2f5778 100644 --- a/backend/src/main/java/moadong/club/service/ClubProfileService.java +++ b/backend/src/main/java/moadong/club/service/ClubProfileService.java @@ -20,6 +20,7 @@ import moadong.user.payload.CustomUserDetails; import org.bson.types.ObjectId; import org.javers.core.Javers; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,8 +43,9 @@ public class ClubProfileService { public void updateClubInfo(ClubInfoRequest request, CustomUserDetails user) { Club club = clubRepository.findClubByUserId(user.getId()) .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); + validateClubNameUnique(club.getId(), request.name()); club.update(request); - Club saved = clubRepository.save(club); + Club saved = saveClub(club); javers.commit(user.getUsername(), saved); } @@ -109,8 +111,9 @@ public void updateClubInfoByClubId(String clubId, ClubInfoRequest request, Custo ObjectId objectId = ObjectIdConverter.convertString(clubId); Club club = clubRepository.findClubById(objectId) .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); + validateClubNameUnique(club.getId(), request.name()); club.update(request); - Club saved = clubRepository.save(club); + Club saved = saveClub(club); javers.commit(user.getUsername(), saved); } @@ -138,4 +141,23 @@ public void updateClubRecruitmentInfoByClubId(String clubId, ClubRecruitmentInfo ); } } + + private void validateClubNameUnique(String clubId, String name) { + if (name == null || name.isBlank()) { + return; + } + + if (clubRepository.existsByNameAndIdNot(name, clubId)) { + throw new RestApiException(ErrorCode.CLUB_NAME_ALREADY_EXISTS); + } + } + + // 레이스 컨디션을 위한 시큐어 코딩 + private Club saveClub(Club club) { + try { + return clubRepository.save(club); + } catch (DuplicateKeyException e) { + throw new RestApiException(ErrorCode.CLUB_NAME_ALREADY_EXISTS); + } + } } diff --git a/backend/src/main/java/moadong/global/exception/ErrorCode.java b/backend/src/main/java/moadong/global/exception/ErrorCode.java index 2d31d706d..42cf5ec68 100644 --- a/backend/src/main/java/moadong/global/exception/ErrorCode.java +++ b/backend/src/main/java/moadong/global/exception/ErrorCode.java @@ -18,6 +18,7 @@ public enum ErrorCode { TOO_MANY_TAGS(HttpStatus.BAD_REQUEST, "600-8", "태그는 최대 3개까지 입력할 수 있습니다."), TOO_LONG_TAG(HttpStatus.BAD_REQUEST, "600-9", "태그는 최대 5글자까지 입력할 수 있습니다."), TOO_LONG_INTRODUCTION(HttpStatus.BAD_REQUEST, "600-10", "소개는 최대 24글자까지 입력할 수 있습니다."), + CLUB_NAME_ALREADY_EXISTS(HttpStatus.CONFLICT, "600-11", "이미 사용 중인 동아리 이름입니다."), // 601xx: 파일/미디어 관련 오류 IMAGE_UPLOAD_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "601-1", "이미지 업로드에 실패하였습니다."), From 67e7dbcc62ef4bba652c650793eafc6256174770 Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 16 Feb 2026 17:03:00 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20partialFilter=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=ED=95=98=EC=A7=80=EC=95=8A=EC=9D=80=20=ED=91=9C=ED=98=84?= =?UTF-8?q?=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nin을 partialFilter에서 지원하지않음 --- backend/src/main/java/moadong/club/entity/Club.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/moadong/club/entity/Club.java b/backend/src/main/java/moadong/club/entity/Club.java index c2826911f..9f1296591 100644 --- a/backend/src/main/java/moadong/club/entity/Club.java +++ b/backend/src/main/java/moadong/club/entity/Club.java @@ -29,7 +29,7 @@ public class Club implements Persistable { private String id; @Indexed(name = "uk_club_name_not_blank", unique = true, - partialFilter = "{ \"name\": { \"$exists\": true, \"$nin\": [null, \"\"] } }") + partialFilter = "{ \"name\": { \"$type\": \"string\", \"$gt\": \"\" } }") private String name; private String category; From 6776fc5ef6306a9b6b1c3ed337282704946571ca Mon Sep 17 00:00:00 2001 From: lepitaaar Date: Mon, 23 Feb 2026 14:48:38 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EA=B2=80=EC=83=89=20con?= =?UTF-8?q?troller=20endpoint=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/moadong/club/controller/ClubProfileController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/moadong/club/controller/ClubProfileController.java b/backend/src/main/java/moadong/club/controller/ClubProfileController.java index cc5f20f1c..f57e54c7a 100644 --- a/backend/src/main/java/moadong/club/controller/ClubProfileController.java +++ b/backend/src/main/java/moadong/club/controller/ClubProfileController.java @@ -31,7 +31,7 @@ public ResponseEntity getClubDetail(@PathVariable String clubId) { return Response.ok(clubDetailedPageResponse); } - @GetMapping("/{clubName:@[^/]+}") + @GetMapping("/@{clubName}") @Operation(summary = "클럽 상세 정보 조회", description = "클럽 이름을 이용해 상세 정보를 조회합니다.") public ResponseEntity getClubDetailByClubName(@PathVariable String clubName) { ClubDetailedResponse clubDetailedResponse = clubProfileService.getClubDetailByClubName(clubName);