Repository: atwoz-dev/atwoz_server
Pull request #210: 셀프 소개 공개/비공개 변경 구현
Author: login: Kong-TaeHyeon
Labels: name: enhancement
description: New feature or request
Created: 2025-05-24T07:10:36Z | Merged: 2025-05-26T06:09:47Z

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **신규 기능**
  - 관리자가 자기소개를 공개 또는 비공개로 변경할 수 있는 PATCH API 엔드포인트가 추가되었습니다.
  - 관리자가 자기소개 목록을 조회할 수 있는 관리용 GET API가 추가되었습니다.

- **버그 수정**
  - 없음

- **테스트**
  - 자기소개 공개/비공개 상태 변경에 대한 서비스 및 도메인 단위 테스트가 추가되었습니다.

- **문서화**
  - 관리용 API에 Swagger 태그가 추가되어 문서화가 개선되었습니다.

- **리팩터**
  - 패키지 구조 및 import 경로가 통일되고 정리되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

## 참고 자료
- 인호님께서 이벤트를 검증하시는 테스트 코드를 참고하여 작성하였습니다.

## 노트
셀프 소개의 상태(공개/비공개)를 관리자가 업데이트하는 기능을 위해서, Event를 추가하고 관리자는 단순히 이벤트를 발행하는 방식을 통해,
셀프 소개 도메인과 관리자 도메인 사이의 결합도를 분리하고자 하였습니다! 


Diff:
diff --git a/src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java b/src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
deleted file mode 100644
index 9c8723060..000000000
--- a/src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package atwoz.atwoz.admin.presentation.selfintroduction;
-
-import atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionQueryRepository;
-import atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionSearchCondition;
-import atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionView;
-import atwoz.atwoz.common.enums.StatusType;
-import atwoz.atwoz.common.response.BaseResponse;
-import lombok.RequiredArgsConstructor;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.web.PageableDefault;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/admin/selfintroduction")
-@RequiredArgsConstructor
-public class AdminSelfIntroductionController {
-
-    private final AdminSelfIntroductionQueryRepository adminSelfIntroductionQueryRepository;
-
-    @GetMapping
-    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
-        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
-        @PageableDefault(size = 100) Pageable pageable
-    ) {
-        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
-            adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
-    }
-
-}
diff --git a/src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java b/src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java
index 193d53ce0..26b317d77 100644
--- a/src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java
+++ b/src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java
@@ -40,6 +40,16 @@ public void delete(Long id, Long memberId) {
         selfIntroductionCommandRepository.deleteById(id);
     }
 
