Skip to content

Commit f96997a

Browse files
authored
[RELEASE] 260119 릴리즈 (#609)
* fix: yml 들여쓰기 수정 (#555) * fix: yml 들여쓰기 수정 * fix: jdk 변경 * refactor: 멘토 및 채팅 관련 API 응답 수정 (#537) * refactor: 멘토의 멘토링 조회 응답에서 mentoringId가 아니라 roomId를 포함하도록 * refactor: 파트너가 멘토인 경우 partnerId는 mentorId로 - AS IS: 멘토/멘티 모두 partnerId가 siteUserId - TO BE: 멘티: siteUserId, 멘토: mentorId * refactor: 응답의 senderId가 mentorId/siteUserId가 되도록 * refactor: senderId에 해당하는 chatParticipant가 없을 경우 예외 처리하는 로직 추가 * refactor: 메서드명에 맞게 시그니처 변경 * refactor: getChatMessages 메서드에서 응답으로 siteUserId를 넘겨주도록 - AS IS: mentorId(mentor) / siteUserId(mentee) - TO BE: siteUserId(all) * refactor: 헬퍼 메서드로 메서드 복잡성을 분산한다 * refactor: getChatPartner 메서드의 응답으로 siteUserId를 넘겨주도록 - AS IS: mentorId(mentor) / siteUserId(mentee) - TO BE: siteUserId(all) * refactor: CD 성능 개선 (#552) * fix: deprecated된 base image를 eclipse-temurin:17-jdk로 변경 * refactor: scp 파일 전송하는 방식에서 GHCR로 push/pull하도록 변경 * fix: GHCR image 제거시 Org의 GITHUB_TOKEN 사용하도록 변경 * refactor : scp 파일 전송하는 방식에서 GHCR로 push/pull하도록 prod-cd.yml과 docker-compose.prod.yml 변경 * fix: prod 인스턴스 old image 이름 통일 * fix: prod-cd.yml StrictHostKeyChecking 옵션 문법 오류 수정 * fix: prod-cd.yml StrictHostKeyChecking 옵션 문법 오류 수정 * fix: dev-cd.yml Old images 정리 작업 중 이미지 이름 불일치 문제 해결 * chore: 마지막 줄 개행 추가 * chore: 마지막 줄 개행 추가 * feat: stage 인스턴스에 대한 최신 이미지 5개 유지 기능 및 old 이미지 제거 기능 추가 * chore: 중복된 환경변수 지정 제거 * chore: 중복된 pem키 생성 로직 제거 * fix: 잘못된 pem키 이름 수정 * refactor: 원격 호스트에서 pull할 경우, 최소 권한으로 실행하도록 Github App으로 임시토큰 발급하도록 수정 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 (#565) * fix: GitHub app token permission 문제 해결 (#566) * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 Contents 읽기 권한 추가 * fix: GitHub app token permission 문제 오류 해결 (#567) * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 Contents 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: GitHub app token permission이 repo 레벨에서 부여되는 문제 해결 (#568) * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 Contents 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: GitHub app token permission 권한 오류 해결 (#569) * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 Contents 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * test: fork repo의 작업 branch에서 해당 workflows가 실행되도록 임시 수정 * refactor: test용 설정 제거 * feat: claude.md 파일 추가 (#560) * fix : 동일 멘토 멘티 중복 신청 불가능하도록 수정 (#563) * fix : 동일 멘토 멘티 중복 신청 불가능하도록 수정 - UK 제약조건 추가 - flyway script 추가 - CustomException 추가 - Service 로직 수정 - Test code 추가 * fix : column명 오류 수정 - column명 camelCase -> snake_case로 변경 * fix : column명 오류 수정 - column명 name으로 명시 * fix: GitHub app token permission 권한 오류 해결 (#570) * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 읽기 권한 명시 문법 오류 수정 * fix: Github App이 발행한 임시 토큰에 대해 Contents 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * fix: Github App이 발행한 임시 토큰에 대해 조직 레벨에서 읽기 권한 추가 * test: fork repo의 작업 branch에서 해당 workflows가 실행되도록 임시 수정 * refactor: test용 설정 제거 * fix: docker login username 불일치 문제 * refactor: 최소권한 원칙 적용을 위한 Action Job 분리 * refactor: 필요없는 주석 제거 * fix: GHCR 정리 권한 PAT로 해결 (#573) * feat: 지원서가 APPROVED 인 유저의 멘토 생성 기능 추가 (#562) * feat: 지원서가 APPROVED 인 유저의 멘토 생성 기능 추가 * refactor: submitMentorApplication 메서드의 멘토 지원 유효 검증 부분을 메서드로 추출 * refactor: MentorMyPage 생성, 수정 부분의 channel 생성, 업데이트 부분 중복 제거 * test: Mentor 생성 관련 테스트 추가 * fix: 코드래빗 리뷰 적용 * refactor: 멘토 생성 시 channelRequest가 null 일 떄 예외 처리 * feat: MentorApplicationRequest 필드에 유효성 어노테이션 추가 * test: 채널 검색 시 siteUserId로 조회하는 문제 해결 * fix: 리뷰 수정사항 적용 * fix: 파일 끝에 개행 추가 * refactor: 멘토 생성 메서드에서 siteUser의 검증 제외 * refactor: dto 단에서 채널 리스트 null 검증 * feat: MentorApplication에 termId 추가 flyway 스크립트 추가 * fix: flyway 버전 충돌 해결 * feat: 어드민 멘토 승격 요청 페이징 조회 기능 추가 (#576) * feat: 어드민 멘토 지원서 페이징 조회 기능 추가 * feat: mentor/repository 패키지에 custom 패키지 추가 - custom 패키지에 페이징 조회를 책임지는 MentorApplicationFilterRepository 추가 - MentorApplicationSearchCondition 에서 넘긴 keyword 기반으로 닉네임, 권역, 나라, 학교명으로 필터링 검색 기능 추가 - MentorApplicationSearchCondition 에서 넘긴 mentorApplicationStatus 기반으로 승인, 거절, 진행중 으로 필터링 기능 추가 * test: 어드민 멘토 지원서 페이징 조회 테스트 추가 * feat: MentorApplication 엔티티에 approved_at 필드 추가 flyway 스크립트 작성 * fix: 파일 끝에 개행 추가 * refactor: 페이징 조회 시 count 쿼리에 불필요한 조인 막기 * fix: 코드래빗 리뷰 적용 * fix: flyway V39 스크립트 파일명 수정 * test: 테스트 코드 오류 수정, 검증 추가   * test: 기대하는 값이랑 다른 테스트 응답을 수정합니다 * feat: 어드민 멘토 승격 지원서 승인/거절 기능, 상태 별 지원서 개수 조회 기능 추가 (#577) * feat: 어드민 멘토 승격 지원서 승인/거절 기능 추가 * test: 어드민 멘토 지원서 승인/거절 테스트 추가 * feat: 멘토 지원서 상태별 개수 조회 기능 추가 * test: 멘토 지원서 상태별 개수 조회 테스트 추가 * fix: 대학이 선택되지 않은 멘토 지원서 승인 시 예외 발생하도록 수정 * refactor: 리뷰 내용 반영 * refactor: MENTOR_APPLICATION_ALREADY_CONFIRM -> MENTOR_APPLICATION_ALREADY_CONFIRMED 로 수정 * refactor: 멘토 지원서 거절 사유 관련하여 기획에 명시되지 않은 길이 제한 제거 * refactor: 리뷰 적용 * refactor: 변수명, 필드명 일관성 맞추기 * test: assertAll 적용 * feat: region 관련 관리 기능 추가 (#561) * feat: 지역 생성 기능 구현 (AdminRegion) 지역을 생성하는 기능을 구현했습니다: - AdminRegionCreateRequest: 지역 생성 요청 DTO - AdminRegionResponse: 지역 응답 DTO - AdminRegionService.createRegion(): 중복 검사를 포함한 지역 생성 로직 - AdminRegionController.createRegion(): HTTP POST 엔드포인트 중복 검사: - 지역 코드 중복 확인 - 한글명 중복 확인 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: 지역 수정/삭제/조회 기능 구현 및 테스트 추가 (AdminRegion) 지역 관리 기능을 완성했습니다: 구현 기능: - AdminRegionUpdateRequest: 지역 수정 요청 DTO - AdminRegionService.updateRegion(): 한글명 중복 검사를 포함한 지역 수정 - AdminRegionService.deleteRegion(): 지역 삭제 - AdminRegionService.getAllRegions(): 전체 지역 조회 - AdminRegionController: 수정/삭제/조회 HTTP 엔드포인트 테스트 코드 (AdminRegionServiceTest): - CREATE: 정상 생성, 코드 중복, 한글명 중복 테스트 - UPDATE: 정상 수정, NOT_FOUND, 중복 한글명, 동일 한글명 테스트 - DELETE: 정상 삭제, NOT_FOUND 테스트 - READ: 빈 목록, 전체 조회 테스트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: 지역 관리 관련 에러코드 추가 (ErrorCode) 지역 관리 기능에 필요한 에러코드를 추가했습니다: - REGION_NOT_FOUND: 존재하지 않는 지역 - REGION_ALREADY_EXISTS: 이미 존재하는 지역 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: jpa 오류 수정 * refactor: 코드리뷰 반영 --------- Co-authored-by: Claude <noreply@anthropic.com> * fix: config.alloy 파일 경로 불일치 문제 해결 (#586) * fix: config.alloy 파일 경로 불일치 문제 해결 * refactor: docker-compose를 수정하는게 아닌 cd.yml의 경로를 수정하여 해결 * feat: 소셜 로그인 사용자는 비밀번호 변경을 할 수 없다. (#582) * feat: 소셜 로그인 사용자는 비밀번호 변경을 할 수 없도록 * test: 소셜 로그인 사용자 비밀번호 변경 관련 테스트 코드 작성 * chore: 컨벤션에 맞게 메서드명 변경 - ~~ 예외가 발생한다 * chore: 충돌 해결 * fix: Upgrade 헤더 유무에 따라 Connection 헤더의 값을 동적으로 설정하도록 (#581) * fix: Upgrade 헤더 유무에 따라 Connection 헤더의 값을 동적으로 설정하도록 - Upgrade 헤더가 존재하면(e.g. WebSocket) upgrade로 설정 - Upgrade 헤더가 존재하지 않으면 keep-alive로 설정 * chore: 서브모듈 업데이트 * feat: 멘토 지원서 대학교 매핑 기능, 대학 선택 상태 페이징 조건 추가 (#583) * feat: 멘토 지원서 검색 조건에 UniversitySelectType 추가 * feat: 어드민 멘토 지원서 페이징 조회 응답에 UniversitySelectType 추가 * test: 멘토 지원서 조회 테스트 추가 - test: UniversitySelectType 기반 페이징 조회 테스트 추가 * feat: 멘토 지원서에 대학 매핑 기능 추가 * test: 멘토 지원서 대학 매핑 테스트 추가 * refactor: 의미 없는 import 제거 * refactor: 리뷰 내용 반영 * refactor: 개행 및 공백 추가 * refactor: pathVariable 네이밍을 kebab-case 로 통일 * refactor: Service 레이어의 검증 로직을 도메인으로 이동 * refactor: PENDING 상태 및 OTHER 타입 검증을 도메인 메서드로 관리 * refactor: assignUniversity() 호출 전 검증 책임을 도메인 엔티티에 위임 * test : assertAll 로 검증 그룹화 * refactor: 스프링 부트 앱 외의 사이드 인프라 배포 과정을 분리 (#592) * refactor: dev 환경에서의 side-infra 배포 과정 분리 * refactor: prod 환경에서의 side-infra 배포 과정 분리 * refactor: docker-compose 가 실행되고 있지 않아도 스크립트가 실패하지 않게 변경 * fix: docker compose up 시에 사용할 환경변수 중 누락된 변수를 추가 * fix: S3 이름 불일치 문제 해결 (#594) * fix: s3 이름 불일치 문제 해결 * fix: s3와의 연동된 cloudfront URL로 수정 * refactor: 분리한 사이드 인프라에 대해서 필요없는 파일 정리 (#596) * test: flyway 스크립트를 검증하는 테스트 코드 작성 (#588) * test: flyway 스크립트를 검증하는 테스트 코드 작성 * fix: DirtiesContext 어노테이션을 통해 기존 컨텍스트를 폐기하도록 - 새로운 MySQL 환경에서 마이그레이션이 이루어지도록 수정 * fix: flyway 검증용의 별도의 MySQL 컨테이너를 사용하도록 * chore: 테스트 의도를 쉽게 이해할 수 있도록 주석 추가 * chore: 명시적으로 컨테이너를 시작하도록 - 또한 MySQLTestContainer 코드와 유사한 컨벤션을 가지도록 수정 * refactor: 게시글 조회 응답에 댓글 deprecated 여부 포함하도록 (#599) * feat: 유저의 멘토 지원서 신청 이력 조회 기능 추가 (#603) * feat: 유저의 멘토 지원 이력 조회 기능 추가 * refactor: 매개변수 타입 통일 * refactor: long 타입을 Long 으로 수정 * test: 멘토 지원서 이력 조회 테스트 추가 * test: MentorApplicationFixtureBuilder 에 rejectedReason 필드 및 빌더 메서드 추가 * refactor: 리뷰 사항 적용 * test: 멘토 지원서 이력 조회 에서 user와 university 재사용 * refactor: 긴 uri 를 짧게 수정 * refactor: 서브모듈 해시값 되돌리기 * refactor: 개행 지우기 * refactor: applicationOrder 자료형을 long 으로 수정 * fix: applicationOrder 를 int 자료형으로 처리하도록 복구 - 순서를 나타내고, 해당 값이 21억을 넘길 수 없다 판단하여 더 적합한 int 자료형으로 복구 * test: long type 을 기대하던 테스트 에러 해결 * fix: 탈퇴한 사용자가 물리적 삭제가 되지 않았던 문제를 해결한다 (#574) * refactor: FK에 ON DELETE CASCADE 옵션 추가 * refactor: 삭제 메서드로 사용자 연관 데이터를 삭제하도록 * feat: 어드민 유저 차단 기능 추가 (#604) * feat: 어드민 차단 기능 * test: 어드민 차단 기능 * feat: API 성능 로깅, 쿼리 별 메트릭 전송 추가 (#602) * feat: HTTP 요청/응답 로깅 필터 구현 - traceId 기반 요청 추적 - 요청/응답 로깅 - CustomExceptionHandler와 중복 로깅 방지 - Actuator 엔드포인트 로깅 제외 * feat: ExceptionHandler에 중복 로깅 방지 플래그 및 userId 로깅 추가 * feat: API 수행시간 로깅 인터셉터 추가 * feat: ApiPerf 인터셉터, Logging 필터 빈 등록 * refactor: logback 설정 변경 - info, warn, error, api_perf 로 로그 파일 분리해서 관리 * feat: 쿼리 별 수행시간 메트릭 모니터링 추가 * feat: 데이터소스 프록시 의존성 및 config 파일 추가 * feat: 데이터 소스 프록시가 metric을 찍을 수 있도록 listener 클래스 추가 * feat: 요청 시 method, uri 정보를 listener에서 활용하기 위해 RequestContext 및 관련 interceptor 추가 * refactor: 비효율적인 Time 빌더 생성 개선 - Time.builder 를 사용하면 매번 빌더를 생성하여 비효율적인 문제를 meterRegistry.timer 방식으로 해결 * feat: 로깅을 위해 HttpServeletRequest 속성에 userId 추가 * refactor: logback 설정 중 local은 console만 찍도록 수정 * refactor: FILE_PATTERN -> LOG_PATTERN 으로 수정 * test: TokenAuthenticationFilter에서 request에 userId 설정 검증 추가 - principal 조회 예외를 막기 위해 siteUserDetailsService given 추가 * refacotr: 코드 래빗 리뷰사항 반영 * test: 중복되는 테스트 제거 * refactor: 사용하지 않는 필드 제거 * refactor: 리뷰 내용 반영 * refactor: ApiPerformanceInterceptor에서 uri 정규화 관련 코드 제거 * refactor: ApiPerformanceInterceptor에서 if-return 문을 if-else 문으로 수정 * refactor: 추가한 interceptor 의 설정에 actuator 경로 무시하도록 셋팅 * refactor: 중복되는 의존성 제거 * refactor: 로깅 시 민감한 쿼리 파라미터 마스킹 - EXCLUDE_QUERIES 에 해당하는 쿼리 파라미터 KEY 값의 VALUE 를 masking 값으로 치환 * refactor: 예외 처리 후에도 Response 로그 찍도록 수정 * refactor: CustomExceptionHandler 원상복구 - Response 로그를 통해 user를 추적할 수 있으므로 로그에 userId 를 추가하지 않습니다 * refactor: 리뷰 사항 반영 * refactor: RequestContext 빌더 제거 * refactor: RequestContextInterceptor import 수정 * refactor: logback yml 파일에서 timestamp 서버 시간과 동일한 규격으로 수정 * refactor: ApiPerformanceInterceptor 에서 동일 내용 로그 중복으로 찍는 문제 수정 * fix: decode를 두 번 하는 문제 수정 * test: 로깅 관련 filter, interceptor 테스트 추가 * refactor: 코드래빗 리뷰사항 반영 * test: contains 로 비교하던 검증 로직을 isEqualTo 로 수정 * test: preHandle 테스트 에서 result 값을 항상 검증 * refactor: 단위테스트에 TestContainer 어노테이션 제거 * fix: conflict 해결 * fix: docker-compose 충돌 해결 (#610) * chore: release github action 임의 실행 추가 * refactor: 기본 추천 대학 후보 추가 (#161) * fix: config.alloy 경로 수정 * hotfix: 모의지원 현황 어드민 권한 제거 * hotfix: import 제거 * chore: 서브모듈 해시 업데이트 (#611)
1 parent fe70cdb commit f96997a

File tree

48 files changed

+2376
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2376
-61
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ dependencies {
6868
implementation 'org.hibernate.validator:hibernate-validator'
6969
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
7070
implementation 'org.springframework.boot:spring-boot-starter-websocket'
71+
72+
// Database Proxy
73+
implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0'
7174
}
7275

7376
tasks.named('test', Test) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.example.solidconnection.admin.controller;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.web.bind.annotation.PatchMapping;
5+
import org.springframework.web.bind.annotation.PathVariable;
6+
import org.springframework.web.bind.annotation.PostMapping;
7+
import org.springframework.web.bind.annotation.RequestBody;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import com.example.solidconnection.admin.dto.UserBanRequest;
12+
import com.example.solidconnection.admin.service.AdminUserBanService;
13+
import com.example.solidconnection.common.resolver.AuthorizedUser;
14+
15+
import jakarta.validation.Valid;
16+
import lombok.RequiredArgsConstructor;
17+
18+
@RequiredArgsConstructor
19+
@RequestMapping("/admin/users")
20+
@RestController
21+
public class AdminUserBanController {
22+
private final AdminUserBanService adminUserBanService;
23+
24+
@PostMapping("/{user-id}/ban")
25+
public ResponseEntity<Void> banUser(
26+
@AuthorizedUser long adminId,
27+
@PathVariable(name = "user-id") long userId,
28+
@Valid @RequestBody UserBanRequest request
29+
) {
30+
adminUserBanService.banUser(userId, adminId, request);
31+
return ResponseEntity.ok().build();
32+
}
33+
34+
@PatchMapping("/{user-id}/unban")
35+
public ResponseEntity<Void> unbanUser(
36+
@AuthorizedUser long adminId,
37+
@PathVariable(name = "user-id") long userId
38+
) {
39+
adminUserBanService.unbanUser(userId, adminId);
40+
return ResponseEntity.ok().build();
41+
}
42+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.solidconnection.admin.dto;
2+
3+
import com.example.solidconnection.siteuser.domain.UserBanDuration;
4+
5+
import jakarta.validation.constraints.NotNull;
6+
7+
public record UserBanRequest(
8+
@NotNull(message = "차단 기간을 입력해주세요.")
9+
UserBanDuration duration
10+
) {
11+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.example.solidconnection.admin.service;
2+
3+
import static java.time.ZoneOffset.UTC;
4+
5+
import com.example.solidconnection.admin.dto.UserBanRequest;
6+
import com.example.solidconnection.chat.repository.ChatMessageRepository;
7+
import com.example.solidconnection.common.exception.CustomException;
8+
import com.example.solidconnection.common.exception.ErrorCode;
9+
import com.example.solidconnection.community.post.repository.PostRepository;
10+
import com.example.solidconnection.report.repository.ReportRepository;
11+
import com.example.solidconnection.siteuser.domain.SiteUser;
12+
import com.example.solidconnection.siteuser.domain.UserBan;
13+
import com.example.solidconnection.siteuser.domain.UserStatus;
14+
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
15+
import com.example.solidconnection.siteuser.repository.UserBanRepository;
16+
import java.time.ZonedDateTime;
17+
import java.util.List;
18+
import lombok.RequiredArgsConstructor;
19+
import lombok.extern.slf4j.Slf4j;
20+
import org.springframework.scheduling.annotation.Scheduled;
21+
import org.springframework.stereotype.Service;
22+
import org.springframework.transaction.annotation.Transactional;
23+
24+
@Slf4j
25+
@RequiredArgsConstructor
26+
@Service
27+
public class AdminUserBanService {
28+
29+
private final UserBanRepository userBanRepository;
30+
private final ReportRepository reportRepository;
31+
private final SiteUserRepository siteUserRepository;
32+
private final PostRepository postRepository;
33+
private final ChatMessageRepository chatMessageRepository;
34+
35+
@Transactional
36+
public void banUser(long userId, long adminId, UserBanRequest request) {
37+
SiteUser user = siteUserRepository.findById(userId)
38+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
39+
validateNotAlreadyBanned(userId);
40+
validateReportExists(userId);
41+
42+
user.updateUserStatus(UserStatus.BANNED);
43+
updateReportedContentIsDeleted(userId, true);
44+
createUserBan(userId, adminId, request);
45+
}
46+
47+
private void validateNotAlreadyBanned(long userId) {
48+
if (userBanRepository.existsByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(userId, ZonedDateTime.now(UTC))) {
49+
throw new CustomException(ErrorCode.ALREADY_BANNED_USER);
50+
}
51+
}
52+
53+
private void validateReportExists(long userId) {
54+
if (!reportRepository.existsByReportedId(userId)) {
55+
throw new CustomException(ErrorCode.REPORT_NOT_FOUND);
56+
}
57+
}
58+
59+
private void updateReportedContentIsDeleted(long userId, boolean isDeleted) {
60+
postRepository.updateReportedPostsIsDeleted(userId, isDeleted);
61+
chatMessageRepository.updateReportedChatMessagesIsDeleted(userId, isDeleted);
62+
}
63+
64+
private void createUserBan(long userId, long adminId, UserBanRequest request) {
65+
ZonedDateTime now = ZonedDateTime.now(UTC);
66+
ZonedDateTime expiredAt = now.plusDays(request.duration().getDays());
67+
UserBan userBan = new UserBan(userId, adminId, request.duration(), expiredAt);
68+
userBanRepository.save(userBan);
69+
}
70+
71+
@Transactional
72+
public void unbanUser(long userId, long adminId) {
73+
SiteUser user = siteUserRepository.findById(userId)
74+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
75+
UserBan userBan = findActiveBan(userId);
76+
userBan.manuallyUnban(adminId);
77+
78+
user.updateUserStatus(UserStatus.REPORTED);
79+
updateReportedContentIsDeleted(userId, false);
80+
}
81+
82+
private UserBan findActiveBan(long userId) {
83+
return userBanRepository
84+
.findByBannedUserIdAndIsExpiredFalseAndExpiredAtAfter(userId, ZonedDateTime.now(UTC))
85+
.orElseThrow(() -> new CustomException(ErrorCode.NOT_BANNED_USER));
86+
}
87+
88+
@Transactional
89+
@Scheduled(cron = "0 0 0 * * *")
90+
public void expireUserBans() {
91+
try {
92+
ZonedDateTime now = ZonedDateTime.now(UTC);
93+
List<Long> expiredUserIds = userBanRepository.findExpiredBannedUserIds(now);
94+
95+
if (expiredUserIds.isEmpty()) {
96+
return;
97+
}
98+
99+
userBanRepository.bulkExpireUserBans(now);
100+
siteUserRepository.bulkUpdateUserStatus(expiredUserIds, UserStatus.REPORTED);
101+
bulkUpdateReportedContentIsDeleted(expiredUserIds);
102+
log.info("Finished processing expired blocks:: userIds={}", expiredUserIds);
103+
} catch (Exception e) {
104+
log.error("Failed to process expired blocks", e);
105+
}
106+
}
107+
108+
private void bulkUpdateReportedContentIsDeleted(List<Long> expiredUserIds) {
109+
postRepository.bulkUpdateReportedPostsIsDeleted(expiredUserIds, false);
110+
chatMessageRepository.bulkUpdateReportedChatMessagesIsDeleted(expiredUserIds, false);
111+
}
112+
113+
}
Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package com.example.solidconnection.auth.dto;
22

3-
import com.example.solidconnection.siteuser.domain.AuthType;
43
import com.example.solidconnection.siteuser.domain.ExchangeStatus;
5-
import com.example.solidconnection.siteuser.domain.Role;
6-
import com.example.solidconnection.siteuser.domain.SiteUser;
74
import com.fasterxml.jackson.annotation.JsonProperty;
85
import jakarta.validation.constraints.NotBlank;
96
import java.util.List;
@@ -20,27 +17,4 @@ public record SignUpRequest(
2017

2118
@NotBlank(message = "닉네임을 입력해주세요.")
2219
String nickname) {
23-
24-
public SiteUser toOAuthSiteUser(String email, AuthType authType) {
25-
return new SiteUser(
26-
email,
27-
this.nickname,
28-
this.profileImageUrl,
29-
this.exchangeStatus,
30-
Role.MENTEE,
31-
authType
32-
);
33-
}
34-
35-
public SiteUser toEmailSiteUser(String email, String encodedPassword) {
36-
return new SiteUser(
37-
email,
38-
this.nickname,
39-
this.profileImageUrl,
40-
this.exchangeStatus,
41-
Role.MENTEE,
42-
AuthType.EMAIL,
43-
encodedPassword
44-
);
45-
}
4620
}

src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.example.solidconnection.siteuser.domain.AuthType;
1414
import com.example.solidconnection.siteuser.domain.Role;
1515
import com.example.solidconnection.siteuser.domain.SiteUser;
16+
import com.example.solidconnection.siteuser.domain.UserStatus;
1617
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
1718
import lombok.RequiredArgsConstructor;
1819
import org.springframework.stereotype.Service;
@@ -56,7 +57,8 @@ public SignInResponse signUp(SignUpRequest signUpRequest) {
5657
signUpRequest.exchangeStatus(),
5758
Role.MENTEE,
5859
authType,
59-
password
60+
password,
61+
UserStatus.ACTIVE
6062
));
6163

6264
// 관심 지역, 국가 저장

src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
import lombok.AccessLevel;
1616
import lombok.Getter;
1717
import lombok.NoArgsConstructor;
18+
import org.hibernate.annotations.Where;
1819

1920
@Entity
2021
@Getter
2122
@NoArgsConstructor(access = AccessLevel.PROTECTED)
23+
@Where(clause = "is_deleted = false")
2224
public class ChatMessage extends BaseEntity {
2325

2426
@Id
@@ -33,6 +35,9 @@ public class ChatMessage extends BaseEntity {
3335
@ManyToOne(fetch = FetchType.LAZY)
3436
private ChatRoom chatRoom;
3537

38+
@Column(name = "is_deleted", columnDefinition = "boolean default false", nullable = false)
39+
private boolean isDeleted = false;
40+
3641
@OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL, orphanRemoval = true)
3742
private final List<ChatAttachment> chatAttachments = new ArrayList<>();
3843

src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.springframework.data.domain.Pageable;
77
import org.springframework.data.domain.Slice;
88
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Modifying;
910
import org.springframework.data.jpa.repository.Query;
1011
import org.springframework.data.repository.query.Param;
1112

@@ -48,4 +49,20 @@ SELECT MAX(cm2.id)
4849
GROUP BY cm.chatRoom.id
4950
""")
5051
List<UnreadCountDto> countUnreadMessagesBatch(@Param("chatRoomIds") List<Long> chatRoomIds, @Param("userId") long userId);
52+
53+
@Modifying(clearAutomatically = true, flushAutomatically = true)
54+
@Query(value = """
55+
UPDATE chat_message cm SET cm.is_deleted = :isDeleted
56+
WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
57+
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id = :siteUserId)
58+
""", nativeQuery = true)
59+
void updateReportedChatMessagesIsDeleted(@Param("siteUserId") long siteUserId, @Param("isDeleted") boolean isDeleted);
60+
61+
@Modifying(clearAutomatically = true, flushAutomatically = true)
62+
@Query(value = """
63+
UPDATE chat_message cm SET cm.is_deleted = :isDeleted
64+
WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
65+
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id IN :siteUserIds)
66+
""", nativeQuery = true)
67+
void bulkUpdateReportedChatMessagesIsDeleted(@Param("siteUserIds") List<Long> siteUserIds, @Param("isDeleted") boolean isDeleted);
5168
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.example.solidconnection.common.config.datasource;
2+
3+
import com.example.solidconnection.common.listener.QueryMetricsListener;
4+
import javax.sql.DataSource;
5+
import lombok.RequiredArgsConstructor;
6+
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
7+
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.context.annotation.Primary;
11+
12+
@RequiredArgsConstructor
13+
@Configuration
14+
public class DataSourceProxyConfig {
15+
16+
private final QueryMetricsListener queryMetricsListener;
17+
18+
@Bean
19+
@Primary
20+
public DataSource proxyDataSource(DataSourceProperties props) {
21+
DataSource dataSource = props.initializeDataSourceBuilder().build();
22+
23+
return ProxyDataSourceBuilder
24+
.create(dataSource)
25+
.listener(queryMetricsListener)
26+
.name("main")
27+
.build();
28+
}
29+
}

src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package com.example.solidconnection.common.config.web;
22

3+
import com.example.solidconnection.common.interceptor.BannedUserInterceptor;
4+
import com.example.solidconnection.common.filter.HttpLoggingFilter;
5+
import com.example.solidconnection.common.interceptor.ApiPerformanceInterceptor;
6+
import com.example.solidconnection.common.interceptor.RequestContextInterceptor;
37
import com.example.solidconnection.common.resolver.AuthorizedUserResolver;
48
import com.example.solidconnection.common.resolver.CustomPageableHandlerMethodArgumentResolver;
59
import java.util.List;
610
import lombok.RequiredArgsConstructor;
11+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
12+
import org.springframework.context.annotation.Bean;
713
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.core.Ordered;
815
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
16+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
917
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
1018

1119
@Configuration
@@ -14,6 +22,10 @@ public class WebMvcConfig implements WebMvcConfigurer {
1422

1523
private final AuthorizedUserResolver authorizedUserResolver;
1624
private final CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver;
25+
private final BannedUserInterceptor bannedUserInterceptor;
26+
private final HttpLoggingFilter httpLoggingFilter;
27+
private final ApiPerformanceInterceptor apiPerformanceInterceptor;
28+
private final RequestContextInterceptor requestContextInterceptor;
1729

1830
@Override
1931
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
@@ -22,4 +34,26 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
2234
customPageableHandlerMethodArgumentResolver
2335
));
2436
}
37+
38+
@Override
39+
public void addInterceptors(InterceptorRegistry registry){
40+
registry.addInterceptor(apiPerformanceInterceptor)
41+
.addPathPatterns("/**")
42+
.excludePathPatterns("/actuator/**");
43+
44+
registry.addInterceptor(requestContextInterceptor)
45+
.addPathPatterns("/**")
46+
.excludePathPatterns("/actuator/**");
47+
48+
registry.addInterceptor(bannedUserInterceptor)
49+
.addPathPatterns("/posts/**", "/comments/**", "/chats/**", "/boards/**");
50+
}
51+
52+
@Bean
53+
public FilterRegistrationBean<HttpLoggingFilter> customHttpLoggingFilter() {
54+
FilterRegistrationBean<HttpLoggingFilter> filterBean = new FilterRegistrationBean<>();
55+
filterBean.setFilter(httpLoggingFilter);
56+
filterBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
57+
return filterBean;
58+
}
2559
}

0 commit comments

Comments
 (0)