From 04e66c4ab832dec9486f901d71495fdd5ea1865f Mon Sep 17 00:00:00 2001 From: redjungi Date: Sat, 18 Apr 2026 01:19:04 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20GLOBAL=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conflict/CannotQuitGlobalGroupException.java | 10 ++++++++++ .../group/exception/statuscode/GroupStatusCode.java | 6 ++++++ .../group/port/out/GroupRepositoryPort.java | 2 ++ .../clash/domain/group/enums/GroupCategory.java | 3 ++- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/process/clash/application/group/exception/exception/conflict/CannotQuitGlobalGroupException.java diff --git a/src/main/java/com/process/clash/application/group/exception/exception/conflict/CannotQuitGlobalGroupException.java b/src/main/java/com/process/clash/application/group/exception/exception/conflict/CannotQuitGlobalGroupException.java new file mode 100644 index 000000000..1c07b5762 --- /dev/null +++ b/src/main/java/com/process/clash/application/group/exception/exception/conflict/CannotQuitGlobalGroupException.java @@ -0,0 +1,10 @@ +package com.process.clash.application.group.exception.exception.conflict; + +import com.process.clash.application.common.exception.exception.ConflictException; +import com.process.clash.application.group.exception.statuscode.GroupStatusCode; + +public class CannotQuitGlobalGroupException extends ConflictException { + public CannotQuitGlobalGroupException() { + super(GroupStatusCode.CANNOT_QUIT_GLOBAL_GROUP); + } +} diff --git a/src/main/java/com/process/clash/application/group/exception/statuscode/GroupStatusCode.java b/src/main/java/com/process/clash/application/group/exception/statuscode/GroupStatusCode.java index cdc324027..c452c9e80 100644 --- a/src/main/java/com/process/clash/application/group/exception/statuscode/GroupStatusCode.java +++ b/src/main/java/com/process/clash/application/group/exception/statuscode/GroupStatusCode.java @@ -67,6 +67,12 @@ public enum GroupStatusCode implements StatusCode { "DUPLICATE_GROUP_NAME", "이미 사용 중인 그룹 이름입니다.", HttpStatus.CONFLICT + ), + + CANNOT_QUIT_GLOBAL_GROUP( + "CANNOT_QUIT_GLOBAL_GROUP", + "전체 유저 그룹에서는 탈퇴할 수 없습니다.", + HttpStatus.CONFLICT ); private final String code; diff --git a/src/main/java/com/process/clash/application/group/port/out/GroupRepositoryPort.java b/src/main/java/com/process/clash/application/group/port/out/GroupRepositoryPort.java index 85c7ec2de..7e817dcca 100644 --- a/src/main/java/com/process/clash/application/group/port/out/GroupRepositoryPort.java +++ b/src/main/java/com/process/clash/application/group/port/out/GroupRepositoryPort.java @@ -13,6 +13,8 @@ public interface GroupRepositoryPort { Optional findById(Long groupId); + Optional findByCategory(GroupCategory category); + void deleteById(Long groupId); PageResult findAllByPage(Integer page, Integer size); diff --git a/src/main/java/com/process/clash/domain/group/enums/GroupCategory.java b/src/main/java/com/process/clash/domain/group/enums/GroupCategory.java index 1283491cd..387aa7d7f 100644 --- a/src/main/java/com/process/clash/domain/group/enums/GroupCategory.java +++ b/src/main/java/com/process/clash/domain/group/enums/GroupCategory.java @@ -5,5 +5,6 @@ public enum GroupCategory { CLUB, TEAM, NARSHA, - ETC + ETC, + GLOBAL } From a6c78ace98bb36b5882ecfa63cef3d38d2d1dba8 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sat, 18 Apr 2026 01:19:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EA=B7=B8=EB=A3=B9=20=ED=83=88=ED=87=B4/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C/=EC=88=98=EC=A0=95=20=EC=B0=A8=EB=8B=A8=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20=EA=B0=80=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/group/util/GroupCategoryConverter.java | 4 ++++ .../clash/application/group/policy/GroupPolicy.java | 11 +++++++++++ .../application/group/service/DeleteGroupService.java | 1 + .../application/group/service/UpdateGroupService.java | 1 + .../user/user/service/VerifyEmailService.java | 9 ++++++++- 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/process/clash/adapter/web/group/util/GroupCategoryConverter.java b/src/main/java/com/process/clash/adapter/web/group/util/GroupCategoryConverter.java index b864e6cb0..61006def2 100644 --- a/src/main/java/com/process/clash/adapter/web/group/util/GroupCategoryConverter.java +++ b/src/main/java/com/process/clash/adapter/web/group/util/GroupCategoryConverter.java @@ -25,6 +25,10 @@ public static GroupCategory toCategoryFilter(String category) { return null; } + if ("GLOBAL".equalsIgnoreCase(category)) { + throw new InvalidCategoryException("유효하지 않은 카테고리입니다: 'GLOBAL'"); + } + try { return GroupCategory.valueOf(category.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { diff --git a/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java b/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java index 000416d2e..a44cb7ec6 100644 --- a/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java +++ b/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java @@ -2,11 +2,13 @@ import com.process.clash.application.common.actor.Actor; import com.process.clash.application.common.exception.exception.ValidationException; +import com.process.clash.application.group.exception.exception.conflict.CannotQuitGlobalGroupException; import com.process.clash.application.group.exception.exception.conflict.GroupDuplicateNameException; import com.process.clash.application.group.exception.exception.conflict.GroupMemberLimitReachedException; import com.process.clash.application.group.exception.exception.conflict.GroupOwnerCannotQuitException; import com.process.clash.application.group.exception.exception.forbidden.GroupAccessDeniedException; import com.process.clash.domain.group.entity.Group; +import com.process.clash.domain.group.enums.GroupCategory; import org.springframework.stereotype.Component; @Component @@ -43,8 +45,17 @@ public void validateMemberLimit(int maxMembers, int memberCount) { } public void canQuitGroup(Actor actor, Group group) { + if (group.category() == GroupCategory.GLOBAL) { + throw new CannotQuitGlobalGroupException(); + } if (group.ownerId().equals(actor.id())) { throw new GroupOwnerCannotQuitException(); } } + + public void validateNotGlobal(Group group) { + if (group.category() == GroupCategory.GLOBAL) { + throw new GroupAccessDeniedException(); + } + } } diff --git a/src/main/java/com/process/clash/application/group/service/DeleteGroupService.java b/src/main/java/com/process/clash/application/group/service/DeleteGroupService.java index 590809003..999d00bcc 100644 --- a/src/main/java/com/process/clash/application/group/service/DeleteGroupService.java +++ b/src/main/java/com/process/clash/application/group/service/DeleteGroupService.java @@ -26,6 +26,7 @@ public void execute(DeleteGroupData.Command command) { Group group = groupRepositoryPort.findById(command.groupId()) .orElseThrow(GroupNotFoundException::new); + groupPolicy.validateNotGlobal(group); groupPolicy.validateOwnership(command.actor(), group); List memberUserIds = groupRepositoryPort.findMemberUserIdsByGroupIds(List.of(group.id())); groupRepositoryPort.deleteById(group.id()); diff --git a/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java b/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java index 57451bbcf..5ce1e7cc1 100644 --- a/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java +++ b/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java @@ -30,6 +30,7 @@ public void execute(UpdateGroupData.Command command) { .orElseThrow(GroupNotFoundException::new); int currentMemberCount = groupRepositoryPort.countMembers(command.groupId()); + policy.validateNotGlobal(group); policy.validateOwnership(command.actor(), group); policy.validateMaxMembers(command.maxMembers()); policy.validateMemberLimit(group.maxMembers(), currentMemberCount); diff --git a/src/main/java/com/process/clash/application/user/user/service/VerifyEmailService.java b/src/main/java/com/process/clash/application/user/user/service/VerifyEmailService.java index 13b0ec934..42ff91eb2 100644 --- a/src/main/java/com/process/clash/application/user/user/service/VerifyEmailService.java +++ b/src/main/java/com/process/clash/application/user/user/service/VerifyEmailService.java @@ -1,5 +1,6 @@ package com.process.clash.application.user.user.service; +import com.process.clash.application.group.port.out.GroupRepositoryPort; import com.process.clash.application.mail.port.out.VerificationCodePort; import com.process.clash.application.user.user.data.VerifyEmailData; import com.process.clash.application.user.user.exception.exception.badrequest.VerificationCodeExpiredOrWrongEmailException; @@ -10,6 +11,7 @@ import com.process.clash.application.user.user.port.in.VerifyEmailUseCase; import com.process.clash.application.user.user.port.out.PendingUserCachePort; import com.process.clash.application.user.user.port.out.UserRepositoryPort; +import com.process.clash.domain.group.enums.GroupCategory; import com.process.clash.domain.user.user.entity.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,6 +24,7 @@ public class VerifyEmailService implements VerifyEmailUseCase { private final VerificationCodePort verificationCodePort; private final UserRepositoryPort userRepositoryPort; private final PendingUserCachePort pendingUserCachePort; + private final GroupRepositoryPort groupRepositoryPort; @Override @Transactional @@ -46,7 +49,11 @@ public void execute(VerifyEmailData.Command command) { } // 인증 완료 처리 후 DB 저장 - userRepositoryPort.save(pendingUser.active()); + User savedUser = userRepositoryPort.save(pendingUser.active()); + + // 전체 유저 그룹 자동 가입 + groupRepositoryPort.findByCategory(GroupCategory.GLOBAL) + .ifPresent(g -> groupRepositoryPort.addMember(g.id(), savedUser.id())); // Redis 정리 pendingUserCachePort.delete(command.token()); From ae7759f0efefb69645685232068b9fc7e737dd76 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sat, 18 Apr 2026 01:20:22 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EA=B7=B8=EB=A3=B9=20JPA=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=20=EB=B0=8F=20flyway=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=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 --- .../persistence/group/GroupJpaEntity.java | 4 ++-- .../persistence/group/GroupJpaMapper.java | 2 +- .../persistence/group/GroupJpaRepository.java | 9 +++++++-- .../group/GroupPersistenceAdapter.java | 16 +++++++++++++--- .../db/migration/V39__add_global_user_group.sql | 12 ++++++++++++ 5 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/db/migration/V39__add_global_user_group.sql diff --git a/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaEntity.java b/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaEntity.java index 7aa31493e..611e214a2 100644 --- a/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaEntity.java +++ b/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaEntity.java @@ -61,7 +61,7 @@ public class GroupJpaEntity { @Enumerated(EnumType.STRING) private GroupCategory category; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "fk_user_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY, optional = true) + @JoinColumn(name = "fk_user_id", nullable = true) private UserJpaEntity owner; } diff --git a/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaMapper.java b/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaMapper.java index f4d63aa8b..8f725f931 100644 --- a/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaMapper.java +++ b/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaMapper.java @@ -38,7 +38,7 @@ public Group toDomain(GroupJpaEntity groupJpaEntity) { groupJpaEntity.getPassword(), groupJpaEntity.getPasswordRequired(), groupJpaEntity.getCategory(), - groupJpaEntity.getOwner().getId() + groupJpaEntity.getOwner() != null ? groupJpaEntity.getOwner().getId() : null ); } } diff --git a/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaRepository.java b/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaRepository.java index 4937fa0ff..8ea4676f8 100644 --- a/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaRepository.java +++ b/src/main/java/com/process/clash/adapter/persistence/group/GroupJpaRepository.java @@ -14,7 +14,8 @@ public interface GroupJpaRepository extends JpaRepository { @EntityGraph(attributePaths = {"owner"}) - Page findAll(Pageable pageable); + @Query("select g from GroupJpaEntity g where g.category != :excluded") + Page findAllExcluding(@Param("excluded") GroupCategory excluded, Pageable pageable); @EntityGraph(attributePaths = {"owner"}) Page findAllByCategory(GroupCategory category, Pageable pageable); @@ -22,6 +23,9 @@ public interface GroupJpaRepository extends JpaRepository @EntityGraph(attributePaths = {"owner"}) Optional findById(Long id); + @EntityGraph(attributePaths = {"owner"}) + Optional findByCategory(GroupCategory category); + boolean existsByName(String name); @EntityGraph(attributePaths = {"owner"}) @@ -30,8 +34,9 @@ public interface GroupJpaRepository extends JpaRepository from GroupJpaEntity g join GroupMemberJpaEntity gm on gm.group = g where gm.user.id = :userId + and g.category != :excluded """) - Page findAllByMemberUserId(@Param("userId") Long userId, Pageable pageable); + Page findAllByMemberUserId(@Param("userId") Long userId, @Param("excluded") GroupCategory excluded, Pageable pageable); @EntityGraph(attributePaths = {"owner"}) @Query(""" diff --git a/src/main/java/com/process/clash/adapter/persistence/group/GroupPersistenceAdapter.java b/src/main/java/com/process/clash/adapter/persistence/group/GroupPersistenceAdapter.java index 30b1a01cb..ab90a7ca0 100644 --- a/src/main/java/com/process/clash/adapter/persistence/group/GroupPersistenceAdapter.java +++ b/src/main/java/com/process/clash/adapter/persistence/group/GroupPersistenceAdapter.java @@ -31,12 +31,19 @@ public class GroupPersistenceAdapter implements GroupRepositoryPort { @Override public Group save(Group group) { - UserJpaEntity owner = userJpaRepository.getReferenceById(group.ownerId()); + UserJpaEntity owner = group.ownerId() != null + ? userJpaRepository.getReferenceById(group.ownerId()) + : null; GroupJpaEntity entity = groupJpaMapper.toJpaEntity(group, owner); GroupJpaEntity saved = groupJpaRepository.save(entity); return groupJpaMapper.toDomain(saved); } + @Override + public Optional findByCategory(GroupCategory category) { + return groupJpaRepository.findByCategory(category).map(groupJpaMapper::toDomain); + } + @Override public Optional findById(Long groupId) { return groupJpaRepository.findById(groupId) @@ -52,7 +59,7 @@ public void deleteById(Long groupId) { @Override public PageResult findAllByPage(Integer page, Integer size) { Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt")); - Page pageResult = groupJpaRepository.findAll(pageable); + Page pageResult = groupJpaRepository.findAllExcluding(GroupCategory.GLOBAL, pageable); return mapPageResult(pageResult); } @@ -66,12 +73,15 @@ public PageResult findAllByPageAndCategory(Integer page, Integer size, GroupCate @Override public PageResult findAllByMemberUserId(Long userId, Integer page, Integer size) { Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt")); - Page pageResult = groupJpaRepository.findAllByMemberUserId(userId, pageable); + Page pageResult = groupJpaRepository.findAllByMemberUserId(userId, GroupCategory.GLOBAL, pageable); return mapPageResult(pageResult); } @Override public PageResult findAllByMemberUserIdAndCategory(Long userId, Integer page, Integer size, GroupCategory category) { + if (category == GroupCategory.GLOBAL) { + return new PageResult(List.of(), 0); + } Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt")); Page pageResult = groupJpaRepository.findAllByMemberUserIdAndCategory(userId, category, pageable); return mapPageResult(pageResult); diff --git a/src/main/resources/db/migration/V39__add_global_user_group.sql b/src/main/resources/db/migration/V39__add_global_user_group.sql new file mode 100644 index 000000000..de823482e --- /dev/null +++ b/src/main/resources/db/migration/V39__add_global_user_group.sql @@ -0,0 +1,12 @@ +-- 1. groups 테이블의 fk_user_id 컬럼을 nullable로 변경 +ALTER TABLE groups ALTER COLUMN fk_user_id DROP NOT NULL; + +-- 2. 전체 유저 그룹 생성 (owner 없음) +INSERT INTO groups (created_at, updated_at, name, description, max_members, password, password_required, category, fk_user_id) +VALUES (NOW(), NOW(), '전체 유저', '모든 클래시 유저가 자동으로 참여하는 그룹입니다.', 2147483647, '', false, 'GLOBAL', NULL); + +-- 3. 기존 활성 유저 전원을 GLOBAL 그룹에 추가 +INSERT INTO group_members (fk_group_id, fk_user_id) +SELECT (SELECT id FROM groups WHERE category = 'GLOBAL'), u.id +FROM users u +WHERE u.user_status = 'ACTIVE'; From eacc3183f849eef0d1c59ff9f9777436c8c5cd01 Mon Sep 17 00:00:00 2001 From: redjungi Date: Fri, 8 May 2026 10:44:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20flyway=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B2=84=EC=A0=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...__add_global_user_group.sql => V42__add_global_user_group.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V39__add_global_user_group.sql => V42__add_global_user_group.sql} (100%) diff --git a/src/main/resources/db/migration/V39__add_global_user_group.sql b/src/main/resources/db/migration/V42__add_global_user_group.sql similarity index 100% rename from src/main/resources/db/migration/V39__add_global_user_group.sql rename to src/main/resources/db/migration/V42__add_global_user_group.sql From 9aad85770b1ae46a359f805fe2b133199675ec57 Mon Sep 17 00:00:00 2001 From: redjungi Date: Sat, 9 May 2026 17:27:48 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EA=B7=B8=EB=A3=B9=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=EB=A5=BC=20GLOBAL=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=98=EB=8A=94=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process/clash/application/group/policy/GroupPolicy.java | 6 ++++++ .../clash/application/group/service/UpdateGroupService.java | 1 + 2 files changed, 7 insertions(+) diff --git a/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java b/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java index a44cb7ec6..94e623283 100644 --- a/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java +++ b/src/main/java/com/process/clash/application/group/policy/GroupPolicy.java @@ -58,4 +58,10 @@ public void validateNotGlobal(Group group) { throw new GroupAccessDeniedException(); } } + + public void validateNotGlobal(GroupCategory category) { + if (category == GroupCategory.GLOBAL) { + throw new GroupAccessDeniedException(); + } + } } diff --git a/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java b/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java index 5ce1e7cc1..552207b73 100644 --- a/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java +++ b/src/main/java/com/process/clash/application/group/service/UpdateGroupService.java @@ -31,6 +31,7 @@ public void execute(UpdateGroupData.Command command) { int currentMemberCount = groupRepositoryPort.countMembers(command.groupId()); policy.validateNotGlobal(group); + policy.validateNotGlobal(command.category()); policy.validateOwnership(command.actor(), group); policy.validateMaxMembers(command.maxMembers()); policy.validateMemberLimit(group.maxMembers(), currentMemberCount);