+    @Transactional
+    public void changeOpenStatus(Long id, boolean open) {
+        SelfIntroduction selfIntroduction = getSelfIntroductionById(id);
+        if (open) {
+            selfIntroduction.open();
+        } else {
+            selfIntroduction.close();
+        }
+    }
+
     private void validateSelfIntroductionAuthor(Long memberIdFromSelfIntroduction, Long memberId) {
         if (memberIdFromSelfIntroduction != memberId) {
             throw new NotSelfIntroductionAuthorException();
diff --git a/src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java b/src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java
index a0c3db651..a3a1c799b 100644
--- a/src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java
+++ b/src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java
@@ -52,7 +52,11 @@ public void update(String title, String content) {
     }
 
     public void close() {
-        isOpened = Boolean.FALSE;
+        isOpened = false;
+    }
+
+    public void open() {
+        isOpened = true;
     }
 
     private void setTitle(@NonNull String title) {
diff --git a/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
new file mode 100644
index 000000000..274eb10ba
--- /dev/null
+++ b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
@@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.AdminSelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final AdminSelfIntroductionQueryRepository adminSelfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
+
+    @Operation(summary = "셀프 소개 공개 전환 API")
+    @PatchMapping("/{id}/open")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
+        selfIntroductionService.changeOpenStatus(id, true);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
+
+    @Operation(summary = "셀프 소개 비공개 전환 API")
+    @PatchMapping("/{id}/close")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToClose(@PathVariable Long id) {
+        selfIntroductionService.changeOpenStatus(id, false);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
+}
diff --git a/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java
index 01b323ae7..2e98e9282 100644
--- a/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java
+++ b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java
@@ -5,10 +5,10 @@
 import atwoz.atwoz.common.enums.StatusType;
 import atwoz.atwoz.common.response.BaseResponse;
 import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionSearchCondition;
 import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionSearchRequest;
 import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionWriteRequest;
 import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
-import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionSearchCondition;
 import atwoz.atwoz.community.query.selfintroduction.view.SelfIntroductionSummaryView;
 import atwoz.atwoz.community.query.selfintroduction.view.SelfIntroductionView;
 import lombok.RequiredArgsConstructor;
diff --git a/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java
index 4c02077b3..cfc87fdee 100644
--- a/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java
+++ b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java
@@ -1,7 +1,7 @@
 package atwoz.atwoz.community.presentation.selfintroduction;
 
+import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionSearchCondition;
 import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionSearchRequest;
-import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionSearchCondition;
 import atwoz.atwoz.member.command.domain.member.Gender;
 import atwoz.atwoz.member.query.member.AgeConverter;
 
diff --git a/src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionSearchCondition.java b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/AdminSelfIntroductionSearchCondition.java
similarity index 75%
rename from src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionSearchCondition.java
rename to src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/AdminSelfIntroductionSearchCondition.java
index 8f3d11d11..c7f81c132 100644
--- a/src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionSearchCondition.java
+++ b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/AdminSelfIntroductionSearchCondition.java
@@ -1,4 +1,4 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.presentation.selfintroduction.dto;
 
 import java.time.LocalDate;
 
diff --git a/src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionSearchCondition.java b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/SelfIntroductionSearchCondition.java
similarity index 81%
rename from src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionSearchCondition.java
rename to src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/SelfIntroductionSearchCondition.java
index 214052017..a07739209 100644
--- a/src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionSearchCondition.java
+++ b/src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/SelfIntroductionSearchCondition.java
@@ -1,4 +1,4 @@
-package atwoz.atwoz.community.query.selfintroduction;
+package atwoz.atwoz.community.presentation.selfintroduction.dto;
 
 import atwoz.atwoz.member.command.domain.member.City;
 import atwoz.atwoz.member.command.domain.member.Gender;
diff --git a/src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepository.java b/src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
similarity index 74%
rename from src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
rename to src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
index 68a4d21fd..beab8b79a 100644
--- a/src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
+++ b/src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
@@ -1,6 +1,10 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.query.selfintroduction;
 
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import atwoz.atwoz.community.query.selfintroduction.view.QAdminSelfIntroductionView;
 import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.Expressions;
 import com.querydsl.jpa.impl.JPAQueryFactory;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.domain.Page;
@@ -18,10 +22,14 @@
 @Repository
 @RequiredArgsConstructor
 public class AdminSelfIntroductionQueryRepository {
+
     private final JPAQueryFactory queryFactory;
 
     public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
         Pageable pageable) {
+
+        BooleanExpression searchCondition = buildAdminSearchCondition(condition);
+
         List<AdminSelfIntroductionView> content = queryFactory
             .select(
                 new QAdminSelfIntroductionView(
@@ -37,13 +45,7 @@ public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroducti
             )
             .from(selfIntroduction)
             .join(member).on(member.id.eq(selfIntroduction.memberId))
-            .where(
-                nicknameEq(condition.nickname()),
-                isOpenedEq(condition.isOpened()),
-                startDateGoe(condition.startDate()),
-                loeEndDate(condition.endDate()),
-                phoneNumberEq(condition.phoneNumber())
-            )
+            .where(searchCondition)
             .offset(pageable.getOffset())
             .limit(pageable.getPageSize())
             .fetch();
@@ -53,19 +55,23 @@ public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroducti
                 .select(selfIntroduction.count())
                 .from(selfIntroduction)
                 .join(member).on(member.id.eq(selfIntroduction.memberId))
-                .where(
-                    nicknameEq(condition.nickname()),
-                    isOpenedEq(condition.isOpened()),
-                    startDateGoe(condition.startDate()),
-                    loeEndDate(condition.endDate()),
-                    phoneNumberEq(condition.phoneNumber())
-                )
+                .where(searchCondition)
                 .fetchOne()
         ).orElse(0L);
 
         return new PageImpl<>(content, pageable, totalCount);
     }
 
+    private BooleanExpression buildAdminSearchCondition(AdminSelfIntroductionSearchCondition condition) {
+        return Expressions.allOf(
+            nicknameEq(condition.nickname()),
+            isOpenedEq(condition.isOpened()),
+            startDateGoe(condition.startDate()),
+            loeEndDate(condition.endDate()),
+            phoneNumberEq(condition.phoneNumber())
+        );
+    }
+
     private BooleanExpression nicknameEq(String nickname) {
         if (nickname == null || nickname.isBlank()) {
             return null;
@@ -91,7 +97,7 @@ private BooleanExpression loeEndDate(LocalDate endDate) {
         if (endDate == null) {
             return null;
         }
-        return selfIntroduction.createdAt.loe(endDate.plusDays(1).atStartOfDay().minusSeconds(1));
+        return selfIntroduction.createdAt.lt(endDate.plusDays(1).atStartOfDay());
     }
 
     private BooleanExpression phoneNumberEq(String phoneNumber) {
diff --git a/src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java b/src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
index b5eea105c..f57aa87ea 100644
--- a/src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
+++ b/src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
@@ -1,5 +1,6 @@
 package atwoz.atwoz.community.query.selfintroduction;
 
+import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionSearchCondition;
 import atwoz.atwoz.community.query.selfintroduction.view.QSelfIntroductionSummaryView;
 import atwoz.atwoz.community.query.selfintroduction.view.QSelfIntroductionView;
 import atwoz.atwoz.community.query.selfintroduction.view.SelfIntroductionSummaryView;
diff --git a/src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionView.java b/src/main/java/atwoz/atwoz/community/query/selfintroduction/view/AdminSelfIntroductionView.java
similarity index 94%
rename from src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionView.java
rename to src/main/java/atwoz/atwoz/community/query/selfintroduction/view/AdminSelfIntroductionView.java
index dcf03f45a..5676c78b4 100644
--- a/src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionView.java
+++ b/src/main/java/atwoz/atwoz/community/query/selfintroduction/view/AdminSelfIntroductionView.java
@@ -1,4 +1,4 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.query.selfintroduction.view;
 
 import com.querydsl.core.annotations.QueryProjection;
 
diff --git a/src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java b/src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java
index 66573c6c6..0158da1ae 100644
--- a/src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java
+++ b/src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java
@@ -2,7 +2,7 @@
 
 import atwoz.atwoz.member.query.profileimage.view.ProfileImageView;
 import atwoz.atwoz.member.query.profileimage.view.QProfileImageView;
-import com.querydsl.jpa.JPQLQueryFactory;
+import com.querydsl.jpa.impl.JPAQueryFactory;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
 
@@ -13,7 +13,7 @@
 @Repository
 @RequiredArgsConstructor
 public class ProfileImageQueryRepository {
-    private final JPQLQueryFactory queryFactory;
+    private final JPAQueryFactory queryFactory;
 
     public List<ProfileImageView> findByMemberId(Long memberId) {
         return queryFactory
diff --git a/src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java b/src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java
index b9e461712..b525b0b42 100644
--- a/src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java
+++ b/src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java
@@ -254,5 +254,65 @@ void deleteSelfIntroduction() {
         }
     }
 
+    @Nested
+    @DisplayName("셀프 소개 공개 여부 변경")
+    class UpdateSelfIntroduction {
+        @Test
+        @DisplayName("변경하고자 하는 셀프 소개가 존재하지 않는 경우, 예외 발생")
+        void throwExceptionWhenSelfIntroductionNotFound() {
+            // Given
+            Long selfIntroductionId = 1L;
+            Mockito.when(selfIntroductionCommandRepository.findById(selfIntroductionId))
+                .thenReturn(Optional.empty());
+
+            // When & Then
+            Assertions.assertThatThrownBy(() -> selfIntroductionService.changeOpenStatus(selfIntroductionId, false))
+                .isInstanceOf(SelfIntroductionNotFoundException.class);
+        }
+
+        @Test
+        @DisplayName("셀프 소개를 공개로 변경한다.")
+        void changeToOpen() {
+            // Given
+            Long selfIntroductionId = 1L;
+            Long memberId = 2L;
+            SelfIntroduction selfIntroduction = SelfIntroduction.write(
+                memberId,
+                "셀프 소개 제목",
+                "셀프 소개 내용입니다. 최소 내용이 30자 이상입니다~!!! (30자 이상)"
+            );
+            selfIntroduction.close();
+
+            Mockito.when(selfIntroductionCommandRepository.findById(selfIntroductionId))
+                .thenReturn(Optional.of(selfIntroduction));
 
+            // When
+            selfIntroductionService.changeOpenStatus(selfIntroductionId, true);
+
+            // Then
+            Assertions.assertThat(selfIntroduction.isOpened()).isTrue();
+        }
+
+        @Test
+        @DisplayName("셀프 소개를 비공개로 변경한다.")
+        void changeToClose() {
+            // Given
+            Long selfIntroductionId = 1L;
+            Long memberId = 2L;
+            SelfIntroduction selfIntroduction = SelfIntroduction.write(
+                memberId,
+                "셀프 소개 제목",
+                "셀프 소개 내용입니다. 최소 내용이 30자 이상입니다~!!! (30자 이상)"
+            );
+
+            Mockito.when(selfIntroductionCommandRepository.findById(selfIntroductionId))
+                .thenReturn(Optional.of(selfIntroduction));
+
+            // When
+            selfIntroductionService.changeOpenStatus(selfIntroductionId, false);
+
+            // Then
+            Assertions.assertThat(selfIntroduction.isOpened()).isFalse();
+        }
+    }
 }
diff --git a/src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java b/src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java
index a34d397a0..323cf60e2 100644
--- a/src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java
+++ b/src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java
@@ -115,5 +115,21 @@ void updateClose() {
             // Then
             assertThat(selfIntroduction.isOpened()).isFalse();
         }
+
+        @Test
+        @DisplayName("셀프 소개를 공개로 변경한다.")
+        void updateOpen() {
+            // Given
+            Long memberId = 1L;
+            String title = "셀프 소개 제목";
+            String content = "셀프 소개 내용이 공백 포함하여 최소 30자 이상이어야 합니다.";
+            SelfIntroduction selfIntroduction = SelfIntroduction.write(memberId, title, content);
+
+            // When
+            selfIntroduction.open();
+
+            // Then
+            assertThat(selfIntroduction.isOpened()).isTrue();
+        }
     }
 }
diff --git a/src/test/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepositoryTest.java b/src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java
similarity index 77%
rename from src/test/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepositoryTest.java
rename to src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java
index 9dc5f2208..3f96e2019 100644
--- a/src/test/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepositoryTest.java
+++ b/src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java
@@ -1,11 +1,15 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.query;
 
 import atwoz.atwoz.common.config.QueryDslConfig;
 import atwoz.atwoz.community.command.domain.selfintroduction.SelfIntroduction;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.AdminSelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
 import atwoz.atwoz.member.command.domain.member.Gender;
 import atwoz.atwoz.member.command.domain.member.Member;
 import atwoz.atwoz.member.command.domain.member.vo.MemberProfile;
 import atwoz.atwoz.member.command.domain.member.vo.Nickname;
+import org.assertj.core.api.AssertionsForClassTypes;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -22,11 +26,9 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
-
 @DataJpaTest
 @Import({QueryDslConfig.class, AdminSelfIntroductionQueryRepository.class})
-class AdminSelfIntroductionQueryRepositoryTest {
+public class AdminSelfIntroductionQueryRepositoryTest {
 
     private static final int PAGE_SIZE = 5;
     Member manMember;
@@ -34,9 +36,9 @@ class AdminSelfIntroductionQueryRepositoryTest {
     List<SelfIntroduction> selfIntroductionsByMan;
     List<SelfIntroduction> selfIntroductionsByWoman;
     @Autowired
-    private TestEntityManager entityManager;
-    @Autowired
     private AdminSelfIntroductionQueryRepository adminSelfIntroductionQueryRepository;
+    @Autowired
+    private TestEntityManager entityManager;
 
     @BeforeEach
     void setUp() {
@@ -87,8 +89,8 @@ void setUp() {
         entityManager.clear();
     }
 
-    @Test
     @DisplayName("모든 셀프 소개를 조회합니다.")
+    @Test
     void findAllSelfIntroductions() {
         // Given
         AdminSelfIntroductionSearchCondition condition = new AdminSelfIntroductionSearchCondition(
@@ -105,7 +107,7 @@ void findAllSelfIntroductions() {
             condition, pageRequest);
 
         // Then
-        assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(
+        AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(
             selfIntroductionsByWoman.size() + selfIntroductionsByMan.size());
     }
 
@@ -128,7 +130,7 @@ void findSelfIntroductionsByOpenedFlag() {
 
         // Then
         for (AdminSelfIntroductionView view : selfIntroductionViews.getContent()) {
-            assertThat(view.isOpened()).isTrue();
+            AssertionsForClassTypes.assertThat(view.isOpened()).isTrue();
         }
     }
 
@@ -150,9 +152,11 @@ void findSelfIntroductionsByNickname() {
             condition, pageRequest);
 
         // Then
-        assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(selfIntroductionsByMan.size());
+        AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements())
+            .isEqualTo(selfIntroductionsByMan.size());
         for (AdminSelfIntroductionView view : selfIntroductionViews.getContent()) {
-            assertThat(view.nickname()).isEqualTo(manMember.getProfile().getNickname().getValue());
+            AssertionsForClassTypes.assertThat(view.nickname())
+                .isEqualTo(manMember.getProfile().getNickname().getValue());
         }
     }
 
@@ -174,18 +178,24 @@ void findSelfIntroductionsByPhoneNumber() {
             condition, pageRequest);
 
         // Then
-        assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(PAGE_SIZE);
+        AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(PAGE_SIZE);
 
         for (int i = 0; i < PAGE_SIZE; i++) {
             SelfIntroduction selfIntroductionByWoman = selfIntroductionsByWoman.get(i);
             AdminSelfIntroductionView view = selfIntroductionViews.getContent().get(i);
-            assertThat(view.nickname()).isEqualTo(womanMember.getProfile().getNickname().getValue());
-            assertThat(view.selfIntroductionId()).isEqualTo(selfIntroductionByWoman.getId());
-            assertThat(view.gender()).isEqualTo(womanMember.getProfile().getGender().name());
-            assertThat(view.content()).isEqualTo(selfIntroductionByWoman.getContent());
-            assertThat(view.createdDate()).isEqualTo(toDate(selfIntroductionByWoman.getCreatedAt()));
-            assertThat(view.updatedDate()).isEqualTo(toDate(selfIntroductionByWoman.getUpdatedAt()));
-            assertThat(view.deletedDate()).isEqualTo(toDate(selfIntroductionByWoman.getDeletedAt()));
+            AssertionsForClassTypes.assertThat(view.nickname())
+                .isEqualTo(womanMember.getProfile().getNickname().getValue());
+            AssertionsForClassTypes.assertThat(view.selfIntroductionId())
+                .isEqualTo(selfIntroductionByWoman.getId());
+            AssertionsForClassTypes.assertThat(view.gender())
+                .isEqualTo(womanMember.getProfile().getGender().name());
+            AssertionsForClassTypes.assertThat(view.content()).isEqualTo(selfIntroductionByWoman.getContent());
+            AssertionsForClassTypes.assertThat(view.createdDate())
+                .isEqualTo(toDate(selfIntroductionByWoman.getCreatedAt()));
+            AssertionsForClassTypes.assertThat(view.updatedDate())
+                .isEqualTo(toDate(selfIntroductionByWoman.getUpdatedAt()));
+            AssertionsForClassTypes.assertThat(view.deletedDate())
+                .isEqualTo(toDate(selfIntroductionByWoman.getDeletedAt()));
         }
     }
 
@@ -207,10 +217,12 @@ void findSelfIntroductionsByStartDateAndEndDate() {
             condition, pageRequest);
 
         // Then
-        assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(PAGE_SIZE);
+        AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(PAGE_SIZE);
     }
 
     private String toDate(LocalDateTime localDateTime) {
         return localDateTime != null ? DateTimeFormatter.ofPattern("yyyy/MM/dd").format(localDateTime) : null;
     }
+
+
 }
diff --git a/src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java b/src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
index ff54d52bd..65e16e52e 100644
--- a/src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
+++ b/src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
@@ -3,8 +3,8 @@
 import atwoz.atwoz.common.config.QueryDslConfig;
 import atwoz.atwoz.common.event.Events;
 import atwoz.atwoz.community.command.domain.selfintroduction.SelfIntroduction;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.SelfIntroductionSearchCondition;
 import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
-import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionSearchCondition;
 import atwoz.atwoz.community.query.selfintroduction.view.SelfIntroductionSummaryView;
 import atwoz.atwoz.community.query.selfintroduction.view.SelfIntroductionView;
 import atwoz.atwoz.like.command.domain.Like;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java (1)</summary><blockquote>

`43-51`: **Method 구현이 올바르나 조건문 로직을 개선할 수 있습니다.**

`changeOpenStatus` method의 구현이 정확하고 기존 service layer pattern을 잘 따르고 있습니다. 다만 conditional logic을 더 간결하게 만들 수 있습니다.



다음 diff를 적용하여 코드를 더 간결하게 만들어보세요:

```diff
 @Transactional
 public void changeOpenStatus(Long id, boolean open) {
     SelfIntroduction selfIntroduction = getSelfIntroductionById(id);
-    if (open) {
-        selfIntroduction.open();
-    } else {
-        selfIntroduction.close();
-    }
+    if (open) {
+        selfIntroduction.open();
+        return;
+    }
+    selfIntroduction.close();
 }
```

또는 domain object에 `changeOpenStatus(boolean)` method를 추가하여 conditional logic을 domain layer로 이동시키는 것도 고려해볼 수 있습니다.

</blockquote></details>
<details>
<summary>src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/event/SelfIntroductionOpenStatusChangeEvent.java (1)</summary><blockquote>

`1-18`: **Event class 설계가 우수하나 parameter type 일관성을 개선하세요.**

`SelfIntroductionOpenStatusChangeEvent` class가 event-driven architecture pattern을 잘 따르고 있습니다. Immutable design과 controlled instantiation을 통한 encapsulation이 훌륭합니다.




Parameter type의 일관성을 위해 다음 수정을 고려해보세요:

```diff
- public static SelfIntroductionOpenStatusChangeEvent of(long id, boolean open) {
+ public static SelfIntroductionOpenStatusChangeEvent of(Long id, boolean open) {
```

Field는 `Long` type이지만 factory method parameter는 `long` primitive를 사용하고 있어 일관성이 부족합니다. 또는 field를 `long`으로 변경하는 것도 고려할 수 있습니다.

</blockquote></details>
<details>
<summary>src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java (1)</summary><blockquote>

`10-11`: **@Service 어노테이션 사용 검토 필요**

Infrastructure layer의 event handler에서 @Service 어노테이션을 사용하는 것보다는 @Component 어노테이션이 더 적절할 수 있습니다. @Service는 일반적으로 business logic을 담당하는 application layer에서 사용됩니다.



```diff
-@Service
+@Component
 @RequiredArgsConstructor
 public class SelfIntroductionEventHandler {
```

</blockquote></details>
<details>
<summary>src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java (1)</summary><blockquote>

`257-259`: **Test class 명명 개선 제안**

현재 class명 'UpdateSelfIntroduction'이 너무 일반적입니다. 실제로는 open status 변경만을 테스트하므로 더 구체적인 이름을 사용하는 것이 좋겠습니다.



```diff
 @Nested
 @DisplayName("셀프 소개 공개 여부 변경")
-class UpdateSelfIntroduction {
+class ChangeOpenStatus {
```

</blockquote></details>
<details>
<summary>src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java (1)</summary><blockquote>

`11-14`: **Input validation 추가 고려**

Method parameter에 대한 null check나 validation 로직 추가를 고려해보세요. 현재는 null selfIntroductionId가 전달될 경우 runtime error가 발생할 수 있습니다.



```diff
 @Transactional
 public void convertToClose(Long selfIntroductionId) {
+    if (selfIntroductionId == null) {
+        throw new IllegalArgumentException("selfIntroductionId cannot be null");
+    }
     Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
 }
```

</blockquote></details>
<details>
<summary>src/test/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandServiceTest.java (1)</summary><blockquote>

`12-12`: **Service instance 생성 방식 개선 제안**

Service를 직접 new로 생성하는 것보다 Spring test context를 활용하거나 @Mock과 @InjectMocks를 사용하는 것이 더 적절할 수 있습니다. 향후 service에 dependencies가 추가될 경우를 대비해서입니다.



```diff
+@ExtendWith(MockitoExtension.class)
 public class AdminSelfIntroductionCommandServiceTest {
 
-    AdminSelfIntroductionCommandService adminSelfIntroductionCommandService = new AdminSelfIntroductionCommandService();
+    @InjectMocks
+    private AdminSelfIntroductionCommandService adminSelfIntroductionCommandService;
```

</blockquote></details>
<details>
<summary>src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java (1)</summary><blockquote>

`14-14`: **wildcard import 사용을 지양하고 명시적 import를 사용하세요.**

wildcard import (`import org.springframework.web.bind.annotation.*;`)보다는 필요한 annotation들을 명시적으로 import하는 것이 코드의 가독성과 유지보수성 측면에서 더 좋습니다.



```diff
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b917f02994560e03b9361a3b5e2fdbb073da77cd and f06b4e4e7eb6fbbb847590ee5b23d4639e91c454.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java` (1 hunks)
* `src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java` (3 hunks)
* `src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/event/SelfIntroductionOpenStatusChangeEvent.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java` (1 hunks)
* `src/test/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandServiceTest.java` (1 hunks)
* `src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java` (1 hunks)
* `src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (2)</summary>

<details>
<summary>`**/*.java`: In addition to existing rules, review code based on: - Domain-driven design: enforce aggregate boundaries and immutable value objects   - Clean code: single responsibi...</summary>

> `**/*.java`: In addition to existing rules, review code based on: - Domain-driven design: enforce aggregate boundaries and immutable value objects   - Clean code: single responsibility methods, meaningful names, no duplication   - Object-oriented programming: SOLID(SRP, OCP, LSP, ISP, DIP) principles
> 

- `src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java`
- `src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java`
- `src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java`
- `src/test/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandServiceTest.java`
- `src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java`
- `src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java`
- `src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java`
- `src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/event/SelfIntroductionOpenStatusChangeEvent.java`
- `src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java`

</details>
<details>
<summary>`**/*Controller*.java`: Controllers should not contain business logic. Delegate to service layer. Review the API endpoints based on this guidelines.
### Richardson Maturity Model (...</summary>

> `**/*Controller*.java`: Controllers should not contain business logic. Delegate to service layer. Review the API endpoints based on this guidelines.
> ### Richardson Maturity Model (RMM) 1. Level 0: Single URI with POST requests for all actions. 2. Level 1: Separate URIs for individual resources. 3. Level 2: Use of HTTP methods (GET, POST, PUT, DELETE) to define operations on resources. 4. Level 3: Hypermedia (HATEOAS) for advanced RESTful APIs.
> ### API Conventions - URI Rules:
>   - Should be intuitive and self-explanatory.
>   - Should not map 1:1 to database tables.
>   - Must be stateless, with no session state between requests.
>   - Include "api" and version in the URI (/api/{version}/resource).
>   - Use kebab-case for URIs and camelCase for parameters and body contents.
>   - Resource identifiers should be unique and only one per URI path.
> 
> - Design Principles:
>   - APIs should be designed around resources, which are abstractions rather than direct database tables.
>   - Stateless APIs facilitate scalability and flexibility.
>   - Clear separation of frontend and backend via URI structure.
>   - Versioning in URI paths is preferred for clarity and ease of caching.
>   - Maintain consistent naming conventions across the API.
>   - Use plural forms for resource names (/users instead of /user).
>   - Complex actions can include verbs in the URI (/orders/{orderId}/cancel).
> 
> - Implementation Details:
>   - Avoid deeply nested resource paths to ensure maintainability.
>   - Ensure URIs reflect the data they provide, not the permissions or roles required to access them.
>   - Keep URIs simple and predictable, aiding both developers and automated systems.
> 

- `src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java`

</details>

</details><details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java (1)</summary><blockquote>

<details>
<summary>src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java (2)</summary>

* `Nested` (31-98)
* `DisplayName` (100-134)

</details>

</blockquote></details>
<details>
<summary>src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java (2)</summary><blockquote>

<details>
<summary>src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java (1)</summary>

* `Service` (14-66)

</details>
<details>
<summary>src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java (1)</summary>

* `Service` (8-20)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>src/test/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroductionTest.java (1)</summary>

`119-133`: **Test 구현이 우수합니다.**

새로운 `updateOpen()` test method가 올바르게 구현되었습니다. Given-When-Then pattern을 잘 따르고 있으며, 기존 test들과 일관된 구조를 유지하고 있습니다. Domain method의 behavior를 정확히 검증하고 있습니다.

</details>
<details>
<summary>src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/SelfIntroduction.java (2)</summary>

`55-56`: **Primitive boolean 사용이 적절합니다.**

`close()` method에서 primitive `boolean false`를 직접 할당하는 것이 올바른 접근입니다. 이는 performance 관점에서도 더 효율적입니다.

---

`58-60`: **Domain method 구현이 우수합니다.**

새로운 `open()` method가 domain-driven design 원칙에 맞게 잘 구현되었습니다. Domain object의 상태 변경을 명시적인 method로 제공하여 encapsulation을 유지하고 있습니다.

</details>
<details>
<summary>src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java (1)</summary>

`16-19`: **Event-driven architecture 구현이 우수함**

Event handler의 구현이 Clean하고 단일 책임 원칙을 잘 준수하고 있습니다. Event를 통한 도메인 간 결합도 감소와 관심사 분리가 잘 이루어져 있습니다.

</details>
<details>
<summary>src/test/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionServiceTest.java (3)</summary>

`260-271`: **Exception handling test가 우수함**

존재하지 않는 selfIntroduction에 대한 예외 처리 테스트가 적절하게 구현되어 있습니다. Edge case 처리가 잘 되어 있습니다.

---

`273-294`: **Domain state 변경 테스트가 정확함**

SelfIntroduction 도메인 객체의 상태 변경을 올바르게 검증하고 있습니다. Given-When-Then 패턴의 사용과 domain method 호출 검증이 적절합니다.

---

`296-317`: **Close 상태 변경 테스트 로직 검증**

Test 로직이 올바르게 구현되어 있고, domain 객체의 초기 상태와 변경된 상태를 명확히 검증하고 있습니다.

</details>
<details>
<summary>src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java (1)</summary>

`16-19`: **Event-driven design이 우수함**

Admin domain에서 community domain으로의 직접적인 의존성을 피하고 event를 통한 loose coupling을 구현한 것이 DDD 원칙에 매우 적합합니다. Domain 경계를 잘 분리했습니다.

</details>
<details>
<summary>src/test/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandServiceTest.java (2)</summary>

`20-30`: **Event 발행 검증 로직이 정확함**

MockedStatic을 사용한 Events.raise() 호출 검증이 올바르게 구현되어 있습니다. Event-driven architecture의 핵심인 event 발행을 정확히 테스트하고 있습니다.

---

`38-48`: **Test case 대칭성이 우수함**

convertToOpen 메서드에 대한 테스트가 convertToClose와 동일한 패턴으로 잘 구현되어 있어서 일관성이 좋습니다.

</details>
<details>
<summary>src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java (3)</summary>

`22-22`: **Command Service 의존성 주입이 적절합니다.**

Controller가 business logic을 직접 처리하지 않고 Command Service에 위임하는 것은 올바른 설계입니다. DDD의 separation of concerns 원칙을 잘 따르고 있습니다.

---

`33-37`: **REST API 설계가 Richardson Maturity Model Level 2를 잘 따르고 있습니다.**

- PATCH method 사용이 적절함 (partial update)
- URI 구조가 직관적이고 resource 중심적임
- kebab-case 사용으로 naming convention 준수
- 적절한 HTTP status code 반환

Controller가 business logic 없이 service layer에 위임하는 것도 올바른 구조입니다.

---

`39-43`: **일관성 있는 API endpoint 설계입니다.**

open endpoint와 동일한 패턴으로 구현되어 API 일관성이 유지되고 있습니다. Event-driven architecture를 통한 domain 분리도 PR 목적에 부합합니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-24T07:13:32Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T07:15:59Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-24T07:17:44Z
user: login: hainho
body: 고생하셨습니다!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T07:46:28Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T07:58:11Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T07:58:31Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T07:58:35Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T07:59:55Z
user: login: hainho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-24T08:18:21Z
user: login: stemmmm
body: LGTM :)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T05:41:40Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T07:02:45Z
user: login: stemmmm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T08:08:36Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T11:15:27Z
user: login: stemmmm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:01:48Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:07:58Z
user: login: hainho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:10:25Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:12:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f06b4e4e7eb6fbbb847590ee5b23d4639e91c454 and ff39f23c878d1751934f36719461df56c7c7b33e.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepository.java` (0 hunks)
* `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/AdminSelfIntroductionSearchCondition.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/SelfIntroductionSearchCondition.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java` (2 hunks)
* `src/main/java/atwoz/atwoz/community/query/selfintroduction/view/AdminSelfIntroductionView.java` (1 hunks)
* `src/test/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepositoryTest.java` (0 hunks)
* `src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java` (3 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* src/main/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
* src/test/java/atwoz/atwoz/admin/query/selfintroduction/AdminSelfIntroductionQueryRepositoryTest.java

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (4)</summary>

* src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/SelfIntroductionSearchCondition.java
* src/main/java/atwoz/atwoz/community/query/selfintroduction/view/AdminSelfIntroductionView.java
* src/main/java/atwoz/atwoz/community/presentation/selfintroduction/dto/AdminSelfIntroductionSearchCondition.java
* src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (2)</summary>

<details>
<summary>`**/*.java`: In addition to existing rules, review code based on: - Domain-driven design: enforce aggregate boundaries and immutable value objects   - Clean code: single responsibi...</summary>

> `**/*.java`: In addition to existing rules, review code based on: - Domain-driven design: enforce aggregate boundaries and immutable value objects   - Clean code: single responsibility methods, meaningful names, no duplication   - Object-oriented programming: SOLID(SRP, OCP, LSP, ISP, DIP) principles
> 

- `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java`
- `src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java`
- `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java`
- `src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java`

</details>
<details>
<summary>`**/*Controller*.java`: Controllers should not contain business logic. Delegate to service layer. Review the API endpoints based on this guidelines.
### Richardson Maturity Model (...</summary>

> `**/*Controller*.java`: Controllers should not contain business logic. Delegate to service layer. Review the API endpoints based on this guidelines.
> ### Richardson Maturity Model (RMM) 1. Level 0: Single URI with POST requests for all actions. 2. Level 1: Separate URIs for individual resources. 3. Level 2: Use of HTTP methods (GET, POST, PUT, DELETE) to define operations on resources. 4. Level 3: Hypermedia (HATEOAS) for advanced RESTful APIs.
> ### API Conventions - URI Rules:
>   - Should be intuitive and self-explanatory.
>   - Should not map 1:1 to database tables.
>   - Must be stateless, with no session state between requests.
>   - Include "api" and version in the URI (/api/{version}/resource).
>   - Use kebab-case for URIs and camelCase for parameters and body contents.
>   - Resource identifiers should be unique and only one per URI path.
> 
> - Design Principles:
>   - APIs should be designed around resources, which are abstractions rather than direct database tables.
>   - Stateless APIs facilitate scalability and flexibility.
>   - Clear separation of frontend and backend via URI structure.
>   - Versioning in URI paths is preferred for clarity and ease of caching.
>   - Maintain consistent naming conventions across the API.
>   - Use plural forms for resource names (/users instead of /user).
>   - Complex actions can include verbs in the URI (/orders/{orderId}/cancel).
> 
> - Implementation Details:
>   - Avoid deeply nested resource paths to ensure maintainability.
>   - Ensure URIs reflect the data they provide, not the permissions or roles required to access them.
>   - Keep URIs simple and predictable, aiding both developers and automated systems.
> 

- `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java`

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java (1)</summary><blockquote>

<details>
<summary>src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java (1)</summary>

* `RestController` (20-69)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionMapper.java (1)</summary>

`3-3`: **패키지 구조 개선이 적절합니다.**

DTO 클래스를 presentation 패키지로 이동한 것은 적절한 패키지 구조 개선입니다. query 패키지에서 presentation.dto 패키지로 이동함으로써 DTO의 책임이 더 명확해졌습니다.

</details>
<details>
<summary>src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java (1)</summary>

`623-625`: **Helper method 구현이 적절합니다.**

날짜 포맷팅을 위한 helper method가 잘 구현되었습니다. null 체크와 적절한 날짜 형식을 사용하고 있습니다.

</details>
<details>
<summary>src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java (1)</summary>

`24-25`: **의존성 주입 순서가 적절합니다.**

Query repository와 service의 의존성 주입이 적절하게 구성되어 있습니다. Constructor injection을 사용하여 의존성이 명확하게 드러납니다.

</details>
<details>
<summary>src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java (1)</summary>

`131-164`: **Helper method들이 잘 구현되었습니다.**

Null 체크와 blank 체크를 적절히 수행하는 helper method들이 잘 구현되었습니다. QueryDSL의 동적 쿼리 패턴을 올바르게 사용하고 있습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:32:19Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java (1)</summary><blockquote>

`12-12`: **Assertion 스타일의 일관성을 개선해 주세요.**

모든 assertion에서 `AssertionsForClassTypes.assertThat`을 사용하도록 변경되었지만, 일관성을 위해 static import를 사용하는 것을 권장합니다.


Import 구문을 수정하고 assertion을 간소화하세요:

```diff
-import org.assertj.core.api.AssertionsForClassTypes;
+import static org.assertj.core.api.Assertions.assertThat;
```

그리고 모든 assertion을 간소화:

```diff
-AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(
-    selfIntroductionsByWoman.size() + selfIntroductionsByMan.size());
+assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(
+    selfIntroductionsByWoman.size() + selfIntroductionsByMan.size());
```


Also applies to: 110-111, 133-133, 155-160, 181-199, 220-220

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ff39f23c878d1751934f36719461df56c7c7b33e and f2de5ec6295fbaf9e90dbfc88c243054a933cc6f.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java` (1 hunks)
* `src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java` (2 hunks)
* `src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java` (1 hunks)
* `src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java` (8 hunks)
* `src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
* src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.java`: In addition to existing rules, review code based on: - Domain-driven design: enforce aggregate boundaries and immutable value objects   - Clean code: single responsibi...</summary>

> `**/*.java`: In addition to existing rules, review code based on: - Domain-driven design: enforce aggregate boundaries and immutable value objects   - Clean code: single responsibility methods, meaningful names, no duplication   - Object-oriented programming: SOLID(SRP, OCP, LSP, ISP, DIP) principles
> 

- `src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java`
- `src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java`

</details>

</details><details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>📓 Common learnings</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java:33-43
Timestamp: 2025-05-24T07:17:42.281Z
Learning: AdminSelfIntroductionCommandService는 직접적인 검증 없이 이벤트를 발행하고, SelfIntroductionEventHandler가 이벤트를 수신하여 SelfIntroductionService.changeOpenStatus()를 호출하며, 이 메서드에서 ID 존재 여부를 검증한다. 이는 admin 도메인과 self-introduction 도메인 간의 결합도를 낮추는 이벤트 기반 아키텍처 패턴이다.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java (3)</summary>

`1-1`: **Package 이동이 도메인 경계를 명확하게 개선합니다.**

admin domain에서 community domain으로 repository를 이동한 것은 self-introduction이 community domain의 일부임을 명확히 하는 좋은 architectural decision입니다.

---

`27-71`: **Query 구조와 pagination 로직이 적절합니다.**

QueryDSL을 사용한 dynamic query 구현이 올바르게 되어 있으며, pagination 처리와 count query 분리도 performance 측면에서 적절합니다.

---

`73-106`: **Filter 메서드들이 null-safe하게 잘 구현되었습니다.**

각 filtering 조건들이 null check와 함께 적절하게 구현되어 있습니다. 특히 `loeEndDate` 메서드에서 `plusDays(1).atStartOfDay().minusSeconds(1)` 로직으로 end date의 마지막 시점까지 포함하는 처리가 정확합니다.

</details>
<details>
<summary>src/test/java/atwoz/atwoz/community/query/AdminSelfIntroductionQueryRepositoryTest.java (3)</summary>

`1-1`: **Package 이동이 repository와 일치하여 적절합니다.**

Repository의 package 이동에 맞춰 test class도 community.query package로 이동한 것이 올바릅니다.

---

`93-93`: **Test 활성화가 좋은 개선입니다.**

주석 처리되어 있던 `@Test` annotation을 활성화한 것은 test coverage를 향상시키는 좋은 변경입니다.

---

`40-41`: **Field 순서 변경이 깔끔합니다.**

`@Autowired` field들을 함께 그룹화한 것이 코드 가독성을 향상시킵니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:39:04Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:42:19Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:42:31Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:42:53Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:42:56Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:44:34Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:44:56Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:44:59Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:45:18Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T12:54:54Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-25T12:58:11Z
user: login: Kong-TaeHyeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T06:09:41Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-26T06:10:04Z

Review comments:
diff_hunk: @@ -31,4 +30,15 @@ public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntr
             adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
     }
 
+    @PatchMapping("/{id}/open")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
+        adminSelfIntroductionCommandService.convertToOpen(id);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
+
+    @PatchMapping("/{id}/close")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToClose(@PathVariable Long id) {
+        adminSelfIntroductionCommandService.convertToClose(id);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
path: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Error handling 고려사항을 확인해주세요.**

현재 구현에서는 존재하지 않는 ID나 권한 관련 예외 처리가 명시적으로 보이지 않습니다. Command Service에서 적절한 exception handling이 구현되어 있는지 확인이 필요합니다.



다음 script로 Command Service의 exception handling을 확인해보겠습니다:

---

🏁 Script executed:

```shell
#!/bin/bash
# Command Service의 exception handling 확인
ast-grep --pattern $'class AdminSelfIntroductionCommandService {
  $$$
  convertToOpen($_) {
    $$$
  }
  $$$
  convertToClose($_) {
    $$$
  }
  $$$
}'
```

Length of output: 155

---

다음으로 `convertToOpen` / `convertToClose` 구현 위치를 찾아 exception handling 여부를 직접 확인해보겠습니다.

---

🏁 Script executed:

```shell
#!/bin/bash
# 메서드 정의 위치 및 주변 코드 검색
rg -n "convertToOpen" -C5 -g '*.java'
rg -n "convertToClose" -C5 -g '*.java'
```

Length of output: 9412

---



</details>

**AdminSelfIntroductionCommandService에 ID 존재 여부 및 권한 검증 로직 추가 필요**

현재 `convertToOpen`/`convertToClose`는 단순히 이벤트만 발행하여, 존재하지 않는 ID나 권한이 없는 사용자의 호출도 200 OK로 응답합니다. 다음 위치에 예외 처리 및 검증을 추가하세요.

• src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java  
  - `convertToOpen(Long selfIntroductionId)`  
  - `convertToClose(Long selfIntroductionId)`  

예시 수정안:
```java
public void convertToOpen(Long selfIntroductionId) {
    SelfIntroduction entity = repository.findById(selfIntroductionId)
        .orElseThrow(() -> new ResourceNotFoundException("SelfIntroduction not found: " + selfIntroductionId));
    // 권한 체크 (예: SecurityContextHolder 활용)
    entity.open();
    repository.save(entity);
    Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
}
```
또는 컨트롤러 전역에서 처리할 수 있도록 `@ControllerAdvice` 기반 예외 핸들러를 구현해도 좋습니다.  
이렇게 하면 유효하지 않은 ID 요청 시 404, 권한 오류 시 403 상태를 올바르게 반환할 수 있습니다.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
around the methods convertToOpen(Long selfIntroductionId) and
convertToClose(Long selfIntroductionId), add validation to check if the given
selfIntroductionId exists by querying the repository and throw a
ResourceNotFoundException if not found. Also, implement permission checks (e.g.,
using SecurityContextHolder) to ensure the caller has rights to perform the
operation. Update the entity state and save it only after these validations.
Optionally, implement a @ControllerAdvice exception handler in the controller
layer to translate these exceptions into appropriate HTTP responses like 404 for
not found and 403 for forbidden.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-24T07:13:31Z
author_association: NONE
diff_hunk: @@ -31,4 +30,15 @@ public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntr
             adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
     }
 
+    @PatchMapping("/{id}/open")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
+        adminSelfIntroductionCommandService.convertToOpen(id);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
+
+    @PatchMapping("/{id}/close")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToClose(@PathVariable Long id) {
+        adminSelfIntroductionCommandService.convertToClose(id);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
path: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: Kong-TaeHyeon
body: `Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));` 
해당 이벤트를 처리하는 핸들러에서 `selfIntroductionCommandService` 의 메서드를 호출하고, 해당 메서드 내에서 ID를 통해 SelfIntroduction 이 존재하는지 검증하고 있습니다!

created_at: 2025-05-24T07:15:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,4 +30,15 @@ public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntr
             adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
     }
 
+    @PatchMapping("/{id}/open")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
+        adminSelfIntroductionCommandService.convertToOpen(id);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
+
+    @PatchMapping("/{id}/close")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToClose(@PathVariable Long id) {
+        adminSelfIntroductionCommandService.convertToClose(id);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
path: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@Kong-TaeHyeon 

정확한 지적입니다! 이벤트 핸들러를 통한 검증 로직을 확인해보겠습니다.




Understanding the event-driven architecture flow:
1. **Controller** → `AdminSelfIntroductionCommandService.convertToOpen/Close()`
2. **Command Service** → `Events.raise(SelfIntroductionOpenStatusChangeEvent)`  
3. **Event Handler** → `SelfIntroductionService.changeOpenStatus()`
4. **Domain Service** → ID 검증 및 상태 변경

이는 도메인 간 결합도를 낮추는 좋은 아키텍처 패턴입니다. 감사합니다!



      /\   /\
     (  . .)  
      )   (   *curious about the event flow*
     (  v  ) 
    ^^  W  ^^

---

🏁 Script executed:
```
#!/bin/bash
# SelfIntroductionService의 changeOpenStatus 메서드와 이벤트 핸들러 구현 확인
echo "=== SelfIntroductionService.changeOpenStatus 구현 ==="
rg -A 10 "changeOpenStatus" src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java

echo -e "\n=== SelfIntroductionEventHandler 구현 ==="
rg -A 10 -B 5 "SelfIntroductionOpenStatusChangeEvent" src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java

echo -e "\n=== SelfIntroduction 도메인의 findById 관련 예외 처리 확인 ==="
rg -A 5 "findById" src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java
```

Length of output: 2203

---



</details>

@Kong-TaeHyeon 

완전히 맞습니다! 제가 놓친 부분을 확인했습니다.

**올바른 예외 처리 흐름:**
1. `AdminSelfIntroductionCommandService.convertToOpen()` → 이벤트 발행
2. `SelfIntroductionEventHandler.handle()` → 이벤트 수신  
3. `SelfIntroductionService.changeOpenStatus()` → `getSelfIntroductionById(id)` 호출
4. `getSelfIntroductionById()` → `findById(id).orElseThrow(SelfIntroductionNotFoundException::new)`

**존재하지 않는 ID 요청 시:**
- `SelfIntroductionNotFoundException`이 발생하여 적절한 HTTP 상태 코드로 변환됩니다.

이벤트 기반 아키텍처를 통해 **admin 도메인과 self-introduction 도메인을 완벽히 분리**하면서도 검증 로직을 올바르게 구현하셨네요. 제가 직접적인 검증만 찾아보고 이벤트 흐름을 놓쳤습니다.

훌륭한 도메인 분리 설계입니다! 정정해주셔서 감사합니다.



      /\   /\
     (  ^.^  )  
      )   (   *learned something new!*
     (  v  ) 
    ^^  W  ^^

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java:33-43
Timestamp: 2025-05-24T07:17:42.281Z
Learning: AdminSelfIntroductionCommandService는 직접적인 검증 없이 이벤트를 발행하고, SelfIntroductionEventHandler가 이벤트를 수신하여 SelfIntroductionService.changeOpenStatus()를 호출하며, 이 메서드에서 ID 존재 여부를 검증한다. 이는 admin 도메인과 self-introduction 도메인 간의 결합도를 낮추는 이벤트 기반 아키텍처 패턴이다.
```

---

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java:33-43
Timestamp: 2025-05-24T07:17:42.281Z
Learning: 이벤트 기반 아키텍처에서 AdminSelfIntroductionCommandService는 직접 검증하지 않고 이벤트만 발행한다. SelfIntroductionEventHandler가 이벤트를 받아 SelfIntroductionService.changeOpenStatus()를 호출하고, 여기서 getSelfIntroductionById()를 통해 ID 존재 여부를 검증하여 SelfIntroductionNotFoundException을 던진다. 이는 도메인 간 결합도를 낮추는 올바른 패턴이다.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@Kong-TaeHyeon 

정확한 지적입니다! 이벤트 핸들러를 통한 검증 로직을 확인해보겠습니다.




Understanding the event-driven architecture flow:
1. **Controller** → `AdminSelfIntroductionCommandService.convertToOpen/Close()`
2. **Command Service** → `Events.raise(SelfIntroductionOpenStatusChangeEvent)`  
3. **Event Handler** → `SelfIntroductionService.changeOpenStatus()`
4. **Domain Service** → ID 검증 및 상태 변경

이는 도메인 간 결합도를 낮추는 좋은 아키텍처 패턴입니다. 감사합니다!



      /\   /\
     (  . .)  
      )   (   *curious about the event flow*
     (  v  ) 
    ^^  W  ^^

---

🏁 Script executed:
```
#!/bin/bash
# SelfIntroductionService의 changeOpenStatus 메서드와 이벤트 핸들러 구현 확인
echo "=== SelfIntroductionService.changeOpenStatus 구현 ==="
rg -A 10 "changeOpenStatus" src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java

echo -e "\n=== SelfIntroductionEventHandler 구현 ==="
rg -A 10 -B 5 "SelfIntroductionOpenStatusChangeEvent" src/main/java/atwoz/atwoz/community/command/infra/selfintroduction/SelfIntroductionEventHandler.java

echo -e "\n=== SelfIntroduction 도메인의 findById 관련 예외 처리 확인 ==="
rg -A 5 "findById" src/main/java/atwoz/atwoz/community/command/application/selfintroduction/SelfIntroductionService.java
```

Length of output: 2203

---



</details>

@Kong-TaeHyeon 

완전히 맞습니다! 제가 놓친 부분을 확인했습니다.

**올바른 예외 처리 흐름:**
1. `AdminSelfIntroductionCommandService.convertToOpen()` → 이벤트 발행
2. `SelfIntroductionEventHandler.handle()` → 이벤트 수신  
3. `SelfIntroductionService.changeOpenStatus()` → `getSelfIntroductionById(id)` 호출
4. `getSelfIntroductionById()` → `findById(id).orElseThrow(SelfIntroductionNotFoundException::new)`

**존재하지 않는 ID 요청 시:**
- `SelfIntroductionNotFoundException`이 발생하여 적절한 HTTP 상태 코드로 변환됩니다.

이벤트 기반 아키텍처를 통해 **admin 도메인과 self-introduction 도메인을 완벽히 분리**하면서도 검증 로직을 올바르게 구현하셨네요. 제가 직접적인 검증만 찾아보고 이벤트 흐름을 놓쳤습니다.

훌륭한 도메인 분리 설계입니다! 정정해주셔서 감사합니다.



      /\   /\
     (  ^.^  )  
      )   (   *learned something new!*
     (  v  ) 
    ^^  W  ^^

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java:33-43
Timestamp: 2025-05-24T07:17:42.281Z
Learning: AdminSelfIntroductionCommandService는 직접적인 검증 없이 이벤트를 발행하고, SelfIntroductionEventHandler가 이벤트를 수신하여 SelfIntroductionService.changeOpenStatus()를 호출하며, 이 메서드에서 ID 존재 여부를 검증한다. 이는 admin 도메인과 self-introduction 도메인 간의 결합도를 낮추는 이벤트 기반 아키텍처 패턴이다.
```

---

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java:33-43
Timestamp: 2025-05-24T07:17:42.281Z
Learning: 이벤트 기반 아키텍처에서 AdminSelfIntroductionCommandService는 직접 검증하지 않고 이벤트만 발행한다. SelfIntroductionEventHandler가 이벤트를 받아 SelfIntroductionService.changeOpenStatus()를 호출하고, 여기서 getSelfIntroductionById()를 통해 ID 존재 여부를 검증하여 SelfIntroductionNotFoundException을 던진다. 이는 도메인 간 결합도를 낮추는 올바른 패턴이다.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-24T07:17:44Z
author_association: NONE
diff_hunk: @@ -10,17 +11,15 @@
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.web.PageableDefault;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/admin/selfintroduction")
 @RequiredArgsConstructor
 public class AdminSelfIntroductionController {
path: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: hainho
body: https://github.com/atwoz-dev/atwoz_server/wiki/Swagger-Schema-%EC%BB%A8%EB%B2%A4%EC%85%98#controller

controller 관련된 문서화 컨벤션이 추가됐어요!
컨트롤러 클래스에는 `@Tag`, 컨트롤러 메서드에는 `@ Operation`  사용해서 한글로 설명 작성해주시면 좋을 것 같네요!
created_at: 2025-05-24T07:27:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+public class AdminSelfIntroductionCommandServiceTest {
+
+    AdminSelfIntroductionCommandService adminSelfIntroductionCommandService = new AdminSelfIntroductionCommandService();
+
+    @DisplayName("셀프 소개를 비공개로 변경하기 위한 이벤트를 발행합니다.")
+    @Test
+    void verifyEventIsRaisedOnConvertToClose() {
+        // Given
+        Long selfIntroductionId = 1L;
+
+        try (MockedStatic<Events> mockedStaticEvents = Mockito.mockStatic(Events.class)) {
+            // When
+            adminSelfIntroductionCommandService.convertToClose(selfIntroductionId);
+
+            // Then - 이벤트가 호출되었는지 검증
+            mockedStaticEvents.verify(() ->
+                    Events.raise(Mockito.any(SelfIntroductionOpenStatusChangeEvent.class)),
+                Mockito.times(1)  // 정확히 1번 호출 여부 확인
path: src/test/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandServiceTest.java
user: login: hainho
body: 어떤 값으로 이벤트를 발행하는지도 중요해보여서
아래처럼 발행된 이벤트의 값을 검증하는 것도 좋아보이네요!

```java
eventsMockedStatic.verify(() ->
                    Events.raise(argThat((SelfIntroductionOpenStatusChangeEvent event) ->
                        event.getId().equals(selfIntroductionId) &&
                        event.getOpen().equals(false)
                    )), times(1));
```
created_at: 2025-05-24T07:39:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: hainho
body: 이게 `admin` 도메인에서 구현하다 보니까 이렇게 구현된 것 같은데
`SelfIntroduction` 도메인으로 이동시키면 이벤트 발행 없이 바로 처리 가능해보이네요!
created_at: 2025-05-24T07:44:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+package atwoz.atwoz.community.command.domain.selfintroduction.event;
+
+import atwoz.atwoz.common.event.Event;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class SelfIntroductionOpenStatusChangeEvent extends Event {
+    private final Long id;
+    private final boolean open;
path: src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/event/SelfIntroductionOpenStatusChangeEvent.java
user: login: hainho
body: `isOpened`으로 사용하면 좀 더 의미가 명확할 것 같아요!
created_at: 2025-05-24T07:46:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,18 @@
+package atwoz.atwoz.community.command.domain.selfintroduction.event;
+
+import atwoz.atwoz.common.event.Event;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class SelfIntroductionOpenStatusChangeEvent extends Event {
+    private final Long id;
+    private final boolean open;
path: src/main/java/atwoz/atwoz/community/command/domain/selfintroduction/event/SelfIntroductionOpenStatusChangeEvent.java
user: login: Kong-TaeHyeon
body: 넵! 수정하도록 하겠습니다!!
created_at: 2025-05-24T07:58:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,49 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+public class AdminSelfIntroductionCommandServiceTest {
+
+    AdminSelfIntroductionCommandService adminSelfIntroductionCommandService = new AdminSelfIntroductionCommandService();
+
+    @DisplayName("셀프 소개를 비공개로 변경하기 위한 이벤트를 발행합니다.")
+    @Test
+    void verifyEventIsRaisedOnConvertToClose() {
+        // Given
+        Long selfIntroductionId = 1L;
+
+        try (MockedStatic<Events> mockedStaticEvents = Mockito.mockStatic(Events.class)) {
+            // When
+            adminSelfIntroductionCommandService.convertToClose(selfIntroductionId);
+
+            // Then - 이벤트가 호출되었는지 검증
+            mockedStaticEvents.verify(() ->
+                    Events.raise(Mockito.any(SelfIntroductionOpenStatusChangeEvent.class)),
+                Mockito.times(1)  // 정확히 1번 호출 여부 확인
path: src/test/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandServiceTest.java
user: login: Kong-TaeHyeon
body: 앗 그렇네요! 값을 검증하는 방식으로 변경하도록 하겠습니다!
created_at: 2025-05-24T07:58:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,17 +11,15 @@
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.web.PageableDefault;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/admin/selfintroduction")
 @RequiredArgsConstructor
 public class AdminSelfIntroductionController {
path: src/main/java/atwoz/atwoz/admin/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: Kong-TaeHyeon
body: 넵! 확인하겠습니다!!
created_at: 2025-05-24T07:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: Kong-TaeHyeon
body: admin 계층의 controller 에서 selfIntroduction 계층의 service를 의존하는 방식으로 구현하는 것을 말씀해주신걸까요??
Ex : adminController -> selfIntroductionService 의 convert 함수 호출
created_at: 2025-05-24T07:59:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: hainho
body: `AdminSelfIntroductionController`, `AdminSelfIntroductionCommandService` 둘다 admin 하위가 아니라 별도의 SelfIntroduction 도메인으로 이동시켜서 네이밍은 지금 그대로 admin을 붙여서 사용하는 방식이에요!
created_at: 2025-05-24T08:18:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: Kong-TaeHyeon
body: 인호님이 말씀해주신 방향으로 코드를 수정하려고 하고 있는데,
그렇다면 `AdminSelfIntroductionController` 를 SelfIntroduction 도메인에 두어도, 기존 RequestMapping 은 동일하게 `/admin/selfintroduction`으로 처리해도 괜찮을까요?
* 필터가 관리자 필터 적용을 받아야 하기 때문에,,,

이런 상황때문에 뭔가 admin 으로 가야할 것 같아서요 ..! 나중에 도메인 별로 만약 독립된 서버가 된다면 `AdminSelfIntroductionController`는 관리자쪽으로 결국 넘어가야할 것 같다는 생각에 계속 고민이 되네요...!

호윤님 의견도 한번 여쭤보고 싶습니다! @stemmmm 
created_at: 2025-05-25T07:02:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: stemmmm
body: self introduction으로 넘기는게 나은것 같습니다. 분리되면 관리자 도메인에서 http 요청해서 받아오는 형태가 되겠죠

> AdminSelfIntroductionController는 관리자쪽으로 결국 넘어가야할 것 같다는 생각

이거는 왜 그렇게 생각하시는거에요?
created_at: 2025-05-25T08:08:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: Kong-TaeHyeon
body: <img width="825" alt="image" src="https://github.com/user-attachments/assets/1274cc27-2232-4aa2-a891-2832e1120bfd" />

그림을 그려봤는데, 위 처럼 `AdminSelfIntroductionController`가 `SelfIntroduction` 쪽에 있으면, 멀티 모듈 상태에서 관리자 필터를 적용받지 못하는것 아닌가 싶어서요!!
created_at: 2025-05-25T11:15:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: stemmmm
body: 이해가 잘 안가요ㅠ
created_at: 2025-05-25T12:01:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: Kong-TaeHyeon
body: ### Server가 분리된 상황

#### 만약, SelfIntroduction Server에 AdminSelfIntroductionController 컨트롤러가 존재한다면!

제가 생각하는 문제는 관리자 서버와 다른 서버가 독립적인 상황이라면, 토큰 발급,관리 또한 분리가 되어 관리자가 발급받은 토큰이 관리자 서버 이외의 다른 서버에서 허용되지 않는 문제입니다!

1. 관리자가 특정 셀프 소개 글을 비공개로 만들기 위해 `AdminSelfIntroductionController`에 접근.
2. 관리자의 토큰은 관리자 서버 이외에서 유효하지 않아서, 요청이 받아들여지지 않음. 

따라서, `AdminSelfIntroductionController 를 관리자 패키지에 두어야 하는 것이 아닌가?`라는 생각이 들었어요!
created_at: 2025-05-25T12:07:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: hainho
body: > 그렇다면 AdminSelfIntroductionController 를 SelfIntroduction 도메인에 두어도, 기존 RequestMapping 은 동일하게 /admin/selfintroduction으로 처리해도 괜찮을까요?

네 맞아요! 저도 `AdminInterviewQuestionController` 에서 동일하게 구현했어요!

> 그림을 그려봤는데, 위 처럼 AdminSelfIntroductionController가 SelfIntroduction 쪽에 있으면, 멀티 모듈 상태에서 관리자 필터를 적용받지 못하는것 아닌가 싶어서요!!

`/admin/selfintroduction` 으로 어드민 서버의 SelfIntroduction 도메인에서 처리하면 괜찮을 것 같아요!

> 이해가 잘 안가요ㅠ

api path를 `/selfintroduction` 으로 구현하고 어드민 클라이언트에서 어드민 서버가 아닌 앱 서버에 api request를 직접 보내는 경우에
어드민 클라이언트가 어드민 액세스 토큰을 가지고 있어서 앱 서버에서 401 발생한다는 말씀을 하신 것 같아요!
created_at: 2025-05-25T12:10:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package atwoz.atwoz.admin.command.application.selfintroduction;
+
+import atwoz.atwoz.common.event.Events;
+import atwoz.atwoz.community.command.domain.selfintroduction.event.SelfIntroductionOpenStatusChangeEvent;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class AdminSelfIntroductionCommandService {
+
+    @Transactional
+    public void convertToClose(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, false));
+    }
+
+    @Transactional
+    public void convertToOpen(Long selfIntroductionId) {
+        Events.raise(SelfIntroductionOpenStatusChangeEvent.of(selfIntroductionId, true));
+    }
path: src/main/java/atwoz/atwoz/admin/command/application/selfintroduction/AdminSelfIntroductionCommandService.java
user: login: Kong-TaeHyeon
body: 헉 정리 감사합니다!! 완벽 이해하였습니다!
그럼 인호님 말씀해주신대로 수정하도록 하겠습니다! (`AdminInterviewQuestionController` 참고하겠습니다!)
created_at: 2025-05-25T12:12:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -423,4 +431,197 @@ void getEmptyValueWhenSelfIntroductionIsNotExists() {
                     memberId)).isEmpty();
         }
     }
+
+    @Nested
+    @DisplayName("관리자용 셀프 소개 페이지네이션")
+    class selfIntroductionAdminPaginationTest {
+        private static final int PAGE_SIZE = 5;
+        Member manMember;
+        Member womanMember;
+        List<SelfIntroduction> selfIntroductionsByMan;
+        List<SelfIntroduction> selfIntroductionsByWoman;
+
+        @BeforeEach
+        void setUp() {
+            manMember = Member.fromPhoneNumber("01012345678");
+            womanMember = Member.fromPhoneNumber("01098765432");
+
+            MemberProfile manProfile = MemberProfile
+                .builder()
+                .nickname(Nickname.from("남자닉네임"))
+                .gender(Gender.MALE)
+                .build();
+
+            MemberProfile womanProfile = MemberProfile
+                .builder()
+                .nickname(Nickname.from("여자닉네임"))
+                .gender(Gender.FEMALE)
+                .build();
+
+            manMember.updateProfile(manProfile);
+            womanMember.updateProfile(womanProfile);
+
+            entityManager.persist(manMember);
+            entityManager.persist(womanMember);
+            entityManager.flush();
+
+            selfIntroductionsByMan = new ArrayList<>();
+            selfIntroductionsByWoman = new ArrayList<>();
+
+            for (int i = 0; i < 5; i++) {
+                SelfIntroduction selfIntroductionByMan = SelfIntroduction.write(manMember.getId(), "제목" + i,
+                    "내용은 30자 이상이어야합니다... 30자를 채우자!! " + i);
+                SelfIntroduction selfIntroductionByWoman = SelfIntroduction.write(womanMember.getId(), "제목입니다." + i,
+                    "내용입니다. 내용은 30자 이상이어야합니다... 30자를 채우자!! " + i);
+
+                if (i % 2 == 0) {
+                    selfIntroductionByMan.close();
+                    selfIntroductionByWoman.close();
+                }
+
+                entityManager.persist(selfIntroductionByMan);
+                entityManager.persist(selfIntroductionByWoman);
+
+                selfIntroductionsByMan.add(selfIntroductionByMan);
+                selfIntroductionsByWoman.add(selfIntroductionByWoman);
+            }
+
+            entityManager.flush();
+            entityManager.clear();
+        }
+
+        @DisplayName("모든 셀프 소개를 조회합니다.")
+        void findAllSelfIntroductions() {
+            // Given
+            AdminSelfIntroductionSearchCondition condition = new AdminSelfIntroductionSearchCondition(
+                null,
+                null,
+                null,
+                null,
+                null
+            );
+            PageRequest pageRequest = PageRequest.of(0, PAGE_SIZE);
+
+            // When
+            Page<AdminSelfIntroductionView> selfIntroductionViews = selfIntroductionQueryRepository.findSelfIntroductions(
+                condition, pageRequest);
+
+            // Then
+            AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(
+                selfIntroductionsByWoman.size() + selfIntroductionsByMan.size());
+        }
path: src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**누락된 @Test annotation을 추가해야 합니다.**

Test 메서드에 `@Test` annotation이 누락되어 있어 실제로 실행되지 않을 수 있습니다.


다음과 같이 수정하세요:

```diff
+    @Test
     @DisplayName("모든 셀프 소개를 조회합니다.")
     void findAllSelfIntroductions() {
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
        @Test
        @DisplayName("모든 셀프 소개를 조회합니다.")
        void findAllSelfIntroductions() {
            // Given
            AdminSelfIntroductionSearchCondition condition = new AdminSelfIntroductionSearchCondition(
                null,
                null,
                null,
                null,
                null
            );
            PageRequest pageRequest = PageRequest.of(0, PAGE_SIZE);

            // When
            Page<AdminSelfIntroductionView> selfIntroductionViews = selfIntroductionQueryRepository.findSelfIntroductions(
                condition, pageRequest);

            // Then
            AssertionsForClassTypes.assertThat(selfIntroductionViews.getTotalElements()).isEqualTo(
                selfIntroductionsByWoman.size() + selfIntroductionsByMan.size());
        }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
around lines 493 to 512, the test method findAllSelfIntroductions is missing the
@Test annotation, which prevents it from being executed as a test. Add the @Test
annotation above the method declaration to ensure it runs during testing.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T12:32:18Z
author_association: NONE
diff_hunk: @@ -423,4 +431,197 @@ void getEmptyValueWhenSelfIntroductionIsNotExists() {
                     memberId)).isEmpty();
         }
     }
+
+    @Nested
+    @DisplayName("관리자용 셀프 소개 페이지네이션")
+    class selfIntroductionAdminPaginationTest {
+        private static final int PAGE_SIZE = 5;
+        Member manMember;
+        Member womanMember;
+        List<SelfIntroduction> selfIntroductionsByMan;
+        List<SelfIntroduction> selfIntroductionsByWoman;
+
+        @BeforeEach
+        void setUp() {
+            manMember = Member.fromPhoneNumber("01012345678");
+            womanMember = Member.fromPhoneNumber("01098765432");
+
+            MemberProfile manProfile = MemberProfile
+                .builder()
+                .nickname(Nickname.from("남자닉네임"))
+                .gender(Gender.MALE)
+                .build();
+
+            MemberProfile womanProfile = MemberProfile
+                .builder()
+                .nickname(Nickname.from("여자닉네임"))
+                .gender(Gender.FEMALE)
+                .build();
+
+            manMember.updateProfile(manProfile);
+            womanMember.updateProfile(womanProfile);
+
+            entityManager.persist(manMember);
+            entityManager.persist(womanMember);
+            entityManager.flush();
+
+            selfIntroductionsByMan = new ArrayList<>();
+            selfIntroductionsByWoman = new ArrayList<>();
+
+            for (int i = 0; i < 5; i++) {
+                SelfIntroduction selfIntroductionByMan = SelfIntroduction.write(manMember.getId(), "제목" + i,
+                    "내용은 30자 이상이어야합니다... 30자를 채우자!! " + i);
+                SelfIntroduction selfIntroductionByWoman = SelfIntroduction.write(womanMember.getId(), "제목입니다." + i,
+                    "내용입니다. 내용은 30자 이상이어야합니다... 30자를 채우자!! " + i);
+
+                if (i % 2 == 0) {
+                    selfIntroductionByMan.close();
+                    selfIntroductionByWoman.close();
+                }
+
+                entityManager.persist(selfIntroductionByMan);
+                entityManager.persist(selfIntroductionByWoman);
+
+                selfIntroductionsByMan.add(selfIntroductionByMan);
+                selfIntroductionsByWoman.add(selfIntroductionByWoman);
+            }
+
+            entityManager.flush();
+            entityManager.clear();
+        }
path: src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Test setup이 적절하지만 개선이 필요합니다.**

Test setup은 적절하지만 몇 가지 개선점이 있습니다:

1. 변수명 `manMember`, `womanMember`보다는 `maleMember`, `femaleMember`가 일관성 있습니다 (기존 테스트와 동일)
2. `@Test` annotation이 누락된 테스트 메서드가 있습니다 (line 494)


다음과 같이 수정하는 것을 권장합니다:

```diff
-        Member manMember;
-        Member womanMember;
+        Member maleMember;
+        Member femaleMember;
-        List<SelfIntroduction> selfIntroductionsByMan;
-        List<SelfIntroduction> selfIntroductionsByWoman;
+        List<SelfIntroduction> selfIntroductionsByMale;
+        List<SelfIntroduction> selfIntroductionsByFemale;

+    @Test
     @DisplayName("모든 셀프 소개를 조회합니다.")
     void findAllSelfIntroductions() {
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/test/java/atwoz/atwoz/community/query/SelfIntroductionQueryRepositoryTest.java
around lines 435 to 491, rename the variables manMember and womanMember to
maleMember and femaleMember respectively to maintain naming consistency with
existing tests. Additionally, review the test methods following this setup and
add missing @Test annotations, especially around line 494, to ensure all test
methods are properly recognized and executed by the test framework.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T12:32:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**URI 구조 개선이 필요합니다.**

현재 URI `/admin/selfintroduction`는 API 설계 가이드라인에 따라 개선이 필요합니다:

1. API versioning이 없습니다 (`/api/{version}/` 형태 권장)
2. 리소스명이 단수형입니다 (복수형 권장)
3. kebab-case를 사용해야 합니다


다음과 같이 수정하는 것을 권장합니다:

```diff
-@RequestMapping("/admin/selfintroduction")
+@RequestMapping("/api/v1/admin/self-introductions")
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
at line 20, the URI mapping "/admin/selfintroduction" should be improved to
follow API design guidelines by adding versioning (e.g., "/api/v1/"), using
plural resource names, and applying kebab-case. Update the @RequestMapping
annotation to reflect these changes, such as "/api/v1/admin/self-introductions".
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T12:32:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            selfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
+
+    @Operation(summary = "셀프 소개 공개 전환 API")
+    @PatchMapping("/{id}/open")
+    public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
+        selfIntroductionService.changeOpenStatus(id, true);
+        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
+    }
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**PATCH endpoint URI가 RESTful 원칙을 위반합니다.**

현재 `/admin/selfintroduction/{id}/open` 형태는 동사가 포함된 URI로 RESTful 설계 원칙에 맞지 않습니다.


다음과 같은 접근 방식을 고려해보세요:

**Option 1: Single PATCH endpoint with request body**
```diff
-@PatchMapping("/{id}/open")
-public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
-    selfIntroductionService.changeOpenStatus(id, true);
+@PatchMapping("/{id}")
+public ResponseEntity<BaseResponse<Void>> updateSelfIntroduction(
+    @PathVariable Long id, 
+    @RequestBody SelfIntroductionStatusUpdateRequest request) {
+    selfIntroductionService.changeOpenStatus(id, request.isOpen());
```

**Option 2: Query parameter approach**
```diff
-@PatchMapping("/{id}/open")
-public ResponseEntity<BaseResponse<Void>> updateSelfIntroductionToOpen(@PathVariable Long id) {
-    selfIntroductionService.changeOpenStatus(id, true);
+@PatchMapping("/{id}")
+public ResponseEntity<BaseResponse<Void>> updateSelfIntroduction(
+    @PathVariable Long id,
+    @RequestParam boolean open) {
+    selfIntroductionService.changeOpenStatus(id, open);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    @PatchMapping("/{id}")
    public ResponseEntity<BaseResponse<Void>> updateSelfIntroduction(
        @PathVariable Long id,
        @RequestBody SelfIntroductionStatusUpdateRequest request) {
        selfIntroductionService.changeOpenStatus(id, request.isOpen());
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
around lines 38 to 42, the PATCH endpoint URI includes a verb "/open" which
violates RESTful design principles. Refactor the endpoint to use a more RESTful
URI by either creating a single PATCH endpoint like "/{id}" that accepts a
request body indicating the open status, or by using a query parameter to
specify the status change. Adjust the method signature and service call
accordingly to handle the new input format.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T12:32:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            selfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**페이지네이션 구현에 보안 고려사항이 필요합니다.**

관리자 API이지만 인증/인가 관련 어노테이션이 보이지 않습니다. 관리자 권한 검증이 필요합니다.


다음 스크립트로 보안 관련 어노테이션이나 인터셉터 설정을 확인해보겠습니다:

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check for security annotations and admin authorization
# Test: Search for security annotations and admin role checks

# Search for security annotations in the codebase
rg -A 2 -B 2 "@PreAuthorize|@RolesAllowed|@Secured" --type java

# Search for admin-related authorization
rg -A 2 -B 2 "ADMIN|Admin" --type java | head -20
```

Length of output: 2546

---

추가 검증을 위해 Spring Security 설정 여부를 확인하겠습니다:

---

🏁 Script executed:

```shell
#!/bin/bash
# 1. WebSecurityConfigurerAdapter 상속 여부 확인
rg -A2 -B2 "extends WebSecurityConfigurerAdapter" --type java

# 2. SecurityFilterChain 빈 정의 확인
rg -A2 -B2 "SecurityFilterChain" --type java

# 3. 메서드 보안 관련 어노테이션 활성화 확인
rg -A2 -B2 "@EnableWebSecurity|@EnableGlobalMethodSecurity" --type java
```

Length of output: 182

---



</details>

**관리자 권한 검증 누락 – Security 설정 및 Method Security 적용 필요**

해당 API에 관리자 권한 검증이 전혀 적용되어 있지 않습니다. Spring Security 설정 파일이나 메서드 보안 어노테이션이 확인되지 않으므로, 아래 사항을 반영해주세요.

· File: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java  
  – `getSelfIntroductions` 메서드에 `@PreAuthorize("hasRole('ADMIN')")` 또는 `@RolesAllowed("ROLE_ADMIN")` 추가  

· 프로젝트 전반에 Spring Security 활성화  
  – `@EnableWebSecurity` 및 `@EnableMethodSecurity` (또는 `@EnableGlobalMethodSecurity`) 어노테이션을 붙인 SecurityConfig 클래스 생성  
  – `SecurityFilterChain` 빈을 정의하여 HTTP 요청별 Authentication/Authorization 필터 설정  

위 조치 완료 후 관리자 전용 API의 접근이 적절히 제한되는지 검증 바랍니다.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
around lines 29 to 35, the getSelfIntroductions method lacks any administrator
role verification. Add the @PreAuthorize("hasRole('ADMIN')") annotation to this
method to enforce admin-only access. Additionally, ensure the project has a
SecurityConfig class annotated with @EnableWebSecurity and @EnableMethodSecurity
(or @EnableGlobalMethodSecurity) to activate Spring Security and method-level
security. Define a SecurityFilterChain bean in this config to handle
authentication and authorization for HTTP requests. After these changes, verify
that the admin API access is properly restricted.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:32:18Z
author_association: NONE
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
+
+    private BooleanExpression nicknameEq(String nickname) {
+        if (nickname == null || nickname.isBlank()) {
+            return null;
+        }
+        return member.profile.nickname.value.eq(nickname);
+    }
+
+    private BooleanExpression isOpenedEq(Boolean isOpened) {
+        if (isOpened == null) {
+            return null;
+        }
+        return selfIntroduction.isOpened.eq(isOpened);
+    }
+
+    private BooleanExpression startDateGoe(LocalDate startDate) {
+        if (startDate == null) {
+            return null;
+        }
+        return selfIntroduction.createdAt.goe(startDate.atStartOfDay());
+    }
+
+    private BooleanExpression loeEndDate(LocalDate endDate) {
+        if (endDate == null) {
+            return null;
+        }
+        return selfIntroduction.createdAt.loe(endDate.plusDays(1).atStartOfDay().minusSeconds(1));
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**날짜 범위 조건에서 논리적 오류가 있습니다.**

메서드명 `loeEndDate`가 "less or equal"을 의미하는 것 같지만, 실제로는 "less than or equal" 조건이 아닙니다. 또한 끝 시간 계산에서 1초를 빼는 로직이 복잡합니다.


다음과 같이 개선하는 것을 권장합니다:

```diff
-    private BooleanExpression loeEndDate(LocalDate endDate) {
+    private BooleanExpression endDateLoe(LocalDate endDate) {
         if (endDate == null) {
             return null;
         }
-        return selfIntroduction.createdAt.loe(endDate.plusDays(1).atStartOfDay().minusSeconds(1));
+        return selfIntroduction.createdAt.lt(endDate.plusDays(1).atStartOfDay());
     }
```

이렇게 하면 더 명확하고 간단해집니다.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    private BooleanExpression endDateLoe(LocalDate endDate) {
        if (endDate == null) {
            return null;
        }
        return selfIntroduction.createdAt.lt(endDate.plusDays(1).atStartOfDay());
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
around lines 152 to 157, the loeEndDate method has a logical issue in how it
calculates the end datetime for the "less or equal" condition and uses a complex
approach by subtracting one second. To fix this, simplify the logic by using the
endDate plus one day at the start of the day and apply a "less than" condition
instead of "less or equal," which makes the intent clearer and the code easier
to understand.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:32:18Z
author_association: NONE
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**쿼리 구현이 적절하지만 성능 최적화가 필요합니다.**

새로운 admin 페이지네이션 메서드가 잘 구현되었지만 몇 가지 개선점이 있습니다:

1. Count query와 main query가 동일한 조건을 반복하고 있어 코드 중복이 있습니다
2. Count query에서 불필요한 JOIN이 발생할 수 있습니다


다음과 같이 리팩토링하는 것을 권장합니다:

```diff
+    private BooleanExpression buildAdminSearchCondition(AdminSelfIntroductionSearchCondition condition) {
+        return Expressions.allOf(
+            nicknameEq(condition.nickname()),
+            isOpenedEq(condition.isOpened()),
+            startDateGoe(condition.startDate()),
+            loeEndDate(condition.endDate()),
+            phoneNumberEq(condition.phoneNumber())
+        );
+    }

     public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
         Pageable pageable) {
+        BooleanExpression searchCondition = buildAdminSearchCondition(condition);
+        
         List<AdminSelfIntroductionView> content = queryFactory
             // ... existing select logic
-            .where(
-                nicknameEq(condition.nickname()),
-                isOpenedEq(condition.isOpened()),
-                startDateGoe(condition.startDate()),
-                loeEndDate(condition.endDate()),
-                phoneNumberEq(condition.phoneNumber())
-            )
+            .where(searchCondition)
             
         // Optimize count query by avoiding unnecessary JOINs when possible
         long totalCount = Optional.ofNullable(
             queryFactory
                 .select(selfIntroduction.count())
                 .from(selfIntroduction)
+                .leftJoin(member).on(member.id.eq(selfIntroduction.memberId))
-                .join(member).on(member.id.eq(selfIntroduction.memberId))
-                .where(
-                    nicknameEq(condition.nickname()),
-                    isOpenedEq(condition.isOpened()),
-                    startDateGoe(condition.startDate()),
-                    loeEndDate(condition.endDate()),
-                    phoneNumberEq(condition.phoneNumber())
-                )
+                .where(searchCondition)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
// add this helper near the top of the class
private BooleanExpression buildAdminSearchCondition(AdminSelfIntroductionSearchCondition condition) {
    return Expressions.allOf(
        nicknameEq(condition.nickname()),
        isOpenedEq(condition.isOpened()),
        startDateGoe(condition.startDate()),
        loeEndDate(condition.endDate()),
        phoneNumberEq(condition.phoneNumber())
    );
}

public Page<AdminSelfIntroductionView> findSelfIntroductions(
        AdminSelfIntroductionSearchCondition condition,
        Pageable pageable) {

    BooleanExpression searchCondition = buildAdminSearchCondition(condition);

    List<AdminSelfIntroductionView> content = queryFactory
        .select(new QAdminSelfIntroductionView(
            selfIntroduction.id,
            member.profile.nickname.value,
            member.profile.gender.stringValue(),
            selfIntroduction.isOpened,
            selfIntroduction.content,
            selfIntroduction.createdAt,
            selfIntroduction.updatedAt,
            selfIntroduction.deletedAt
        ))
        .from(selfIntroduction)
        .join(member).on(member.id.eq(selfIntroduction.memberId))
        .where(searchCondition)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

    long totalCount = Optional.ofNullable(
        queryFactory
            .select(selfIntroduction.count())
            .from(selfIntroduction)
            .leftJoin(member).on(member.id.eq(selfIntroduction.memberId))
            .where(searchCondition)
            .fetchOne()
    ).orElse(0L);

    return new PageImpl<>(content, pageable, totalCount);
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
between lines 85 and 129, the findSelfIntroductions method repeats the same
where conditions for both the main query and the count query, causing code
duplication, and the count query includes an unnecessary join with the member
table. Refactor by extracting the common where conditions into a separate
BooleanExpression variable to reuse in both queries, and remove the join from
the count query if it is not needed for filtering, to optimize performance and
reduce redundancy.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:32:19Z
author_association: NONE
diff_hunk: @@ -1,7 +1,10 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.query.selfintroduction;
 
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import atwoz.atwoz.community.query.selfintroduction.view.QAdminSelfIntroductionView;
 import com.querydsl.core.types.dsl.BooleanExpression;
-import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.querydsl.jpa.JPQLQueryFactory;
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**JPQLQueryFactory 변경을 검증해 주세요.**

JPAQueryFactory에서 JPQLQueryFactory로 변경되었습니다. 이 변경이 의도된 것인지, 그리고 기존 QueryDSL 설정과 호환되는지 확인이 필요합니다.





Also applies to: 25-25

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: JPQLQueryFactory 사용이 올바른지 확인하고 관련 설정 검토

# QueryDslConfig에서 어떤 QueryFactory bean이 설정되어 있는지 확인
rg -A 10 -B 5 "QueryFactory" --type java

# 다른 Repository들이 어떤 QueryFactory를 사용하는지 확인  
rg "QueryFactory" --type java -C 3
```

Length of output: 67034

---



</details>

**JPQLQueryFactory 변경 의도 및 일관성 검토 필요**  
현재 QueryDslConfig에서는 `JPAQueryFactory`만 빈으로 등록되어 있습니다.  
`AdminSelfIntroductionQueryRepository`와 `ProfileImageQueryRepository`에서 `JPQLQueryFactory`를 주입하려면 다음 중 하나를 선택해야 합니다:  
- Bean 메서드 반환 타입을 `JPQLQueryFactory`로 변경  
- 두 레포지토리의 주입 타입을 `JPAQueryFactory`로 되돌리기  

수정 위치:  
- src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java:7  
- src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java:7  

제안하는 코드 변경 예시:  
```diff
- import com.querydsl.jpa.JPQLQueryFactory;
+ import com.querydsl.jpa.impl.JPAQueryFactory;

- private final JPQLQueryFactory queryFactory;
+ private final JPAQueryFactory queryFactory;
```

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
at line 7 and similarly in ProfileImageQueryRepository.java at line 7, the
import and injection of JPQLQueryFactory is inconsistent with the current
QueryDslConfig which only registers JPAQueryFactory as a bean. To fix this,
revert the import and injection type back to JPAQueryFactory in both
repositories or alternatively update the QueryDslConfig bean method to return
JPQLQueryFactory to maintain consistency across the application.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:39:03Z
author_association: NONE
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: Kong-TaeHyeon
body: ```
+.                .leftJoin(member).on(member.id.eq(selfIntroduction.memberId))
-                .join(member).on(member.id.eq(selfIntroduction.memberId))
```

이 변경은 불필요한 조인을 개선한 것 같지는 않습니다! 오히려, 기존 Join 에서 존재하지 않는 회원의 소개 글은 포함하지 않으므로 행을 줄일 수 있다는 장점이 있을 것 같아요!
created_at: 2025-05-25T12:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,10 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.query.selfintroduction;
 
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import atwoz.atwoz.community.query.selfintroduction.view.QAdminSelfIntroductionView;
 import com.querydsl.core.types.dsl.BooleanExpression;
-import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.querydsl.jpa.JPQLQueryFactory;
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
user: login: Kong-TaeHyeon
body: 네! 확인하고 수정하겠습니다!
created_at: 2025-05-25T12:42:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: @Kong-TaeHyeon 맞습니다! 정확한 지적이에요. 

기존의 `join`을 유지하는 것이 올바른 접근입니다. `join`을 사용하면 존재하지 않는 회원의 셀프 소개글(orphaned records)은 카운트에서 제외되어 데이터 무결성을 보장하고, 실제로 유효한 셀프 소개글만 카운트하게 됩니다.

제가 제안한 `leftJoin`으로의 변경은 이 경우에는 적절하지 않았습니다. 비즈니스 로직을 고려할 때 `join`이 더 적합합니다.

다만 조건들을 공통 메서드로 추출하여 코드 중복을 제거하는 부분은 여전히 유효한 개선사항입니다:

```java
private BooleanExpression buildAdminSearchCondition(AdminSelfIntroductionSearchCondition condition) {
    return Expressions.allOf(
        nicknameEq(condition.nickname()),
        isOpenedEq(condition.isOpened()),
        startDateGoe(condition.startDate()),
        loeEndDate(condition.endDate()),
        phoneNumberEq(condition.phoneNumber())
    );
}
```

좋은 피드백 감사합니다!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java:0-0
Timestamp: 2025-05-25T12:42:51.898Z
Learning: In SelfIntroductionQueryRepository count queries, using `join` instead of `leftJoin` with member table is preferred to maintain referential integrity and exclude orphaned self-introductions where the member no longer exists.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Kong-TaeHyeon 맞습니다! 정확한 지적이에요. 

기존의 `join`을 유지하는 것이 올바른 접근입니다. `join`을 사용하면 존재하지 않는 회원의 셀프 소개글(orphaned records)은 카운트에서 제외되어 데이터 무결성을 보장하고, 실제로 유효한 셀프 소개글만 카운트하게 됩니다.

제가 제안한 `leftJoin`으로의 변경은 이 경우에는 적절하지 않았습니다. 비즈니스 로직을 고려할 때 `join`이 더 적합합니다.

다만 조건들을 공통 메서드로 추출하여 코드 중복을 제거하는 부분은 여전히 유효한 개선사항입니다:

```java
private BooleanExpression buildAdminSearchCondition(AdminSelfIntroductionSearchCondition condition) {
    return Expressions.allOf(
        nicknameEq(condition.nickname()),
        isOpenedEq(condition.isOpened()),
        startDateGoe(condition.startDate()),
        loeEndDate(condition.endDate()),
        phoneNumberEq(condition.phoneNumber())
    );
}
```

좋은 피드백 감사합니다!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java:0-0
Timestamp: 2025-05-25T12:42:51.898Z
Learning: In SelfIntroductionQueryRepository count queries, using `join` instead of `leftJoin` with member table is preferred to maintain referential integrity and exclude orphaned self-introductions where the member no longer exists.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:42:52Z
author_association: NONE
diff_hunk: @@ -1,7 +1,10 @@
-package atwoz.atwoz.admin.query.selfintroduction;
+package atwoz.atwoz.community.query.selfintroduction;
 
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import atwoz.atwoz.community.query.selfintroduction.view.QAdminSelfIntroductionView;
 import com.querydsl.core.types.dsl.BooleanExpression;
-import com.querydsl.jpa.impl.JPAQueryFactory;
+import com.querydsl.jpa.JPQLQueryFactory;
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: @Kong-TaeHyeon 감사합니다! JPAQueryFactory와 JPQLQueryFactory 간의 일관성 문제를 해결해 주시면 됩니다. 코드의 일관성을 유지하는 것이 중요하니까요.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%EA%B0%80%20%EC%95%8C%EB%A0%A4%EC%A4%80%20%EB%8C%80%EB%A1%9C%20JPQLQueryFactory%20%EB%8C%80%EC%8B%A0%20JPAQueryFactory%EB%A1%9C%20%EC%9D%BC%EA%B4%80%EC%84%B1%20%EC%9E%88%EA%B2%8C%20%EB%B3%80%EA%B2%BD%ED%95%98%EC%84%B8%EC%9A%94!%20%EC%84%A4%EC%A0%95%EA%B3%BC%20%EC%A3%BC%EC%9E%85%20%ED%83%80%EC%9E%85%20%EB%A7%9E%EC%B6%94%EB%A9%B4%20QueryDSL%20%EC%98%A4%EB%A5%98%20%EA%B1%B1%EC%A0%95%20%EB%81%9D!%20%F0%9F%9A%80%F0%9F%94%A7%20%23%EC%BD%94%EB%93%9C%ED%92%88%EC%A7%88%ED%96%A5%EC%83%81&url=https%3A%2F%2Fgithub.com%2Fatwoz-dev%2Fatwoz_server%2Fpull%2F210%23discussion_r2106189124)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Kong-TaeHyeon 감사합니다! JPAQueryFactory와 JPQLQueryFactory 간의 일관성 문제를 해결해 주시면 됩니다. 코드의 일관성을 유지하는 것이 중요하니까요.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%EA%B0%80%20%EC%95%8C%EB%A0%A4%EC%A4%80%20%EB%8C%80%EB%A1%9C%20JPQLQueryFactory%20%EB%8C%80%EC%8B%A0%20JPAQueryFactory%EB%A1%9C%20%EC%9D%BC%EA%B4%80%EC%84%B1%20%EC%9E%88%EA%B2%8C%20%EB%B3%80%EA%B2%BD%ED%95%98%EC%84%B8%EC%9A%94!%20%EC%84%A4%EC%A0%95%EA%B3%BC%20%EC%A3%BC%EC%9E%85%20%ED%83%80%EC%9E%85%20%EB%A7%9E%EC%B6%94%EB%A9%B4%20QueryDSL%20%EC%98%A4%EB%A5%98%20%EA%B1%B1%EC%A0%95%20%EB%81%9D!%20%F0%9F%9A%80%F0%9F%94%A7%20%23%EC%BD%94%EB%93%9C%ED%92%88%EC%A7%88%ED%96%A5%EC%83%81&url=https%3A%2F%2Fgithub.com%2Fatwoz-dev%2Fatwoz_server%2Fpull%2F210%23discussion_r2106189124)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:42:56Z
author_association: NONE
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
+
+    private BooleanExpression nicknameEq(String nickname) {
+        if (nickname == null || nickname.isBlank()) {
+            return null;
+        }
+        return member.profile.nickname.value.eq(nickname);
+    }
+
+    private BooleanExpression isOpenedEq(Boolean isOpened) {
+        if (isOpened == null) {
+            return null;
+        }
+        return selfIntroduction.isOpened.eq(isOpened);
+    }
+
+    private BooleanExpression startDateGoe(LocalDate startDate) {
+        if (startDate == null) {
+            return null;
+        }
+        return selfIntroduction.createdAt.goe(startDate.atStartOfDay());
+    }
+
+    private BooleanExpression loeEndDate(LocalDate endDate) {
+        if (endDate == null) {
+            return null;
+        }
+        return selfIntroduction.createdAt.loe(endDate.plusDays(1).atStartOfDay().minusSeconds(1));
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: Kong-TaeHyeon
body: 네 확인하여 수정하겠습니다!
created_at: 2025-05-25T12:44:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: Kong-TaeHyeon
body: 네 확인하고 수정하겠습니다!
created_at: 2025-05-25T12:44:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
+
+    private BooleanExpression nicknameEq(String nickname) {
+        if (nickname == null || nickname.isBlank()) {
+            return null;
+        }
+        return member.profile.nickname.value.eq(nickname);
+    }
+
+    private BooleanExpression isOpenedEq(Boolean isOpened) {
+        if (isOpened == null) {
+            return null;
+        }
+        return selfIntroduction.isOpened.eq(isOpened);
+    }
+
+    private BooleanExpression startDateGoe(LocalDate startDate) {
+        if (startDate == null) {
+            return null;
+        }
+        return selfIntroduction.createdAt.goe(startDate.atStartOfDay());
+    }
+
+    private BooleanExpression loeEndDate(LocalDate endDate) {
+        if (endDate == null) {
+            return null;
+        }
+        return selfIntroduction.createdAt.loe(endDate.plusDays(1).atStartOfDay().minusSeconds(1));
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: @Kong-TaeHyeon 감사합니다! 메서드명과 로직을 개선해주시면 코드가 더욱 명확해질 것입니다. 날짜 범위 조건이 간단하고 이해하기 쉬워질 거예요.



    (\   /)
   ( ._.)
  o_("_("

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20%EB%8D%95%EB%B6%84%EC%97%90%20%EB%82%A0%EC%A7%9C%20%EB%B2%94%EC%9C%84%20%EC%A1%B0%EA%B1%B4%20%EB%A1%9C%EC%A7%81%EC%9D%84%20%EB%8D%94%20%EB%AA%85%ED%99%95%ED%95%98%EA%B3%A0%20%EA%B0%84%EA%B2%B0%ED%95%98%EA%B2%8C%20%EA%B0%9C%EC%84%A0%ED%96%88%EC%96%B4%EC%9A%94!%20%EB%81%9D%20%EC%8B%9C%EA%B0%84%20%EA%B3%84%EC%82%B0%20%EB%B3%B5%EC%9E%A1%ED%95%A8%20%EC%A4%84%EC%9D%B4%EA%B3%A0%20%EA%B0%80%EB%8F%85%EC%84%B1%20UP!%20%F0%9F%9B%A0%EF%B8%8F%20%EA%BC%AD%20%EC%8D%A8%EB%B3%B4%EC%84%B8%EC%9A%94!%20%23%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0&url=https%3A%2F%2Fgithub.com%2Fatwoz-dev%2Fatwoz_server%2Fpull%2F210%23discussion_r2106187587)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Kong-TaeHyeon 감사합니다! 메서드명과 로직을 개선해주시면 코드가 더욱 명확해질 것입니다. 날짜 범위 조건이 간단하고 이해하기 쉬워질 거예요.



    (\   /)
   ( ._.)
  o_("_("

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20%EB%8D%95%EB%B6%84%EC%97%90%20%EB%82%A0%EC%A7%9C%20%EB%B2%94%EC%9C%84%20%EC%A1%B0%EA%B1%B4%20%EB%A1%9C%EC%A7%81%EC%9D%84%20%EB%8D%94%20%EB%AA%85%ED%99%95%ED%95%98%EA%B3%A0%20%EA%B0%84%EA%B2%B0%ED%95%98%EA%B2%8C%20%EA%B0%9C%EC%84%A0%ED%96%88%EC%96%B4%EC%9A%94!%20%EB%81%9D%20%EC%8B%9C%EA%B0%84%20%EA%B3%84%EC%82%B0%20%EB%B3%B5%EC%9E%A1%ED%95%A8%20%EC%A4%84%EC%9D%B4%EA%B3%A0%20%EA%B0%80%EB%8F%85%EC%84%B1%20UP!%20%F0%9F%9B%A0%EF%B8%8F%20%EA%BC%AD%20%EC%8D%A8%EB%B3%B4%EC%84%B8%EC%9A%94!%20%23%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0&url=https%3A%2F%2Fgithub.com%2Fatwoz-dev%2Fatwoz_server%2Fpull%2F210%23discussion_r2106187587)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:44:59Z
author_association: NONE
diff_hunk: @@ -79,6 +82,87 @@ public Optional<SelfIntroductionView> findSelfIntroductionByIdWithMemberId(Long
         return view.values().stream().findFirst();
     }
 
+    public Page<AdminSelfIntroductionView> findSelfIntroductions(AdminSelfIntroductionSearchCondition condition,
+        Pageable pageable) {
+        List<AdminSelfIntroductionView> content = queryFactory
+            .select(
+                new QAdminSelfIntroductionView(
+                    selfIntroduction.id,
+                    member.profile.nickname.value,
+                    member.profile.gender.stringValue(),
+                    selfIntroduction.isOpened,
+                    selfIntroduction.content,
+                    selfIntroduction.createdAt,
+                    selfIntroduction.updatedAt,
+                    selfIntroduction.deletedAt
+                )
+            )
+            .from(selfIntroduction)
+            .join(member).on(member.id.eq(selfIntroduction.memberId))
+            .where(
+                nicknameEq(condition.nickname()),
+                isOpenedEq(condition.isOpened()),
+                startDateGoe(condition.startDate()),
+                loeEndDate(condition.endDate()),
+                phoneNumberEq(condition.phoneNumber())
+            )
+            .offset(pageable.getOffset())
+            .limit(pageable.getPageSize())
+            .fetch();
+
+        long totalCount = Optional.ofNullable(
+            queryFactory
+                .select(selfIntroduction.count())
+                .from(selfIntroduction)
+                .join(member).on(member.id.eq(selfIntroduction.memberId))
+                .where(
+                    nicknameEq(condition.nickname()),
+                    isOpenedEq(condition.isOpened()),
+                    startDateGoe(condition.startDate()),
+                    loeEndDate(condition.endDate()),
+                    phoneNumberEq(condition.phoneNumber())
+                )
+                .fetchOne()
+        ).orElse(0L);
+
+        return new PageImpl<>(content, pageable, totalCount);
+    }
path: src/main/java/atwoz/atwoz/community/query/selfintroduction/SelfIntroductionQueryRepository.java
user: login: coderabbitai[bot]
body: @Kong-TaeHyeon 좋습니다! 조건 추출 리팩토링으로 코드 중복을 제거하시면 유지보수성이 더 좋아질 것입니다. 수정 후 궁금한 점이 있으시면 언제든 말씀해 주세요.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Kong-TaeHyeon 좋습니다! 조건 추출 리팩토링으로 코드 중복을 제거하시면 유지보수성이 더 좋아질 것입니다. 수정 후 궁금한 점이 있으시면 언제든 말씀해 주세요.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:45:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            selfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: Kong-TaeHyeon
body: 현재 Spring Security 의 Filter Chain 을 적용하지 않고, 임의의 Filter(TokenFilter.class)를 통해 프로젝트의 인증/인가 작업을 수행하고 있습니다!
created_at: 2025-05-25T12:54:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            selfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@Kong-TaeHyeon 커스텀 TokenFilter를 사용하고 계시는군요! 현재 구현된 TokenFilter가 관리자 권한을 어떻게 처리하는지 확인해보겠습니다.

---

🏁 Script executed:
```
#!/bin/bash
# TokenFilter 클래스 구현 확인
ast-grep --pattern 'class TokenFilter {
  $$$
}'

# TokenFilter 관련 파일 검색
fd "TokenFilter" --type f

# /admin 경로에 대한 보안 처리 확인
rg -A 5 -B 5 "/admin|ADMIN" --type java | grep -A 5 -B 5 "filter\|Filter"
```

Length of output: 4503

---

TokenFilter 구현과 관리자 권한 검증 로직을 자세히 확인해보겠습니다.

---

🏁 Script executed:
```
#!/bin/bash
# TokenFilter 전체 구현 확인
cat src/main/java/atwoz/atwoz/auth/presentation/filter/TokenFilter.java

# AuthContext 클래스 구조 확인
fd "AuthContext" --type f --exec cat {}

# Role enum 확인
cat src/main/java/atwoz/atwoz/common/enums/Role.java
```

Length of output: 4628

---

커스텀 인증 시스템을 분석한 결과, **중요한 보안 취약점**을 발견했습니다.




**보안 이슈:**
현재 `AdminSelfIntroductionController`는 인증된 사용자라면 **역할에 관계없이 모든 사용자가 접근 가능**합니다. `TokenFilter`는 인증(authentication)만 처리하고, 권한 부여(authorization) 로직이 없기 때문입니다.

**해결 방안:**
각 admin 메서드에 역할 검증 로직을 추가해야 합니다:

```java
@Operation(summary = "셀프 소개 목록 조회 API")
@GetMapping
public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
    @ModelAttribute AdminSelfIntroductionSearchCondition condition,
    @PageableDefault(size = 100) Pageable pageable,
    @AuthPrincipal AuthContext authContext
) {
    // 관리자 권한 검증
    if (authContext.getRole() != Role.ADMIN && authContext.getRole() != Role.SUPER_ADMIN) {
        return ResponseEntity.status(403)
            .body(BaseResponse.from(StatusType.FORBIDDEN));
    }
    
    return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
        adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
}
```

또는 공통 유틸리티 메서드나 커스텀 어노테이션을 만들어 DRY 원칙을 적용하는 것을 권장합니다.

---

🏁 Script executed:
```
#!/bin/bash
# PathMatcherHelper 구현 확인
cat src/main/java/atwoz/atwoz/auth/presentation/filter/PathMatcherConfig.java

# 다른 Admin 컨트롤러들이 권한 체크를 어떻게 하는지 확인
rg -A 3 -B 3 "authContext\.getRole|Role\.ADMIN|ADMIN" --type java
```

Length of output: 9271

---

분석 결과, **심각한 보안 취약점**이 확인되었습니다.




**현재 상황:**
- `/admin/selfintroduction`은 인증이 필요하지만 **역할 기반 권한 검증이 없음**
- `MEMBER` 역할 사용자도 관리자 API에 접근 가능
- `StatusType.ADMIN_ACCESS_DENIED`가 정의되어 있지만 사용되지 않음

**즉시 수정이 필요한 보안 이슈입니다.**

**권장 해결 방안:**

1. **유틸리티 클래스 생성:**
```java
@Component
public class AdminAuthValidator {
    
    public void validateAdminRole(AuthContext authContext) {
        if (authContext.getRole() != Role.ADMIN && authContext.getRole() != Role.SUPER_ADMIN) {
            throw new AccessDeniedException("관리자 권한이 필요합니다");
        }
    }
}
```

2. **컨트롤러에 권한 검증 추가:**
```java
@GetMapping
public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
    @ModelAttribute AdminSelfIntroductionSearchCondition condition,
    @PageableDefault(size = 100) Pageable pageable,
    @AuthPrincipal AuthContext authContext
) {
    adminAuthValidator.validateAdminRole(authContext);
    // 기존 로직...
}
```

3. **AOP 기반 해결 (권장):**
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminOnly {
}

@Aspect
@Component
public class AdminAuthAspect {
    
    @Before("@annotation(AdminOnly)")
    public void validateAdminAccess(JoinPoint joinPoint) {
        // AuthContext에서 역할 검증 로직
    }
}
```

**이 보안 이슈는 모든 admin 엔드포인트에 적용되어야 합니다.**



---

🏁 Script executed:
```
#!/bin/bash
# 다른 admin 컨트롤러들 확인
fd "*Admin*Controller*.java" --exec basename {}

# admin 패키지의 컨트롤러들 확인
rg -l "RequestMapping.*admin" --type java
```

Length of output: 1032

---

다른 기존 admin 컨트롤러들의 보안 구현을 확인해보겠습니다.




**현재 확인된 상황:**

시스템에 총 **7개의 admin 컨트롤러**가 존재하며, 모두 동일한 보안 취약점을 가질 가능성이 높습니다:

1. `AdminSelfIntroductionController` (신규)
2. `AdminInterviewQuestionController`
3. `AdminHeartPurchaseOptionController`
4. `AdminReportController`
5. `AdminHeartTransactionController`
6. `ScreeningController`
7. `AdminAuthController`

이는 **시스템 전반의 보안 아키텍처 문제**로 보이며, 즉시 해결이 필요합니다.

**시스템 차원의 해결책:**

1. **Admin Role Filter 생성 (권장):**
```java
@Component
@Order(3) // TokenFilter 이후 실행
public class AdminRoleFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        
        if (request.getRequestURI().startsWith("/admin/") && 
            !isExcludedAdminPath(request.getRequestURI())) {
            
            AuthContext authContext = getAuthContextFromRequest();
            if (!isAdminRole(authContext.getRole())) {
                setAccessDeniedResponse(response);
                return;
            }
        }
        
        filterChain.doFilter(request, response);
    }
    
    private boolean isAdminRole(Role role) {
        return role == Role.ADMIN || role == Role.SUPER_ADMIN;
    }
}
```

이렇게 하면 **모든 admin 컨트롤러가 자동으로 보호**됩니다.

---

🏁 Script executed:
```
#!/bin/bash
# 기존 admin 컨트롤러들의 권한 검증 로직 확인
echo "=== AdminReportController ==="
cat src/main/java/atwoz/atwoz/report/presentation/AdminReportController.java

echo -e "\n=== AdminAuthController ==="
cat src/main/java/atwoz/atwoz/admin/presentation/admin/AdminAuthController.java

echo -e "\n=== AdminInterviewQuestionController ==="
head -30 src/main/java/atwoz/atwoz/interview/presentation/question/AdminInterviewQuestionController.java
```

Length of output: 7461

---



</details>

**확인 결과: 시스템 전반의 심각한 보안 취약점 발견**

기존 admin 컨트롤러들도 **동일한 보안 문제**를 가지고 있습니다:

- `AdminReportController`: `@AuthPrincipal AuthContext`를 받지만 **역할 검증 없음**
- `AdminInterviewQuestionController`: **AuthContext 자체가 없음**
- `AdminAuthController`: login/signup/logout은 PathMatcherConfig에서 제외되어 별도 인증 불필요

**이는 전체 admin API의 보안 아키텍처 결함입니다.**

## 즉시 적용 가능한 해결책

**1. AdminRoleFilter 생성 (가장 효율적):**

```java
@Slf4j
@Component
@Order(3) // TokenFilter 이후 실행
public class AdminRoleFilter extends OncePerRequestFilter {

    private final AuthContext authContext;
    private final ResponseHandler responseHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        
        String uri = request.getRequestURI();
        
        // /admin/* 경로이면서 제외 대상이 아닌 경우 역할 검증
        if (uri.startsWith("/admin/") && !isExcludedAdminPath(uri)) {
            
            if (!isAdminRole(authContext.getRole())) {
                log.warn("관리자 권한 없는 접근 시도: userId={}, role={}, uri={}", 
                    authContext.getId(), authContext.getRole(), uri);
                responseHandler.setResponse(response, StatusType.ADMIN_ACCESS_DENIED);
                return;
            }
        }
        
        filterChain.doFilter(request, response);
    }
    
    private boolean isExcludedAdminPath(String uri) {
        return uri.equals("/admin/login") || 
               uri.equals("/admin/signup") || 
               uri.equals("/admin/logout");
    }
    
    private boolean isAdminRole(Role role) {
        return role == Role.ADMIN || role == Role.SUPER_ADMIN;
    }
}
```

**2. PathMatcherConfig 업데이트:**
```java
@Configuration
public class PathMatcherConfig {

    private static final List<String> EXCLUDED_URIS = List.of(
        "/member/login", "/member/logout",
        "/admin/login", "/admin/signup", "/admin/logout", // 관리자 인증 관련만 제외
        "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**",
        "/webjars/**"
    );
    // 기존 코드...
}
```

**이 방법의 장점:**
- **모든 기존 admin 컨트롤러 자동 보호**
- 새로운 admin API 추가 시 자동 적용
- 중앙 집중식 보안 관리
- 최소한의 코드 변경

**즉시 적용을 강력히 권장합니다.** 현재 상태로는 일반 회원이 모든 관리자 기능에 접근할 수 있습니다.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippet from `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java` (lines 20-69)

```java
@RestController
@RequestMapping("/self-introduction")
@RequiredArgsConstructor
public class SelfIntroductionController {

    private final SelfIntroductionService selfIntroductionService;
    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;

    @PostMapping
    public ResponseEntity<BaseResponse<Void>> write(@RequestBody SelfIntroductionWriteRequest request,
        @AuthPrincipal AuthContext authContext) {
        selfIntroductionService.write(request, authContext.getId());
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<BaseResponse<Void>> update(@PathVariable Long id,
        @RequestBody SelfIntroductionWriteRequest request, @AuthPrincipal AuthContext authContext) {
        selfIntroductionService.update(request, authContext.getId(), id);
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<BaseResponse<Void>> delete(@PathVariable Long id, @AuthPrincipal AuthContext authContext) {
        selfIntroductionService.delete(id, authContext.getId());
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }

    @GetMapping
    public ResponseEntity<BaseResponse<List<SelfIntroductionSummaryView>>> getIntroductions(
        @ModelAttribute SelfIntroductionSearchRequest searchRequest, Long lastId) {
        SelfIntroductionSearchCondition searchCondition = SelfIntroductionMapper.toSelfIntroductionSearchCondition(
            searchRequest);
        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
            selfIntroductionQueryRepository.findSelfIntroductions(searchCondition, lastId)));
    }

    @GetMapping("/{id}")
    public ResponseEntity<BaseResponse<SelfIntroductionView>> getIntroduction(@PathVariable Long id,
        @AuthPrincipal AuthContext authContext) {
        SelfIntroductionView view = selfIntroductionQueryRepository.findSelfIntroductionByIdWithMemberId(id,
            authContext.getId()).orElse(null);
        if (view == null) {
            return ResponseEntity.status(404)
                .body(BaseResponse.from(StatusType.NOT_FOUND));
        }

        return ResponseEntity.ok().body(BaseResponse.of(StatusType.OK, view));
    }
}
```

**Summary:**

- This controller handles user-level self-introduction APIs.
- Uses `@AuthPrincipal AuthContext authContext` to get authenticated user info, indicating some authentication mechanism is in place.
- Provides endpoints for creating, updating, deleting, and retrieving self-introductions.
- Uses `SelfIntroductionService` and `SelfIntroductionQueryRepository` for business logic and data access.
- The presence of `@AuthPrincipal` suggests a custom authentication principal resolver, possibly related to the custom TokenFilter mentioned in the user comment.

---

No other code snippets were provided that directly relate to authentication or the filter chain. The above snippet is relevant because it shows how authentication context is injected into controller methods, which ties into the user's comment about using a custom TokenFilter instead of Spring Security's filter chain.

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java:0-0
Timestamp: 2025-05-25T12:58:10.066Z
Learning: 현재 프로젝트는 커스텀 TokenFilter를 사용하여 인증을 처리하지만, 관리자 API에 대한 역할 기반 권한 검증(authorization)이 누락되어 있다. /admin/* 경로는 인증은 필요하지만 MEMBER 역할 사용자도 접근할 수 있는 보안 취약점이 존재한다.
```

---

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java:0-0
Timestamp: 2025-05-25T12:58:10.066Z
Learning: 프로젝트의 모든 admin 컨트롤러(AdminReportController, AdminInterviewQuestionController 등)가 role 기반 권한 검증 없이 구현되어 있어 시스템 전반의 보안 취약점이 존재한다. TokenFilter는 인증만 처리하고 권한 부여는 하지 않아, 일반 MEMBER 역할 사용자도 모든 /admin/* API에 접근할 수 있다.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@Kong-TaeHyeon 커스텀 TokenFilter를 사용하고 계시는군요! 현재 구현된 TokenFilter가 관리자 권한을 어떻게 처리하는지 확인해보겠습니다.

---

🏁 Script executed:
```
#!/bin/bash
# TokenFilter 클래스 구현 확인
ast-grep --pattern 'class TokenFilter {
  $$$
}'

# TokenFilter 관련 파일 검색
fd "TokenFilter" --type f

# /admin 경로에 대한 보안 처리 확인
rg -A 5 -B 5 "/admin|ADMIN" --type java | grep -A 5 -B 5 "filter\|Filter"
```

Length of output: 4503

---

TokenFilter 구현과 관리자 권한 검증 로직을 자세히 확인해보겠습니다.

---

🏁 Script executed:
```
#!/bin/bash
# TokenFilter 전체 구현 확인
cat src/main/java/atwoz/atwoz/auth/presentation/filter/TokenFilter.java

# AuthContext 클래스 구조 확인
fd "AuthContext" --type f --exec cat {}

# Role enum 확인
cat src/main/java/atwoz/atwoz/common/enums/Role.java
```

Length of output: 4628

---

커스텀 인증 시스템을 분석한 결과, **중요한 보안 취약점**을 발견했습니다.




**보안 이슈:**
현재 `AdminSelfIntroductionController`는 인증된 사용자라면 **역할에 관계없이 모든 사용자가 접근 가능**합니다. `TokenFilter`는 인증(authentication)만 처리하고, 권한 부여(authorization) 로직이 없기 때문입니다.

**해결 방안:**
각 admin 메서드에 역할 검증 로직을 추가해야 합니다:

```java
@Operation(summary = "셀프 소개 목록 조회 API")
@GetMapping
public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
    @ModelAttribute AdminSelfIntroductionSearchCondition condition,
    @PageableDefault(size = 100) Pageable pageable,
    @AuthPrincipal AuthContext authContext
) {
    // 관리자 권한 검증
    if (authContext.getRole() != Role.ADMIN && authContext.getRole() != Role.SUPER_ADMIN) {
        return ResponseEntity.status(403)
            .body(BaseResponse.from(StatusType.FORBIDDEN));
    }
    
    return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
        adminSelfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
}
```

또는 공통 유틸리티 메서드나 커스텀 어노테이션을 만들어 DRY 원칙을 적용하는 것을 권장합니다.

---

🏁 Script executed:
```
#!/bin/bash
# PathMatcherHelper 구현 확인
cat src/main/java/atwoz/atwoz/auth/presentation/filter/PathMatcherConfig.java

# 다른 Admin 컨트롤러들이 권한 체크를 어떻게 하는지 확인
rg -A 3 -B 3 "authContext\.getRole|Role\.ADMIN|ADMIN" --type java
```

Length of output: 9271

---

분석 결과, **심각한 보안 취약점**이 확인되었습니다.




**현재 상황:**
- `/admin/selfintroduction`은 인증이 필요하지만 **역할 기반 권한 검증이 없음**
- `MEMBER` 역할 사용자도 관리자 API에 접근 가능
- `StatusType.ADMIN_ACCESS_DENIED`가 정의되어 있지만 사용되지 않음

**즉시 수정이 필요한 보안 이슈입니다.**

**권장 해결 방안:**

1. **유틸리티 클래스 생성:**
```java
@Component
public class AdminAuthValidator {
    
    public void validateAdminRole(AuthContext authContext) {
        if (authContext.getRole() != Role.ADMIN && authContext.getRole() != Role.SUPER_ADMIN) {
            throw new AccessDeniedException("관리자 권한이 필요합니다");
        }
    }
}
```

2. **컨트롤러에 권한 검증 추가:**
```java
@GetMapping
public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
    @ModelAttribute AdminSelfIntroductionSearchCondition condition,
    @PageableDefault(size = 100) Pageable pageable,
    @AuthPrincipal AuthContext authContext
) {
    adminAuthValidator.validateAdminRole(authContext);
    // 기존 로직...
}
```

3. **AOP 기반 해결 (권장):**
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminOnly {
}

@Aspect
@Component
public class AdminAuthAspect {
    
    @Before("@annotation(AdminOnly)")
    public void validateAdminAccess(JoinPoint joinPoint) {
        // AuthContext에서 역할 검증 로직
    }
}
```

**이 보안 이슈는 모든 admin 엔드포인트에 적용되어야 합니다.**



---

🏁 Script executed:
```
#!/bin/bash
# 다른 admin 컨트롤러들 확인
fd "*Admin*Controller*.java" --exec basename {}

# admin 패키지의 컨트롤러들 확인
rg -l "RequestMapping.*admin" --type java
```

Length of output: 1032

---

다른 기존 admin 컨트롤러들의 보안 구현을 확인해보겠습니다.




**현재 확인된 상황:**

시스템에 총 **7개의 admin 컨트롤러**가 존재하며, 모두 동일한 보안 취약점을 가질 가능성이 높습니다:

1. `AdminSelfIntroductionController` (신규)
2. `AdminInterviewQuestionController`
3. `AdminHeartPurchaseOptionController`
4. `AdminReportController`
5. `AdminHeartTransactionController`
6. `ScreeningController`
7. `AdminAuthController`

이는 **시스템 전반의 보안 아키텍처 문제**로 보이며, 즉시 해결이 필요합니다.

**시스템 차원의 해결책:**

1. **Admin Role Filter 생성 (권장):**
```java
@Component
@Order(3) // TokenFilter 이후 실행
public class AdminRoleFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        
        if (request.getRequestURI().startsWith("/admin/") && 
            !isExcludedAdminPath(request.getRequestURI())) {
            
            AuthContext authContext = getAuthContextFromRequest();
            if (!isAdminRole(authContext.getRole())) {
                setAccessDeniedResponse(response);
                return;
            }
        }
        
        filterChain.doFilter(request, response);
    }
    
    private boolean isAdminRole(Role role) {
        return role == Role.ADMIN || role == Role.SUPER_ADMIN;
    }
}
```

이렇게 하면 **모든 admin 컨트롤러가 자동으로 보호**됩니다.

---

🏁 Script executed:
```
#!/bin/bash
# 기존 admin 컨트롤러들의 권한 검증 로직 확인
echo "=== AdminReportController ==="
cat src/main/java/atwoz/atwoz/report/presentation/AdminReportController.java

echo -e "\n=== AdminAuthController ==="
cat src/main/java/atwoz/atwoz/admin/presentation/admin/AdminAuthController.java

echo -e "\n=== AdminInterviewQuestionController ==="
head -30 src/main/java/atwoz/atwoz/interview/presentation/question/AdminInterviewQuestionController.java
```

Length of output: 7461

---



</details>

**확인 결과: 시스템 전반의 심각한 보안 취약점 발견**

기존 admin 컨트롤러들도 **동일한 보안 문제**를 가지고 있습니다:

- `AdminReportController`: `@AuthPrincipal AuthContext`를 받지만 **역할 검증 없음**
- `AdminInterviewQuestionController`: **AuthContext 자체가 없음**
- `AdminAuthController`: login/signup/logout은 PathMatcherConfig에서 제외되어 별도 인증 불필요

**이는 전체 admin API의 보안 아키텍처 결함입니다.**

## 즉시 적용 가능한 해결책

**1. AdminRoleFilter 생성 (가장 효율적):**

```java
@Slf4j
@Component
@Order(3) // TokenFilter 이후 실행
public class AdminRoleFilter extends OncePerRequestFilter {

    private final AuthContext authContext;
    private final ResponseHandler responseHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        
        String uri = request.getRequestURI();
        
        // /admin/* 경로이면서 제외 대상이 아닌 경우 역할 검증
        if (uri.startsWith("/admin/") && !isExcludedAdminPath(uri)) {
            
            if (!isAdminRole(authContext.getRole())) {
                log.warn("관리자 권한 없는 접근 시도: userId={}, role={}, uri={}", 
                    authContext.getId(), authContext.getRole(), uri);
                responseHandler.setResponse(response, StatusType.ADMIN_ACCESS_DENIED);
                return;
            }
        }
        
        filterChain.doFilter(request, response);
    }
    
    private boolean isExcludedAdminPath(String uri) {
        return uri.equals("/admin/login") || 
               uri.equals("/admin/signup") || 
               uri.equals("/admin/logout");
    }
    
    private boolean isAdminRole(Role role) {
        return role == Role.ADMIN || role == Role.SUPER_ADMIN;
    }
}
```

**2. PathMatcherConfig 업데이트:**
```java
@Configuration
public class PathMatcherConfig {

    private static final List<String> EXCLUDED_URIS = List.of(
        "/member/login", "/member/logout",
        "/admin/login", "/admin/signup", "/admin/logout", // 관리자 인증 관련만 제외
        "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**",
        "/webjars/**"
    );
    // 기존 코드...
}
```

**이 방법의 장점:**
- **모든 기존 admin 컨트롤러 자동 보호**
- 새로운 admin API 추가 시 자동 적용
- 중앙 집중식 보안 관리
- 최소한의 코드 변경

**즉시 적용을 강력히 권장합니다.** 현재 상태로는 일반 회원이 모든 관리자 기능에 접근할 수 있습니다.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippet from `src/main/java/atwoz/atwoz/community/presentation/selfintroduction/SelfIntroductionController.java` (lines 20-69)

```java
@RestController
@RequestMapping("/self-introduction")
@RequiredArgsConstructor
public class SelfIntroductionController {

    private final SelfIntroductionService selfIntroductionService;
    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;

    @PostMapping
    public ResponseEntity<BaseResponse<Void>> write(@RequestBody SelfIntroductionWriteRequest request,
        @AuthPrincipal AuthContext authContext) {
        selfIntroductionService.write(request, authContext.getId());
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }

    @PatchMapping("/{id}")
    public ResponseEntity<BaseResponse<Void>> update(@PathVariable Long id,
        @RequestBody SelfIntroductionWriteRequest request, @AuthPrincipal AuthContext authContext) {
        selfIntroductionService.update(request, authContext.getId(), id);
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<BaseResponse<Void>> delete(@PathVariable Long id, @AuthPrincipal AuthContext authContext) {
        selfIntroductionService.delete(id, authContext.getId());
        return ResponseEntity.ok(BaseResponse.from(StatusType.OK));
    }

    @GetMapping
    public ResponseEntity<BaseResponse<List<SelfIntroductionSummaryView>>> getIntroductions(
        @ModelAttribute SelfIntroductionSearchRequest searchRequest, Long lastId) {
        SelfIntroductionSearchCondition searchCondition = SelfIntroductionMapper.toSelfIntroductionSearchCondition(
            searchRequest);
        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
            selfIntroductionQueryRepository.findSelfIntroductions(searchCondition, lastId)));
    }

    @GetMapping("/{id}")
    public ResponseEntity<BaseResponse<SelfIntroductionView>> getIntroduction(@PathVariable Long id,
        @AuthPrincipal AuthContext authContext) {
        SelfIntroductionView view = selfIntroductionQueryRepository.findSelfIntroductionByIdWithMemberId(id,
            authContext.getId()).orElse(null);
        if (view == null) {
            return ResponseEntity.status(404)
                .body(BaseResponse.from(StatusType.NOT_FOUND));
        }

        return ResponseEntity.ok().body(BaseResponse.of(StatusType.OK, view));
    }
}
```

**Summary:**

- This controller handles user-level self-introduction APIs.
- Uses `@AuthPrincipal AuthContext authContext` to get authenticated user info, indicating some authentication mechanism is in place.
- Provides endpoints for creating, updating, deleting, and retrieving self-introductions.
- Uses `SelfIntroductionService` and `SelfIntroductionQueryRepository` for business logic and data access.
- The presence of `@AuthPrincipal` suggests a custom authentication principal resolver, possibly related to the custom TokenFilter mentioned in the user comment.

---

No other code snippets were provided that directly relate to authentication or the filter chain. The above snippet is relevant because it shows how authentication context is injected into controller methods, which ties into the user's comment about using a custom TokenFilter instead of Spring Security's filter chain.

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java:0-0
Timestamp: 2025-05-25T12:58:10.066Z
Learning: 현재 프로젝트는 커스텀 TokenFilter를 사용하여 인증을 처리하지만, 관리자 API에 대한 역할 기반 권한 검증(authorization)이 누락되어 있다. /admin/* 경로는 인증은 필요하지만 MEMBER 역할 사용자도 접근할 수 있는 보안 취약점이 존재한다.
```

---

```
Learnt from: Kong-TaeHyeon
PR: atwoz-dev/atwoz_server#210
File: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java:0-0
Timestamp: 2025-05-25T12:58:10.066Z
Learning: 프로젝트의 모든 admin 컨트롤러(AdminReportController, AdminInterviewQuestionController 등)가 role 기반 권한 검증 없이 구현되어 있어 시스템 전반의 보안 취약점이 존재한다. TokenFilter는 인증만 처리하고 권한 부여는 하지 않아, 일반 MEMBER 역할 사용자도 모든 /admin/* API에 접근할 수 있다.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T12:58:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            selfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: Kong-TaeHyeon
body: 해당 이슈는 따로 작업하겠습니다!
created_at: 2025-05-26T06:09:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+package atwoz.atwoz.community.presentation.selfintroduction;
+
+import atwoz.atwoz.common.enums.StatusType;
+import atwoz.atwoz.common.response.BaseResponse;
+import atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionService;
+import atwoz.atwoz.community.presentation.selfintroduction.dto.AdminSelfIntroductionSearchCondition;
+import atwoz.atwoz.community.query.selfintroduction.SelfIntroductionQueryRepository;
+import atwoz.atwoz.community.query.selfintroduction.view.AdminSelfIntroductionView;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@Tag(name = "어드민 셀프 소개 관리 API")
+@RestController
+@RequestMapping("/admin/selfintroduction")
+@RequiredArgsConstructor
+public class AdminSelfIntroductionController {
+
+    private final SelfIntroductionQueryRepository selfIntroductionQueryRepository;
+    private final SelfIntroductionService selfIntroductionService;
+
+    @Operation(summary = "셀프 소개 목록 조회 API")
+    @GetMapping
+    public ResponseEntity<BaseResponse<Page<AdminSelfIntroductionView>>> getSelfIntroductions(
+        @ModelAttribute AdminSelfIntroductionSearchCondition condition,
+        @PageableDefault(size = 100) Pageable pageable
+    ) {
+        return ResponseEntity.ok(BaseResponse.of(StatusType.OK,
+            selfIntroductionQueryRepository.findSelfIntroductions(condition, pageable)));
+    }
path: src/main/java/atwoz/atwoz/community/presentation/selfintroduction/AdminSelfIntroductionController.java
user: login: coderabbitai[bot]
body: @Kong-TaeHyeon 좋은 접근입니다! 보안 관련 수정사항은 별도 PR로 처리하는 것이 맞습니다.

이 보안 취약점을 추적하기 위해 GitHub 이슈를 생성하시겠습니까? 동의하시면 적절한 제목과 설명으로 이슈를 만들어드리겠습니다.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Kong-TaeHyeon 좋은 접근입니다! 보안 관련 수정사항은 별도 PR로 처리하는 것이 맞습니다.

이 보안 취약점을 추적하기 위해 GitHub 이슈를 생성하시겠습니까? 동의하시면 적절한 제목과 설명으로 이슈를 만들어드리겠습니다.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-26T06:10:04Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-24T07:10:42Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

이번 변경 사항에서는 관리자용 SelfIntroduction 관리 기능이 확장되었습니다. `AdminSelfIntroductionController`가 새로 추가되어, 관리자가 SelfIntroduction의 공개(open)/비공개(close) 상태를 변경할 수 있는 PATCH 엔드포인트(`/admin/selfintroduction/{id}/open`, `/admin/selfintroduction/{id}/close`)가 도입되었습니다. 이 엔드포인트들은 서비스 계층의 `SelfIntroductionService.changeOpenStatus`를 호출하여 도메인 객체의 `open()` 및 `close()` 메서드를 통해 상태를 변경합니다. 관련 단위 테스트가 추가되어 정상 동작과 예외 상황을 검증하며, DTO 및 Query 관련 클래스들의 패키지 구조가 `community.presentation.selfintroduction.dto`로 통일되고, 쿼리 조건 빌드 로직이 개선되었습니다. 또한 일부 QueryDSL 관련 import 및 타입 변경, 테스트 클래스 패키지 이동 및 어서션 방식 변경이 포함되어 있습니다.

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Admin as AdminUser
    participant Controller as AdminSelfIntroductionController
    participant Service as SelfIntroductionService
    participant Repo as SelfIntroductionRepository
    participant Domain as SelfIntroduction

    Admin->>Controller: PATCH /admin/selfintroduction/{id}/open
    Controller->>Service: changeOpenStatus(id, true)
    Service->>Repo: findById(id)
    Repo-->>Service: SelfIntroduction
    Service->>Domain: open()
    Service-->>Controller: void
    Controller-->>Admin: 200 OK (BaseResponse<Void>)

    Admin->>Controller: PATCH /admin/selfintroduction/{id}/close
    Controller->>Service: changeOpenStatus(id, false)
    Service->>Repo: findById(id)
    Repo-->>Service: SelfIntroduction
    Service->>Domain: close()
    Service-->>Controller: void
    Controller-->>Admin: 200 OK (BaseResponse<Void>)
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f2de5ec6295fbaf9e90dbfc88c243054a933cc6f and 30452e0f4aac250cdb75de15f610893cde068ca3.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java` (5 hunks)
* `src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/main/java/atwoz/atwoz/member/query/profileimage/ProfileImageQueryRepository.java

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/main/java/atwoz/atwoz/community/query/selfintroduction/AdminSelfIntroductionQueryRepository.java

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=atwoz-dev/atwoz_server&utm_content=210):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-24T07:12:25Z
body: ## Test Results
509 tests  +4   509 ✅ +4   9s ⏱️ ±0s
157 suites +1     0 💤 ±0 
157 files   +1     0 ❌ ±0 

Results for commit 30452e0f. ± Comparison against base commit b917f029.

<details>
  <summary>This pull request <b>removes</b> 5 and <b>adds</b> 9 tests. <i>Note that renamed tests count towards both.</i></summary>

```
atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionQueryRepositoryTest ‑ 공개 여부로 셀프 소개를 조회합니다.
atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionQueryRepositoryTest ‑ 닉네임으로 셀프 소개를 조회합니다.
atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionQueryRepositoryTest ‑ 모든 셀프 소개를 조회합니다.
atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionQueryRepositoryTest ‑ 시작일과 종료일을 설정해 셀프 소개를 조회합니다.
atwoz.atwoz.admin.query.selfintroduction.AdminSelfIntroductionQueryRepositoryTest ‑ 전화번호로 셀프 소개를 조회합니다.
```

```
atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionServiceTest$UpdateSelfIntroduction ‑ 변경하고자 하는 셀프 소개가 존재하지 않는 경우, 예외 발생
atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionServiceTest$UpdateSelfIntroduction ‑ 셀프 소개를 공개로 변경한다.
atwoz.atwoz.community.command.application.selfintroduction.SelfIntroductionServiceTest$UpdateSelfIntroduction ‑ 셀프 소개를 비공개로 변경한다.
atwoz.atwoz.community.command.domain.selfintroduction.SelfIntroductionTest$Update ‑ 셀프 소개를 공개로 변경한다.
atwoz.atwoz.community.query.AdminSelfIntroductionQueryRepositoryTest ‑ 공개 여부로 셀프 소개를 조회합니다.
atwoz.atwoz.community.query.AdminSelfIntroductionQueryRepositoryTest ‑ 닉네임으로 셀프 소개를 조회합니다.
atwoz.atwoz.community.query.AdminSelfIntroductionQueryRepositoryTest ‑ 모든 셀프 소개를 조회합니다.
atwoz.atwoz.community.query.AdminSelfIntroductionQueryRepositoryTest ‑ 시작일과 종료일을 설정해 셀프 소개를 조회합니다.
atwoz.atwoz.community.query.AdminSelfIntroductionQueryRepositoryTest ‑ 전화번호로 셀프 소개를 조회합니다.
```
</details>

[test-results]:data:application/gzip;base64,H4sIAHQRM2gC/12Myw6CMBAAf4X07GELLA9/xtRtm2wEavo4Gf/dggaKx5lJ5iUsTyaIayWxv1QiJI4F6uRVZLdkMWbMKa4RYadbSET/6sHPrGAXVvF0EsZ753/Gp+V4rnBefs1x3LgYblz+yM0zxwyigRZrA7ZVimoE0vcetZFoOwnD2JA20A2kGvH+ANoN4MMHAQAA

:recycle: This comment has been updated with latest results.
author_association: NONE