diff --git a/.claude/hooks/notify.py b/.claude/hooks/notify.py index d839ea66f..6a2828d41 100644 --- a/.claude/hooks/notify.py +++ b/.claude/hooks/notify.py @@ -13,7 +13,17 @@ ]) elif system == "Windows": ps1_path = os.path.join(script_dir, "notify.ps1") - subprocess.run([ - "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", - "-File", ps1_path - ]) + # VS Code extension 환경에서는 PATH에 powershell이 없을 수 있으므로 절대 경로 사용 + powershell_candidates = [ + r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe", + "powershell", + ] + for ps in powershell_candidates: + try: + subprocess.run( + [ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1_path], + timeout=10, + ) + break + except (FileNotFoundError, subprocess.TimeoutExpired): + continue diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md new file mode 100644 index 000000000..695db1278 --- /dev/null +++ b/.claude/skills/review-pr/SKILL.md @@ -0,0 +1,281 @@ +--- +name: review-pr +description: Pull Request를 체계적으로 리뷰하여 프로젝트 컨벤션 준수 여부와 코드 품질을 검증합니다 +args: (예: /review-pr 666) +--- + +# Pull Request 리뷰 가이드 + +이 skill은 solid-connect-server 프로젝트의 Pull Request를 체계적으로 리뷰합니다. + +## 사용법 + +```bash +/review-pr +``` + +**예제:** +```bash +/review-pr 666 +``` + +--- + +## 리뷰 프로세스 + +### 1단계: PR 정보 수집 + +GitHub CLI로 PR의 기본 정보와 변경사항을 파악합니다. + +```bash +gh pr view <번호> -R solid-connection/solid-connect-server # PR 기본 정보 조회 +gh pr diff <번호> -R solid-connection/solid-connect-server # 변경된 파일과 diff 확인 +gh pr checks <번호> -R solid-connection/solid-connect-server # CI/CD 상태 확인 +``` + +**수집할 정보:** +- PR 제목 및 설명 +- 관련 이슈 번호 +- 변경된 파일 목록 +- CI/CD 체크 상태 + +### 2단계: 변경 파일 분석 + +**도구 우선순위:** + +1. **Serena MCP** (Java 코드 분석에 최적화) + - `mcp__serena__get_symbols_overview <파일경로>` - 파일의 클래스/메서드 구조 파악 + - `mcp__serena__find_symbol <심볼명>` - 특정 심볼 검색 + - `mcp__serena__search_for_pattern <패턴>` - 컨벤션 위반 패턴 검색 + +2. **Read/Grep** (보조 분석) + - `Read <파일경로>` - 파일 전체 읽기 + - `Grep --pattern <패턴>` - 패턴 검색 + +### 3단계: 체크리스트 검증 + +아래 체크리스트를 순서대로 확인합니다. + +--- + +## 리뷰 체크리스트 + +각 항목의 상세 컨벤션은 참조 문서를 확인하세요. + +### 1. 아키텍처 및 계층 구조 + +**검증 항목:** +- 계층형 아키텍처 준수 (Controller → Service → Repository) +- 역계층 참조 금지 +- 순환 의존성 없음 + +👉 **참고:** `CLAUDE.md` - "아키텍처" 섹션 + +--- + +### 2. 네이밍 컨벤션 + +**검증 항목:** +- API 엔드포인트: kebab-case 사용 (예: `/user-profile`) +- DTO 변환 메서드: 단일 파라미터 `from()`, 다중 파라미터 `of()` +- Request/Response: `XXXRequest`, `XXXResponse` 형식 +- 테스트 메서드: 한국어, `어떤_것을_하면_어떤_결과가_나온다()` 패턴 + +👉 **참고:** `CLAUDE.md` - "네이밍 컨벤션" 섹션 + +--- + +### 3. 코드 스타일 + +**검증 항목:** +- 와일드카드(`*`) import 금지 +- 클래스 선언 전 빈 줄 존재 +- private 메서드는 호출하는 public 메서드 바로 아래 위치 +- Controller: 모든 파라미터 줄바꿈 필수 +- 일반 메서드: 3개 이상 파라미터 시 줄바꿈 +- 파일 끝 개행 문자 + +**패턴 검색 예제:** +```bash +mcp__serena__search_for_pattern "import.*\\*" # 와일드카드 import 검색 +``` + +👉 **참고:** `CLAUDE.md` - "코드 스타일" 섹션 + +--- + +### 4. Entity 및 JPA + +**검증 항목:** +- 모든 필드에 `@Column` 어노테이션 존재 +- `name` 속성으로 컬럼명 명시 +- `nullable` 속성 명시 +- null 불가: 원시 타입 (`int`, `long`, `boolean`) +- nullable: Wrapper 타입 (`Integer`, `Long`, `Boolean`) +- 양방향 연관관계 시 편의 메서드 존재 + +👉 **참고:** `CLAUDE.md` - "데이터베이스 접근" 섹션 + +--- + +### 5. 데이터베이스 마이그레이션 + +**검증 항목:** +- 스키마 변경 시 Flyway 마이그레이션 파일 추가 +- 파일명 형식: `V{VERSION}__{DESCRIPTION}.sql` +- 위치: `src/main/resources/db/migration/` +- Entity 변경과 마이그레이션 일치 +- 기존 마이그레이션 파일 수정 금지 (새 버전 생성) + +👉 **참고:** `CLAUDE.md` - "데이터베이스 마이그레이션" 섹션 + +--- + +### 6. 테스트 코드 + +**검증 항목:** +- 새로운 Service/Repository 메서드에 대한 테스트 존재 +- 예외 케이스 테스트 포함 +- `@TestContainerSpringBootTest` 어노테이션 사용 +- `@DisplayName`으로 한글 설명 제공 +- `@Nested`로 기능별 그룹화 +- Given-When-Then 구조 준수 +- Fixture 패턴 사용 (FixtureBuilder + Fixture) + +👉 **참고:** `.claude/skills/test/SKILL.md` + +--- + +### 7. 커밋 메시지 + +**검증 항목:** +- `: ` 형식 +- Type: `feat`, `fix`, `refactor`, `test`, `chore`, `docs`, `perf` +- 간결하고 명확한 설명 + +👉 **참고:** `CLAUDE.md` - "Git 커밋 컨벤션" 섹션 + +--- + +### 8. 코드 품질 및 잠재적 이슈 + +**검증 항목:** +- 비즈니스 로직은 Service 계층에만 +- Controller는 요청/응답 처리만 +- `@Transactional` 적절하게 사용 (읽기 전용: `readOnly = true`) +- CustomException 사용 +- N+1 쿼리 문제 없음 +- 인증/인가 처리 (`@AuthorizedUser`) +- 민감 정보 노출 없음 + +👉 **참고:** `CLAUDE.md` - "아키텍처", "기술 스택 상세" 섹션 + +--- + +## 도구 사용 가이드 + +### Serena MCP (우선 사용) + +```bash +# 파일의 클래스/메서드 구조 파악 +mcp__serena__get_symbols_overview src/main/java/.../MentorService.java + +# 특정 심볼 검색 +mcp__serena__find_symbol "MentorDetailResponse" + +# 컨벤션 위반 패턴 검색 +mcp__serena__search_for_pattern "import.*\\*" +``` + +### GitHub CLI + +```bash +# PR 정보 +gh pr view 666 -R solid-connection/solid-connect-server --json title,body,author,number,url + +# 변경사항 +gh pr diff 666 -R solid-connection/solid-connect-server --patch + +# CI 상태 +gh pr checks 666 -R solid-connection/solid-connect-server +``` + +### 보조 도구 + +```bash +# 파일 읽기 +Read src/main/java/.../MentorService.java + +# 패턴 검색 +Grep --pattern "@Column" --glob "*.java" --path src/main/java/.../domain +``` + +--- + +## 리뷰 결과 출력 형식 + +다음 형식으로 리뷰 결과를 정리하여 제공합니다. + +```markdown +## PR 리뷰 결과: #{번호} - {제목} + +**PR 링크:** {GitHub URL} +**관련 이슈:** #{이슈번호} + +### 📊 PR 정보 요약 + +- **작성자:** {작성자} +- **변경 파일:** {숫자}개 +- **추가 라인:** +{숫자}, **삭제 라인:** -{숫자} +- **CI/CD 상태:** {통과/실패} + +### 주요 변경사항 + +{PR 설명 요약} + +--- + +### ✅ 통과 항목 + +- 아키텍처 계층 구조 준수 +- 네이밍 컨벤션 준수 +- ... + +### ⚠️ 개선 권장 항목 + +- **코드 스타일**: 와일드카드 import 사용 + - 파일: `src/main/java/.../MentorService.java:5` + - 개선: 명시적 import로 변경 + +### ❌ 필수 수정 항목 + +- **Entity**: @Column 어노테이션 누락 + - 파일: `src/main/java/.../domain/Mentor.java:30` + - 수정 방향: 모든 필드에 `@Column` 어노테이션 추가 + +--- + +### 💡 종합 의견 + +{전반적인 리뷰 의견} + +**승인 상태:** ✅ 승인 / ⚠️ 조건부 승인 / ❌ 수정 후 재검토 +``` + +--- + +## 리뷰 시 주의사항 + +1. **컨텍스트 이해 우선**: PR 설명과 관련 이슈를 먼저 읽고 변경의 목적 파악 +2. **Serena MCP 우선 사용**: Java 코드 분석 시 효율적 +3. **건설적 피드백**: 문제점 지적 시 구체적인 개선 방향 제시 +4. **긍정적 피드백**: 잘된 부분도 언급하여 균형 잡힌 리뷰 +5. **우선순위**: 아키텍처 > 네이밍 > 스타일 순으로 중요도 판단 + +--- + +## 참고 자료 + +- **프로젝트 컨벤션**: `CLAUDE.md` - 전체 개발 컨벤션 +- **테스트 가이드**: `.claude/skills/test/SKILL.md` - 테스트 작성 가이드 +- **개발 컨벤션 위키**: https://github.com/solid-connection/solid-connect-server/wiki/개발-컨벤션-정리 diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 92ffe2ad9..294d6b57a 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -19,9 +19,6 @@ jobs: steps: - name: Checkout the code uses: actions/checkout@v4 - with: - token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - submodules: true # --- Java, Gradle 설정 --- - name: Set up JDK 17 diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index c63019b55..0d6bc082d 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -25,9 +25,6 @@ jobs: steps: - name: Checkout the code uses: actions/checkout@v4 - with: - token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} - submodules: true # --- Java, Gradle 설정 --- - name: Set up JDK 17 diff --git a/.gitignore b/.gitignore index 7a58382b4..f6e780891 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ out/ ### Claude Code ### .claude/settings.local.json +### Serena ### +.serena/ + ### YML ### application-secret.yml application-prod.yml diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index a7033a673..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/main/resources/secret"] - path = src/main/resources/secret - url = https://github.com/solid-connection/solid-connect-secret diff --git a/build.gradle b/build.gradle index cea20dd7a..4694747d9 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,7 @@ dependencies { // Etc implementation platform('software.amazon.awssdk:bom:2.41.4') implementation 'software.amazon.awssdk:s3' + implementation 'io.awspring.cloud:spring-cloud-aws-starter-parameter-store:3.0.4' implementation 'org.hibernate.validator:hibernate-validator' implementation 'org.springframework.boot:spring-boot-starter-websocket' diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 02554521f..a7a909d1a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,8 +7,21 @@ services: network_mode: "host" environment: - SPRING_PROFILES_ACTIVE=dev + - AWS_REGION=ap-northeast-2 - SPRING_DATA_REDIS_HOST=127.0.0.1 - SPRING_DATA_REDIS_PORT=6379 volumes: - ./logs:/var/log/spring restart: always + + mysql: + image: mysql:8.0 + container_name: solid-connection-dev-mysql + environment: + MYSQL_DATABASE: solid_connection + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + ports: + - "3306:3306" + volumes: + - ./mysql_data_dev:/var/lib/mysql + restart: always diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 77afba397..99ea7db7b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -7,6 +7,7 @@ services: network_mode: "host" environment: - SPRING_PROFILES_ACTIVE=prod + - AWS_REGION=ap-northeast-2 - SPRING_DATA_REDIS_HOST=127.0.0.1 - SPRING_DATA_REDIS_PORT=6379 volumes: diff --git a/src/main/java/com/example/solidconnection/admin/controller/AdminUserController.java b/src/main/java/com/example/solidconnection/admin/controller/AdminUserController.java new file mode 100644 index 000000000..841ef901e --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/controller/AdminUserController.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.admin.controller; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse; +import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition; +import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse; +import com.example.solidconnection.admin.dto.UserInfoDetailResponse; +import com.example.solidconnection.admin.dto.UserSearchCondition; +import com.example.solidconnection.admin.dto.UserSearchResponse; +import com.example.solidconnection.admin.service.AdminUserService; +import com.example.solidconnection.common.response.PageResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RequestMapping("/admin/users") +@RestController +public class AdminUserController { + + private final AdminUserService adminUserService; + + @GetMapping + public ResponseEntity> searchAllUsers( + @Valid @ModelAttribute UserSearchCondition searchCondition, + Pageable pageable + ) { + Page page = adminUserService.searchAllUsers(searchCondition, pageable); + return ResponseEntity.ok(PageResponse.of(page)); + } + + @GetMapping("/{user-id}") + public ResponseEntity getUserInfoDetail( + @PathVariable(name = "user-id") long userId + ) { + UserInfoDetailResponse response = adminUserService.getUserInfoDetail(userId); + return ResponseEntity.ok(response); + } + + @GetMapping("/restricted") + public ResponseEntity> searchRestrictedUsers( + @Valid @ModelAttribute RestrictedUserSearchCondition searchCondition, + Pageable pageable + ) { + Page page = adminUserService.searchRestrictedUsers(searchCondition, pageable); + return ResponseEntity.ok(PageResponse.of(page)); + } + + @GetMapping("/restricted/{user-id}") + public ResponseEntity getRestrictedUserInfoDetail( + @PathVariable(name = "user-id") long userId + ) { + RestrictedUserInfoDetailResponse response = adminUserService.getRestrictedUserInfoDetail(userId); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/BannedHistoryResponse.java b/src/main/java/com/example/solidconnection/admin/dto/BannedHistoryResponse.java new file mode 100644 index 000000000..de3ab7a53 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/BannedHistoryResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.admin.dto; + +import java.time.ZonedDateTime; + +public record BannedHistoryResponse( + ZonedDateTime createdAt +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/BannedInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/BannedInfoResponse.java new file mode 100644 index 000000000..0aa257f42 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/BannedInfoResponse.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.siteuser.domain.UserBanDuration; +import com.fasterxml.jackson.annotation.JsonProperty; + +public record BannedInfoResponse( + @JsonProperty("isBanned") boolean isBanned, + UserBanDuration duration +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/MatchedInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/MatchedInfoResponse.java new file mode 100644 index 000000000..d3b5c6f68 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/MatchedInfoResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.admin.dto; + +import java.time.ZonedDateTime; + +public record MatchedInfoResponse( + String nickname, + ZonedDateTime matchedDate +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/MenteeInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/MenteeInfoResponse.java new file mode 100644 index 000000000..f4e6cffff --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/MenteeInfoResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.admin.dto; + +import java.util.List; + +public record MenteeInfoResponse( + UnivApplyInfoResponse univApplyInfo, + List mentorInfos +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/MentorApplicationHistoryInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/MentorApplicationHistoryInfoResponse.java new file mode 100644 index 000000000..f026299f0 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/MentorApplicationHistoryInfoResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.mentor.domain.MentorApplicationStatus; +import java.time.ZonedDateTime; + +public record MentorApplicationHistoryInfoResponse( + MentorApplicationStatus mentorApplicationStatus, + String rejectedReason, + ZonedDateTime createdAt +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/MentorInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/MentorInfoResponse.java new file mode 100644 index 000000000..bd3dad8d5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/MentorInfoResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.admin.dto; + +import java.util.List; + +public record MentorInfoResponse( + List menteeInfos, + List mentorApplicationHistory +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/ReportedHistoryResponse.java b/src/main/java/com/example/solidconnection/admin/dto/ReportedHistoryResponse.java new file mode 100644 index 000000000..d7fbaa94c --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/ReportedHistoryResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.admin.dto; + +import java.time.ZonedDateTime; + +import com.example.solidconnection.report.domain.ReportType; + +public record ReportedHistoryResponse( + ZonedDateTime reportedDate, + ReportType reportType +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/ReportedInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/ReportedInfoResponse.java new file mode 100644 index 000000000..95d7f20a3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/ReportedInfoResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.admin.dto; + +import java.time.ZonedDateTime; + +import com.example.solidconnection.report.domain.ReportType; +import com.example.solidconnection.report.domain.TargetType; + +public record ReportedInfoResponse( + ZonedDateTime reportedDate, + TargetType targetType, + ReportType reportType +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserInfoDetailResponse.java b/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserInfoDetailResponse.java new file mode 100644 index 000000000..1c9cc40bb --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserInfoDetailResponse.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.admin.dto; + +import java.util.List; + +public record RestrictedUserInfoDetailResponse( + List reportedHistoryResponses, + List bannedHistoryResponses +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchCondition.java b/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchCondition.java new file mode 100644 index 000000000..2f9db3309 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchCondition.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.UserStatus; + +public record RestrictedUserSearchCondition( + Role role, + UserStatus userStatus, + String keyword +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchResponse.java b/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchResponse.java new file mode 100644 index 000000000..cc7227916 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/RestrictedUserSearchResponse.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.UserStatus; + +public record RestrictedUserSearchResponse( + Long id, + String nickname, + Role role, + UserStatus userStatus, + ReportedInfoResponse reportedInfoResponse, + BannedInfoResponse bannedInfoResponse +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/UnivApplyInfoResponse.java b/src/main/java/com/example/solidconnection/admin/dto/UnivApplyInfoResponse.java new file mode 100644 index 000000000..225991d64 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/UnivApplyInfoResponse.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.admin.dto; + +public record UnivApplyInfoResponse( + String firstChoiceUnivName, + String secondChoiceUnivName, + String thirdChoiceUnivName +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/UserInfoDetailResponse.java b/src/main/java/com/example/solidconnection/admin/dto/UserInfoDetailResponse.java new file mode 100644 index 000000000..9c2dd637d --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/UserInfoDetailResponse.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.admin.dto; + +import java.util.List; + +public record UserInfoDetailResponse( + MentorInfoResponse mentorInfoResponse, // 멘티일 경우 null + MenteeInfoResponse menteeInfoResponse, // 멘토일 경우 null + List reportedHistoryResponses +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/UserSearchCondition.java b/src/main/java/com/example/solidconnection/admin/dto/UserSearchCondition.java new file mode 100644 index 000000000..9d6bc1bd8 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/UserSearchCondition.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.siteuser.domain.Role; + +public record UserSearchCondition( + Role role, + String keyword +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/dto/UserSearchResponse.java b/src/main/java/com/example/solidconnection/admin/dto/UserSearchResponse.java new file mode 100644 index 000000000..a1150db90 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/dto/UserSearchResponse.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.admin.dto; + +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.UserStatus; + +public record UserSearchResponse( + Long id, + String nickname, + String email, + Role role, + UserStatus userStatus +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java b/src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java new file mode 100644 index 000000000..6b4a39e21 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java @@ -0,0 +1,59 @@ +package com.example.solidconnection.admin.location.country.controller; + +import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest; +import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse; +import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest; +import com.example.solidconnection.admin.location.country.service.AdminCountryService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/admin/countries") +@RestController +public class AdminCountryController { + + private final AdminCountryService adminCountryService; + + @GetMapping + public ResponseEntity> getAllCountries() { + List responses = adminCountryService.getAllCountries(); + return ResponseEntity.ok(responses); + } + + @PostMapping + public ResponseEntity createCountry( + @Valid @RequestBody AdminCountryCreateRequest request + ) { + AdminCountryResponse response = adminCountryService.createCountry(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @PutMapping("/{code}") + public ResponseEntity updateCountry( + @PathVariable String code, + @Valid @RequestBody AdminCountryUpdateRequest request + ) { + AdminCountryResponse response = adminCountryService.updateCountry(code, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{code}") + public ResponseEntity deleteCountry( + @PathVariable String code + ) { + adminCountryService.deleteCountry(code); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java new file mode 100644 index 000000000..0c5fe8b40 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.admin.location.country.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminCountryCreateRequest( + @NotBlank(message = "국가 코드는 필수입니다") + @Size(min = 2, max = 2, message = "국가 코드는 2자여야 합니다") + String code, + + @NotBlank(message = "한글 국가명은 필수입니다") + @Size(min = 1, max = 100, message = "한글 국가명은 1자 이상 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "지역 코드는 필수입니다") + @Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java new file mode 100644 index 000000000..19bc69eac --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.admin.location.country.dto; + +import com.example.solidconnection.location.country.domain.Country; + +public record AdminCountryResponse( + String code, + String koreanName, + String regionCode +) { + + public static AdminCountryResponse from(Country country) { + return new AdminCountryResponse( + country.getCode(), + country.getKoreanName(), + country.getRegionCode() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java new file mode 100644 index 000000000..a9c3c7a43 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.admin.location.country.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminCountryUpdateRequest( + @NotBlank(message = "한글 국가명은 필수입니다") + @Size(min = 1, max = 100, message = "한글 국가명은 1자 이상 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "지역 코드는 필수입니다") + @Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java b/src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java new file mode 100644 index 000000000..279fcfc97 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.admin.location.country.service; + +import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest; +import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse; +import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.location.country.domain.Country; +import com.example.solidconnection.location.country.repository.CountryRepository; +import com.example.solidconnection.location.region.repository.RegionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AdminCountryService { + + private final CountryRepository countryRepository; + private final RegionRepository regionRepository; + + @Transactional(readOnly = true) + public List getAllCountries() { + return countryRepository.findAll() + .stream() + .map(AdminCountryResponse::from) + .toList(); + } + + @Transactional + public AdminCountryResponse createCountry(AdminCountryCreateRequest request) { + validateCodeNotExists(request.code()); + validateKoreanNameNotExists(request.koreanName()); + validateRegionCodeExists(request.regionCode()); + + Country country = new Country(request.code(), request.koreanName(), request.regionCode()); + Country savedCountry = countryRepository.save(country); + + return AdminCountryResponse.from(savedCountry); + } + + private void validateCodeNotExists(String code) { + countryRepository.findByCode(code) + .ifPresent(country -> { + throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS); + }); + } + + private void validateKoreanNameNotExists(String koreanName) { + countryRepository.findAllByKoreanNameIn(List.of(koreanName)) + .stream() + .findFirst() + .ifPresent(country -> { + throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS); + }); + } + + private void validateRegionCodeExists(String regionCode) { + if (regionCode != null) { + regionRepository.findById(regionCode) + .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND)); + } + } + + @Transactional + public AdminCountryResponse updateCountry(String code, AdminCountryUpdateRequest request) { + Country country = countryRepository.findByCode(code) + .orElseThrow(() -> new CustomException(ErrorCode.COUNTRY_NOT_FOUND)); + + validateKoreanNameNotDuplicated(request.koreanName(), code); + validateRegionCodeExists(request.regionCode()); + + country.updateKoreanName(request.koreanName()); + country.updateRegionCode(request.regionCode()); + + return AdminCountryResponse.from(country); + } + + private void validateKoreanNameNotDuplicated(String koreanName, String excludeCode) { + countryRepository.findAllByKoreanNameIn(List.of(koreanName)) + .stream() + .findFirst() + .ifPresent(existingCountry -> { + if (!existingCountry.getCode().equals(excludeCode)) { + throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS); + } + }); + } + + @Transactional + public void deleteCountry(String code) { + Country country = countryRepository.findByCode(code) + .orElseThrow(() -> new CustomException(ErrorCode.COUNTRY_NOT_FOUND)); + + countryRepository.delete(country); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminUserService.java b/src/main/java/com/example/solidconnection/admin/service/AdminUserService.java new file mode 100644 index 000000000..b2051b46d --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/service/AdminUserService.java @@ -0,0 +1,50 @@ +package com.example.solidconnection.admin.service; + +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse; +import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition; +import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse; +import com.example.solidconnection.admin.dto.UserInfoDetailResponse; +import com.example.solidconnection.admin.dto.UserSearchCondition; +import com.example.solidconnection.admin.dto.UserSearchResponse; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.siteuser.repository.custom.SiteUserFilterRepository; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class AdminUserService { + + private final SiteUserRepository siteUserRepository; + + @Transactional(readOnly = true) + public Page searchAllUsers(UserSearchCondition searchCondition, Pageable pageable) { + return siteUserRepository.searchAllUsers(searchCondition, pageable); + } + + @Transactional(readOnly = true) + public Page searchRestrictedUsers(RestrictedUserSearchCondition searchCondition, Pageable pageable) { + return siteUserRepository.searchRestrictedUsers(searchCondition, pageable); + } + + @Transactional(readOnly = true) + public UserInfoDetailResponse getUserInfoDetail(long userId) { + SiteUser siteUser = siteUserRepository.findById(userId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + return siteUserRepository.getUserInfoDetailByUserId(siteUser); + } + + @Transactional(readOnly = true) + public RestrictedUserInfoDetailResponse getRestrictedUserInfoDetail(long userId) { + SiteUser siteUser = siteUserRepository.findById(userId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + return siteUserRepository.getRestrictedUserInfoDetail(siteUser.getId()); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java index c03e9f526..416ba8fb8 100644 --- a/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java +++ b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java @@ -11,6 +11,8 @@ import com.example.solidconnection.admin.university.dto.AdminHostUniversityResponse; import com.example.solidconnection.admin.university.dto.AdminHostUniversitySearchCondition; import com.example.solidconnection.admin.university.dto.AdminHostUniversityUpdateRequest; +import com.example.solidconnection.cache.annotation.DefaultCacheOut; +import com.example.solidconnection.cache.manager.CustomCacheManager; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.location.country.domain.Country; import com.example.solidconnection.location.country.repository.CountryRepository; @@ -19,6 +21,7 @@ import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.repository.HostUniversityRepository; import com.example.solidconnection.university.repository.UnivApplyInfoRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -33,6 +36,7 @@ public class AdminHostUniversityService { private final CountryRepository countryRepository; private final RegionRepository regionRepository; private final UnivApplyInfoRepository univApplyInfoRepository; + private final CustomCacheManager cacheManager; @Transactional(readOnly = true) public Page getHostUniversities( @@ -56,6 +60,11 @@ public AdminHostUniversityDetailResponse getHostUniversity(Long id) { } @Transactional + @DefaultCacheOut( + key = {"univApplyInfoTextSearch", "university:recommend:general"}, + cacheManager = "customCacheManager", + prefix = true + ) public AdminHostUniversityDetailResponse createHostUniversity(AdminHostUniversityCreateRequest request) { validateKoreanNameNotExists(request.koreanName()); @@ -89,6 +98,11 @@ private void validateKoreanNameNotExists(String koreanName) { } @Transactional + @DefaultCacheOut( + key = {"univApplyInfoTextSearch", "university:recommend:general"}, + cacheManager = "customCacheManager", + prefix = true + ) public AdminHostUniversityDetailResponse updateHostUniversity(Long id, AdminHostUniversityUpdateRequest request) { HostUniversity hostUniversity = hostUniversityRepository.findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); @@ -112,6 +126,8 @@ public AdminHostUniversityDetailResponse updateHostUniversity(Long id, AdminHost region ); + evictUnivApplyInfoDetailCaches(id); + return AdminHostUniversityDetailResponse.from(hostUniversity); } @@ -135,6 +151,11 @@ private Region findRegionByCode(String regionCode) { } @Transactional + @DefaultCacheOut( + key = {"univApplyInfoTextSearch", "university:recommend:general"}, + cacheManager = "customCacheManager", + prefix = true + ) public void deleteHostUniversity(Long id) { HostUniversity hostUniversity = hostUniversityRepository.findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); @@ -149,4 +170,14 @@ private void validateNoReferences(Long hostUniversityId) { throw new CustomException(HOST_UNIVERSITY_HAS_REFERENCES); } } + + private void evictUnivApplyInfoDetailCaches(Long hostUniversityId) { + List affectedUnivApplyInfoIds = univApplyInfoRepository.findIdsByUniversityId(hostUniversityId); + + List cacheKeys = affectedUnivApplyInfoIds.stream() + .map(univApplyInfoId -> "univApplyInfo:" + univApplyInfoId) + .toList(); + + cacheManager.evictMultiple(cacheKeys); + } } diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 9308a6872..1f68be171 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -5,10 +5,11 @@ import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; -import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthResult; import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.oauth.OAuthService; import com.example.solidconnection.auth.service.signin.EmailSignInService; @@ -47,11 +48,9 @@ public ResponseEntity processAppleOAuth( @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, HttpServletResponse httpServletResponse ) { - OAuthResponse oAuthResponse = oAuthService.processOAuth(AuthType.APPLE, oAuthCodeRequest); - if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { - refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); - } - return ResponseEntity.ok(oAuthResponse); + OAuthResult oAuthResult = oAuthService.processOAuth(AuthType.APPLE, oAuthCodeRequest); + setRefreshTokenCookie(httpServletResponse, oAuthResult.refreshToken()); + return ResponseEntity.ok(oAuthResult.response()); } @PostMapping("/kakao") @@ -59,11 +58,9 @@ public ResponseEntity processKakaoOAuth( @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, HttpServletResponse httpServletResponse ) { - OAuthResponse oAuthResponse = oAuthService.processOAuth(AuthType.KAKAO, oAuthCodeRequest); - if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { - refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); - } - return ResponseEntity.ok(oAuthResponse); + OAuthResult oAuthResult = oAuthService.processOAuth(AuthType.KAKAO, oAuthCodeRequest); + setRefreshTokenCookie(httpServletResponse, oAuthResult.refreshToken()); + return ResponseEntity.ok(oAuthResult.response()); } @PostMapping("/email/sign-in") @@ -71,9 +68,9 @@ public ResponseEntity signInWithEmail( @Valid @RequestBody EmailSignInRequest signInRequest, HttpServletResponse httpServletResponse ) { - SignInResponse signInResponse = emailSignInService.signIn(signInRequest); - refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); - return ResponseEntity.ok(signInResponse); + SignInResult signInResult = emailSignInService.signIn(signInRequest); + refreshTokenCookieManager.setCookie(httpServletResponse, signInResult.refreshToken()); + return ResponseEntity.ok(SignInResponse.from(signInResult)); } /* 이메일 회원가입 시 signUpToken 을 발급받기 위한 api */ @@ -87,10 +84,12 @@ public ResponseEntity signUpWithEmail( @PostMapping("/sign-up") public ResponseEntity signUp( - @Valid @RequestBody SignUpRequest signUpRequest + @Valid @RequestBody SignUpRequest signUpRequest, + HttpServletResponse httpServletResponse ) { - SignInResponse signInResponse = signUpService.signUp(signUpRequest); - return ResponseEntity.ok(signInResponse); + SignInResult signInResult = signUpService.signUp(signUpRequest); + refreshTokenCookieManager.setCookie(httpServletResponse, signInResult.refreshToken()); + return ResponseEntity.ok(SignInResponse.from(signInResult)); } @PostMapping("/sign-out") @@ -123,6 +122,12 @@ public ResponseEntity reissueToken(HttpServletRequest request) return ResponseEntity.ok(reissueResponse); } + private void setRefreshTokenCookie(HttpServletResponse httpServletResponse, String refreshToken) { + if (refreshToken != null) { + refreshTokenCookieManager.setCookie(httpServletResponse, refreshToken); + } + } + private String getAccessToken(Authentication authentication) { if (authentication == null || !(authentication.getCredentials() instanceof String accessToken)) { throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "엑세스 토큰이 없습니다."); diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index ac9d39290..05c8563a5 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,14 +1,10 @@ package com.example.solidconnection.auth.dto; -import com.example.solidconnection.auth.domain.AccessToken; -import com.example.solidconnection.auth.domain.RefreshToken; - public record SignInResponse( - String accessToken, - String refreshToken + String accessToken ) { - public static SignInResponse of(AccessToken accessToken, RefreshToken refreshToken) { - return new SignInResponse(accessToken.token(), refreshToken.token()); + public static SignInResponse from(SignInResult signInResult) { + return new SignInResponse(signInResult.accessToken()); } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResult.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResult.java new file mode 100644 index 000000000..78f0f002a --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResult.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.auth.dto; + +import com.example.solidconnection.auth.domain.AccessToken; +import com.example.solidconnection.auth.domain.RefreshToken; + +public record SignInResult( + String accessToken, + String refreshToken +) { + + public static SignInResult of(AccessToken accessToken, RefreshToken refreshToken) { + return new SignInResult(accessToken.token(), refreshToken.token()); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResult.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResult.java new file mode 100644 index 000000000..ee152f9f3 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResult.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.auth.dto.oauth; + +public record OAuthResult( + OAuthResponse response, + String refreshToken +) { + +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java index 6ac121c46..9e9f75851 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java @@ -1,8 +1,11 @@ package com.example.solidconnection.auth.dto.oauth; +import com.example.solidconnection.auth.dto.SignInResult; + public record OAuthSignInResponse( - boolean isRegistered, - String accessToken, - String refreshToken) implements OAuthResponse { + String accessToken) implements OAuthResponse { + public static OAuthSignInResponse from(SignInResult signInResult) { + return new OAuthSignInResponse(signInResult.accessToken()); + } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java index 5a6c60c57..663b8906a 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java @@ -1,7 +1,6 @@ package com.example.solidconnection.auth.dto.oauth; public record SignUpPrepareResponse( - boolean isRegistered, String nickname, String email, String profileImageUrl, @@ -9,7 +8,6 @@ public record SignUpPrepareResponse( public static SignUpPrepareResponse of(OAuthUserInfoDto oAuthUserInfoDto, String signUpToken) { return new SignUpPrepareResponse( - false, oAuthUserInfoDto.getNickname(), oAuthUserInfoDto.getEmail(), oAuthUserInfoDto.getProfileImageUrl(), diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java index 08ab0c0b7..6f8e65d3b 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -1,9 +1,9 @@ package com.example.solidconnection.auth.service.oauth; import com.example.solidconnection.auth.domain.SignUpToken; -import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; -import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthResult; import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; @@ -32,26 +32,26 @@ public class OAuthService { private final OAuthClientMap oauthClientMap; @Transactional - public OAuthResponse processOAuth(AuthType authType, OAuthCodeRequest codeRequest) { + public OAuthResult processOAuth(AuthType authType, OAuthCodeRequest codeRequest) { OAuthClient oauthClient = oauthClientMap.getOAuthClient(authType); OAuthUserInfoDto userInfo = oauthClient.getUserInfo(codeRequest.code()); Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(userInfo.getEmail(), authType); if (optionalSiteUser.isPresent()) { SiteUser siteUser = optionalSiteUser.get(); - return getSignInResponse(siteUser); + return getSignInResult(siteUser); } - return getSignUpPrepareResponse(userInfo, authType); + return getSignUpPrepareResult(userInfo, authType); } - private OAuthSignInResponse getSignInResponse(SiteUser siteUser) { - SignInResponse signInResponse = signInService.signIn(siteUser); - return new OAuthSignInResponse(true, signInResponse.accessToken(), signInResponse.refreshToken()); + private OAuthResult getSignInResult(SiteUser siteUser) { + SignInResult signInResult = signInService.signIn(siteUser); + return new OAuthResult(OAuthSignInResponse.from(signInResult), signInResult.refreshToken()); } - private SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto, AuthType authType) { + private OAuthResult getSignUpPrepareResult(OAuthUserInfoDto userInfoDto, AuthType authType) { SignUpToken signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType); - return SignUpPrepareResponse.of(userInfoDto, signUpToken.token()); + return new OAuthResult(SignUpPrepareResponse.of(userInfoDto, signUpToken.token()), null); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java index 29f379a22..0eda2e5b2 100644 --- a/src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/signin/EmailSignInService.java @@ -3,7 +3,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.SIGN_IN_FAILED; import com.example.solidconnection.auth.dto.EmailSignInRequest; -import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -22,7 +22,7 @@ public class EmailSignInService { private final PasswordEncoder passwordEncoder; @Transactional(readOnly = true) - public SignInResponse signIn(EmailSignInRequest signInRequest) { + public SignInResult signIn(EmailSignInRequest signInRequest) { SiteUser siteUser = getEmailMatchingUserOrThrow(signInRequest.email()); validatePassword(signInRequest.password(), siteUser.getPassword()); return signInService.signIn(siteUser); diff --git a/src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java index ee63a02c3..e4be77a3c 100644 --- a/src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/signin/SignInService.java @@ -2,7 +2,7 @@ import com.example.solidconnection.auth.domain.AccessToken; import com.example.solidconnection.auth.domain.RefreshToken; -import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; @@ -16,11 +16,11 @@ public class SignInService { private final AuthTokenProvider authTokenProvider; @Transactional - public SignInResponse signIn(SiteUser siteUser) { + public SignInResult signIn(SiteUser siteUser) { resetQuitedAt(siteUser); AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - return SignInResponse.of(accessToken, refreshToken); + return SignInResult.of(accessToken, refreshToken); } private void resetQuitedAt(SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java index 8f814be4a..72a419beb 100644 --- a/src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/signup/SignUpService.java @@ -4,7 +4,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.common.exception.ErrorCode.USER_ALREADY_EXISTED; -import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.service.signin.SignInService; import com.example.solidconnection.common.exception.CustomException; @@ -38,7 +38,7 @@ public class SignUpService { private final PasswordTemporaryStorage passwordTemporaryStorage; @Transactional - public SignInResponse signUp(SignUpRequest signUpRequest) { + public SignInResult signUp(SignUpRequest signUpRequest) { // 검증 signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken()); @@ -66,7 +66,7 @@ public SignInResponse signUp(SignUpRequest signUpRequest) { interestedCountryService.saveInterestedCountry(siteUser, signUpRequest.interestedCountries()); // 로그인 - SignInResponse response = signInService.signIn(siteUser); + SignInResult response = signInService.signIn(siteUser); // 회원가입을 위해 저장한 데이터(SignUpToken, 비밀번호) 삭제 clearSignUpData(email, authType); diff --git a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java index 581822f73..6a0631a70 100644 --- a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java +++ b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java @@ -1,6 +1,7 @@ package com.example.solidconnection.cache.manager; import java.time.Duration; +import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; @@ -38,4 +39,10 @@ public void evictUsingPrefix(String key) { redisTemplate.delete(keys); } } + + public void evictMultiple(List keys) { + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } } diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 48e11d846..84a6a8d00 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -41,6 +41,7 @@ public enum ErrorCode { REGION_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "지역을 찾을 수 없습니다."), REGION_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 지역을 찾을 수 없습니다."), REGION_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 지역입니다."), + COUNTRY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 국가입니다."), HOST_UNIVERSITY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 파견 대학입니다."), HOST_UNIVERSITY_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 파견 대학을 참조하는 대학 지원 정보가 존재합니다."), COUNTRY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "국가를 찾을 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/location/country/domain/Country.java b/src/main/java/com/example/solidconnection/location/country/domain/Country.java index f9a487eda..67e295540 100644 --- a/src/main/java/com/example/solidconnection/location/country/domain/Country.java +++ b/src/main/java/com/example/solidconnection/location/country/domain/Country.java @@ -29,4 +29,12 @@ public Country(String code, String koreanName, String regionCode) { this.koreanName = koreanName; this.regionCode = regionCode; } + + public void updateKoreanName(String koreanName) { + this.koreanName = koreanName; + } + + public void updateRegionCode(String regionCode) { + this.regionCode = regionCode; + } } diff --git a/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java b/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java index 69d3426a2..2d0369cb3 100644 --- a/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java +++ b/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java @@ -3,30 +3,21 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; @Configuration public class AmazonS3Config { - @Value("${cloud.aws.credentials.access-key}") - private String accessKey; - - @Value("${cloud.aws.credentials.secret-key}") - private String secretKey; - @Value("${cloud.aws.region.static}") private String region; @Bean public S3Client s3Client() { - AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); - return S3Client.builder() .region(Region.of(region)) - .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .credentialsProvider(DefaultCredentialsProvider.create()) .build(); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index 123c1ab2b..6cf88ca2c 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -6,12 +6,14 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; + +import com.example.solidconnection.siteuser.repository.custom.SiteUserFilterRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface SiteUserRepository extends JpaRepository { +public interface SiteUserRepository extends JpaRepository, SiteUserFilterRepository { Optional findByEmailAndAuthType(String email, AuthType authType); diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepository.java new file mode 100644 index 000000000..5908daf1d --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepository.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.siteuser.repository.custom; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse; +import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition; +import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse; +import com.example.solidconnection.admin.dto.UserInfoDetailResponse; +import com.example.solidconnection.admin.dto.UserSearchCondition; +import com.example.solidconnection.admin.dto.UserSearchResponse; + +public interface SiteUserFilterRepository { + + Page searchAllUsers(UserSearchCondition searchCondition, Pageable pageable); + + Page searchRestrictedUsers(RestrictedUserSearchCondition searchCondition, Pageable pageable); + + UserInfoDetailResponse getUserInfoDetailByUserId(SiteUser user); + + RestrictedUserInfoDetailResponse getRestrictedUserInfoDetail(long userId); +} diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepositoryImpl.java new file mode 100644 index 000000000..020db4863 --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/repository/custom/SiteUserFilterRepositoryImpl.java @@ -0,0 +1,348 @@ +package com.example.solidconnection.siteuser.repository.custom; + +import static com.example.solidconnection.application.domain.QApplication.application; +import static com.example.solidconnection.mentor.domain.QMentor.mentor; +import static com.example.solidconnection.mentor.domain.QMentorApplication.mentorApplication; +import static com.example.solidconnection.mentor.domain.QMentoring.mentoring; +import static com.example.solidconnection.report.domain.QReport.report; +import static com.example.solidconnection.siteuser.domain.QSiteUser.siteUser; +import static com.example.solidconnection.siteuser.domain.QUserBan.userBan; +import static java.time.ZoneOffset.UTC; +import static org.springframework.util.StringUtils.hasText; + +import com.example.solidconnection.admin.dto.MentorApplicationHistoryInfoResponse; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse; +import com.example.solidconnection.university.domain.QUnivApplyInfo; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import com.example.solidconnection.admin.dto.BannedHistoryResponse; +import com.example.solidconnection.admin.dto.BannedInfoResponse; +import com.example.solidconnection.admin.dto.MatchedInfoResponse; +import com.example.solidconnection.admin.dto.MenteeInfoResponse; +import com.example.solidconnection.admin.dto.MentorInfoResponse; +import com.example.solidconnection.admin.dto.ReportedHistoryResponse; +import com.example.solidconnection.admin.dto.ReportedInfoResponse; +import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition; +import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse; +import com.example.solidconnection.admin.dto.UnivApplyInfoResponse; +import com.example.solidconnection.admin.dto.UserInfoDetailResponse; +import com.example.solidconnection.admin.dto.UserSearchCondition; +import com.example.solidconnection.admin.dto.UserSearchResponse; +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.domain.UserStatus; +import com.querydsl.core.types.ConstructorExpression; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; + +@Repository +public class SiteUserFilterRepositoryImpl implements SiteUserFilterRepository { + + private static final ConstructorExpression USER_SEARCH_RESPONSE_PROJECTION = + Projections.constructor( + UserSearchResponse.class, + siteUser.id, + siteUser.nickname, + siteUser.email, + siteUser.role, + siteUser.userStatus + ); + + private static final ConstructorExpression REPORTED_INFO_RESPONSE_PROJECTION = + Projections.constructor( + ReportedInfoResponse.class, + report.createdAt, + report.targetType, + report.reportType + ); + + private static final ConstructorExpression BANNED_INFO_RESPONSE_PROJECTION = + Projections.constructor( + BannedInfoResponse.class, + siteUser.userStatus.eq(UserStatus.BANNED), + userBan.duration + ); + + private static final ConstructorExpression RESTRICTED_USER_SEARCH_RESPONSE_PROJECTION = + Projections.constructor( + RestrictedUserSearchResponse.class, + siteUser.id, + siteUser.nickname, + siteUser.role, + siteUser.userStatus, + REPORTED_INFO_RESPONSE_PROJECTION, + BANNED_INFO_RESPONSE_PROJECTION + ); + + private static final ConstructorExpression REPORTED_HISTORY_RESPONSE_PROJECTION = + Projections.constructor( + ReportedHistoryResponse.class, + report.createdAt, + report.reportType + ); + + private static final ConstructorExpression MATCHED_INFO_RESPONSE_PROJECTION = + Projections.constructor( + MatchedInfoResponse.class, + siteUser.nickname, + mentoring.confirmedAt + ); + + private static final ConstructorExpression MENTOR_APPLICATION_HISTORY_RESPONSE_PROJECTION = + Projections.constructor( + MentorApplicationHistoryInfoResponse.class, + mentorApplication.mentorApplicationStatus, + mentorApplication.rejectedReason, + mentorApplication.createdAt + ); + + private static final ConstructorExpression BANNED_HISTORY_RESPONSE_PROJECTION = + Projections.constructor( + BannedHistoryResponse.class, + userBan.createdAt + ); + + private static final QUnivApplyInfo firstChoiceUnivApplyInfo = new QUnivApplyInfo("firstChoiceUnivApplyInfo"); + private static final QUnivApplyInfo secondChoiceUnivApplyInfo = new QUnivApplyInfo("secondChoiceUnivApplyInfo"); + private static final QUnivApplyInfo thirdChoiceUnivApplyInfo = new QUnivApplyInfo("thirdChoiceUnivApplyInfo"); + + private static final ConstructorExpression UNIV_APPLY_INFO_RESPONSE_PROJECTION = + Projections.constructor( + UnivApplyInfoResponse.class, + firstChoiceUnivApplyInfo.koreanName, + secondChoiceUnivApplyInfo.koreanName, + thirdChoiceUnivApplyInfo.koreanName + ); + + private final JPAQueryFactory queryFactory; + + @Autowired + public SiteUserFilterRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page searchAllUsers(UserSearchCondition condition, Pageable pageable) { + List content = queryFactory + .select(USER_SEARCH_RESPONSE_PROJECTION) + .from(siteUser) + .where( + roleEq(condition.role()), + keywordContains(condition.keyword()) + ) + .orderBy(siteUser.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long totalCount = createUserCountQuery(condition).fetchOne(); + return new PageImpl<>(content, pageable, totalCount != null ? totalCount : 0L); + } + + private JPAQuery createUserCountQuery(UserSearchCondition condition) { + return queryFactory + .select(siteUser.count()) + .from(siteUser) + .where( + roleEq(condition.role()), + keywordContains(condition.keyword()) + ); + } + + @Override + public Page searchRestrictedUsers( + RestrictedUserSearchCondition condition, + Pageable pageable + ) { + List content = queryFactory + .select(RESTRICTED_USER_SEARCH_RESPONSE_PROJECTION) + .from(siteUser) + + // 최신 신고 내역 조회 + .leftJoin(report).on( + report.reportedId.eq(siteUser.id) + .and( + report.id.eq( + JPAExpressions + .select(report.id.max()) + .from(report) + .where(report.reportedId.eq(siteUser.id)) + ) + ) + ) + + // 최신 차단 내역 조회 + .leftJoin(userBan).on( + userBan.bannedUserId.eq(siteUser.id) + .and(userBan.isExpired.eq(false)) + .and(userBan.expiredAt.after(ZonedDateTime.now(UTC))) + ) + + .where( + roleEq(condition.role()), + isRestrictedUser(), + userStatusEq(condition.userStatus()), + keywordContains(condition.keyword()) + ) + .orderBy(siteUser.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long totalCount = createRestrictedUserCountQuery(condition).fetchOne(); + + return new PageImpl<>(content, pageable, totalCount != null ? totalCount : 0L); + } + + + private JPAQuery createRestrictedUserCountQuery(RestrictedUserSearchCondition condition) { + return queryFactory + .select(siteUser.count()) + .from(siteUser) + .where( + roleEq(condition.role()), + isRestrictedUser(), + userStatusEq(condition.userStatus()), + keywordContains(condition.keyword()) + ); + } + + private BooleanExpression isRestrictedUser() { + return siteUser.userStatus.in( + UserStatus.REPORTED, + UserStatus.BANNED + ); + } + + private BooleanExpression roleEq(Role role) { + return role != null ? siteUser.role.eq(role) : null; + } + + private BooleanExpression userStatusEq(UserStatus userStatus) { + return userStatus != null ? siteUser.userStatus.eq(userStatus) : null; + } + + private BooleanExpression keywordContains(String keyword) { + if (!hasText(keyword)) { + return null; + } + return siteUser.nickname.containsIgnoreCase(keyword); + } + + @Override + public UserInfoDetailResponse getUserInfoDetailByUserId(SiteUser user) { + // 신고 내역 + List reportedHistoryResponses = new ArrayList<>(); + if (user.getUserStatus() != UserStatus.ACTIVE) { + reportedHistoryResponses = fetchReportedHistories(user.getId()); + } + + if (user.getRole() == Role.MENTOR) { + // 멘토 상세 내역 + MentorInfoResponse mentorInfoResponse = fetchMentorInfo(user.getId()); + return new UserInfoDetailResponse(mentorInfoResponse, null, reportedHistoryResponses); + } else { + // 멘티 상세 내역 + MenteeInfoResponse menteeInfoResponse = fetchMenteeInfo(user.getId()); + return new UserInfoDetailResponse(null, menteeInfoResponse, reportedHistoryResponses); + } + } + + @Override + public RestrictedUserInfoDetailResponse getRestrictedUserInfoDetail(long userId) { + List reportedHistoryResponses = fetchReportedHistories(userId); + List bannedHistoryResponses = fetchBannedHistories(userId); + + return new RestrictedUserInfoDetailResponse(reportedHistoryResponses, bannedHistoryResponses); + } + + private List fetchReportedHistories(long userId) { + return queryFactory + .select(REPORTED_HISTORY_RESPONSE_PROJECTION) + .from(report) + .where(report.reportedId.eq(userId)) + .orderBy(report.createdAt.desc()) + .fetch(); + } + + private List fetchBannedHistories(long userId) { + return queryFactory + .select(BANNED_HISTORY_RESPONSE_PROJECTION) + .from(userBan) + .where(userBan.bannedUserId.eq(userId)) + .orderBy(userBan.createdAt.desc()) + .fetch(); + } + + private MentorInfoResponse fetchMentorInfo(long userId) { + Long mentorId = queryFactory + .select(mentor.id) + .from(mentor) + .where(mentor.siteUserId.eq(userId)) + .fetchOne(); + + List menteeInfos = new ArrayList<>(); + if (mentorId != null) { + menteeInfos = queryFactory + .select(MATCHED_INFO_RESPONSE_PROJECTION) + .from(mentoring) + .join(siteUser).on(siteUser.id.eq(mentoring.menteeId)) + .where(mentoring.mentorId.eq(mentorId)) + .orderBy(mentoring.confirmedAt.desc()) + .fetch(); + } + + List mentorApplicationHistory = queryFactory + .select(MENTOR_APPLICATION_HISTORY_RESPONSE_PROJECTION) + .from(mentorApplication) + .where(mentorApplication.siteUserId.eq(userId)) + .orderBy(mentorApplication.createdAt.desc()) + .fetch(); + + return new MentorInfoResponse(menteeInfos, mentorApplicationHistory); + } + + private MenteeInfoResponse fetchMenteeInfo(long userId) { + UnivApplyInfoResponse univApplyInfoResponse = fetchUnivApplyInfo(userId); + List mentorInfos = queryFactory + .select(MATCHED_INFO_RESPONSE_PROJECTION) + .from(mentoring) + .join(mentor).on(mentor.id.eq(mentoring.mentorId)) + .join(siteUser).on(siteUser.id.eq(mentor.siteUserId)) + .where(mentoring.menteeId.eq(userId)) + .orderBy(mentoring.confirmedAt.desc()) + .fetch(); + + return new MenteeInfoResponse(univApplyInfoResponse, mentorInfos); + } + + private UnivApplyInfoResponse fetchUnivApplyInfo(long userId) { + UnivApplyInfoResponse result = queryFactory + .select(UNIV_APPLY_INFO_RESPONSE_PROJECTION) + .from(application) + .leftJoin(firstChoiceUnivApplyInfo).on(firstChoiceUnivApplyInfo.id.eq(application.firstChoiceUnivApplyInfoId)) + .leftJoin(secondChoiceUnivApplyInfo).on(secondChoiceUnivApplyInfo.id.eq(application.secondChoiceUnivApplyInfoId)) + .leftJoin(thirdChoiceUnivApplyInfo).on(thirdChoiceUnivApplyInfo.id.eq(application.thirdChoiceUnivApplyInfoId)) + .where(application.siteUserId.eq(userId)) + .orderBy(application.createdAt.desc()) + .fetchFirst(); + + if (result == null) { + return new UnivApplyInfoResponse(null, null, null); + } + + return result; + } + + +} diff --git a/src/main/java/com/example/solidconnection/term/domain/Term.java b/src/main/java/com/example/solidconnection/term/domain/Term.java index 07d9269e1..9c8c5c8da 100644 --- a/src/main/java/com/example/solidconnection/term/domain/Term.java +++ b/src/main/java/com/example/solidconnection/term/domain/Term.java @@ -31,19 +31,11 @@ public class Term { @Column(length = 20) private String name; - @Column(name = "is_current", nullable = false) - private boolean isCurrent = false; + @Column(name = "is_current", unique = true) + private Boolean isCurrent; public Term(String name, boolean isCurrent) { this.name = name; - this.isCurrent = isCurrent; - } - - public void setAsCurrent() { - this.isCurrent = true; - } - - public void setAsNotCurrent() { - this.isCurrent = false; + this.isCurrent = isCurrent ? true : null; } } diff --git a/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java b/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java index e0b71f8a9..0b016750c 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java @@ -68,4 +68,11 @@ default UnivApplyInfo getUnivApplyInfoById(Long id) { List findAllByIds(@Param("ids") List ids); boolean existsByUniversityId(Long universityId); + + @Query(""" + SELECT uai.id + FROM UnivApplyInfo uai + WHERE uai.university.id = :universityId + """) + List findIdsByUniversityId(@Param("universityId") Long universityId); } diff --git a/src/main/resources/application-loadtest.yml b/src/main/resources/application-loadtest.yml new file mode 100644 index 000000000..ede26ed21 --- /dev/null +++ b/src/main/resources/application-loadtest.yml @@ -0,0 +1,32 @@ +spring: + config: + activate: + on-profile: loadtest + + jpa: + show-sql: false + properties: + hibernate: + format_sql: false + + datasource: + hikari: + maximum-pool-size: 50 + minimum-idle: 20 + + flyway: + enabled: false + +websocket: + thread-pool: + inbound: + core-pool-size: 24 + max-pool-size: 48 + queue-capacity: 4000 + outbound: + core-pool-size: 24 + max-pool-size: 48 + queue-capacity: 4000 + heartbeat: + server-interval: 10000 + client-interval: 10000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a644c7e9f..7b3bffa28 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,13 @@ spring: + application: + name: solid-connect-server + config: import: - - classpath:/secret/application-cloud.yml - - classpath:/secret/application-db.yml - - classpath:/secret/application-variable.yml + - optional:classpath:/config/application-cloud.yml + - optional:classpath:/config/application-db.yml + - optional:classpath:/config/application-variable.yml + - aws-parameterstore:/solid-connection/common/ tomcat: threads: @@ -23,3 +27,31 @@ management: web: exposure: include: prometheus + +--- +spring: + config: + activate: + on-profile: local + import: aws-parameterstore:/solid-connection/local/ + +--- +spring: + config: + activate: + on-profile: dev + import: aws-parameterstore:/solid-connection/dev/ + +--- +spring: + config: + activate: + on-profile: prod + import: aws-parameterstore:/solid-connection/prod/ + +--- +spring: + config: + activate: + on-profile: loadtest + import: aws-parameterstore:/solid-connection/loadtest/ diff --git a/src/main/resources/config/application-cloud.yml b/src/main/resources/config/application-cloud.yml new file mode 100644 index 000000000..634b0cb6a --- /dev/null +++ b/src/main/resources/config/application-cloud.yml @@ -0,0 +1,9 @@ +spring: + config: + activate: + on-profile: local, dev, prod, loadtest + +cloud: + aws: + stack: + auto: false diff --git a/src/main/resources/config/application-db.yml b/src/main/resources/config/application-db.yml new file mode 100644 index 000000000..cd461ea38 --- /dev/null +++ b/src/main/resources/config/application-db.yml @@ -0,0 +1,70 @@ +--- +spring: + config: + activate: + on-profile: prod + + jpa: + hibernate: + ddl-auto: none + generate-ddl: false + show-sql: false + database: mysql + defer-datasource-initialization: false + + datasource: + driverClassName: com.mysql.cj.jdbc.Driver + + flyway: + enabled: true + locations: classpath:db/migration + baseline-on-migrate: false + +--- +spring: + config: + activate: + on-profile: dev + + jpa: + hibernate: + ddl-auto: validate + generate-ddl: false + show-sql: false + database: mysql + defer-datasource-initialization: false + + datasource: + driverClassName: com.mysql.cj.jdbc.Driver + + flyway: + enabled: true + locations: classpath:db/migration + baseline-on-migrate: false + +--- +spring: + config: + activate: + on-profile: local + + jpa: + hibernate: + ddl-auto: create + generate-ddl: true + show-sql: true + database: mysql + defer-datasource-initialization: true + properties: + hibernate: + format_sql: true + + sql: + init: + mode: always + + datasource: + driverClassName: com.mysql.cj.jdbc.Driver + + flyway: + enabled: false diff --git a/src/main/resources/config/application-variable.yml b/src/main/resources/config/application-variable.yml new file mode 100644 index 000000000..400f3b396 --- /dev/null +++ b/src/main/resources/config/application-variable.yml @@ -0,0 +1,155 @@ +view: + count: + scheduling: + delay: 3000 + +websocket: + thread-pool: + inbound: + core-pool-size: 6 + max-pool-size: 12 + queue-capacity: 1000 + outbound: + core-pool-size: 6 + max-pool-size: 12 + queue-capacity: 1000 + heartbeat: + server-interval: 15000 + client-interval: 15000 + +oauth: + apple: + token-url: "https://appleid.apple.com/auth/token" + client-secret-audience-url: "https://appleid.apple.com" + public-key-url: "https://appleid.apple.com/auth/keys" + kakao: + token-url: "https://kauth.kakao.com/oauth/token" + user-info-url: "https://kapi.kakao.com/v2/user/me" + +sentry: + send-default-pii: false + traces-sample-rate: 1.0 + exception-resolver-order: -2147483647 + +management: + server: + port: 8081 + +token: + access: + storage-key-prefix: "ACCESS" + expire-time: 1h + refresh: + storage-key-prefix: "REFRESH" + expire-time: 90d + sign-up: + storage-key-prefix: "SIGN_UP" + expire-time: 10m + black-list: + storage-key-prefix: "BLACKLIST" + expire-time: 1h + +--- +spring: + config: + activate: + on-profile: prod + +websocket: + thread-pool: + inbound: + core-pool-size: 16 + max-pool-size: 32 + queue-capacity: 2000 + outbound: + core-pool-size: 16 + max-pool-size: 32 + queue-capacity: 2000 + heartbeat: + server-interval: 10000 + client-interval: 10000 + +cors: + allowed-origins: + - "https://www.solid-connection.com" + - "https://api.solid-connection.com" + - "https://admin.solid-connection.com" + +sentry: + environment: "production" + traces-sample-rate: 0.1 + +token: + refresh: + cookie-domain: ".solid-connection.com" + +--- +spring: + config: + activate: + on-profile: dev + +websocket: + thread-pool: + inbound: + core-pool-size: 4 + max-pool-size: 8 + queue-capacity: 500 + outbound: + core-pool-size: 4 + max-pool-size: 8 + queue-capacity: 500 + heartbeat: + server-interval: 15000 + client-interval: 15000 + +cors: + allowed-origins: + - "https://www.stage.solid-connection.com" + - "https://api.stage.solid-connection.com" + - "https://admins.solid-connection.com" + - "https://www.admins.solid-connection.com" + - "http://localhost:3000" + - "https://localhost:3000" + - "http://localhost:5173" + +sentry: + environment: "dev" + +token: + refresh: + cookie-domain: ".stage.solid-connection.com" + +--- +spring: + config: + activate: + on-profile: local + +websocket: + thread-pool: + inbound: + core-pool-size: 4 + max-pool-size: 8 + queue-capacity: 500 + outbound: + core-pool-size: 4 + max-pool-size: 8 + queue-capacity: 500 + heartbeat: + server-interval: 15000 + client-interval: 15000 + +cors: + allowed-origins: + - "http://localhost:8080" + - "http://localhost:3000" + - "http://localhost:5173" + - "https://localhost:3000" + +sentry: + environment: "development" + +token: + refresh: + cookie-domain: "localhost" diff --git a/src/main/resources/db/migration/V47__add_unique_constraint_to_term_is_current.sql b/src/main/resources/db/migration/V47__add_unique_constraint_to_term_is_current.sql new file mode 100644 index 000000000..86ae721ef --- /dev/null +++ b/src/main/resources/db/migration/V47__add_unique_constraint_to_term_is_current.sql @@ -0,0 +1,9 @@ +ALTER TABLE term + MODIFY COLUMN is_current BOOLEAN NULL DEFAULT NULL; + +UPDATE term +SET is_current = NULL +WHERE is_current = FALSE; + +ALTER TABLE term + ADD CONSTRAINT uk_term_is_current UNIQUE (is_current); diff --git a/src/main/resources/secret b/src/main/resources/secret deleted file mode 160000 index 1f93968a8..000000000 --- a/src/main/resources/secret +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1f93968a8475d4545d90e8f681b96382d25586af diff --git a/src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java b/src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java new file mode 100644 index 000000000..e9784cbcd --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java @@ -0,0 +1,238 @@ +package com.example.solidconnection.admin.location.country.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest; +import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse; +import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.location.country.domain.Country; +import com.example.solidconnection.location.country.fixture.CountryFixture; +import com.example.solidconnection.location.country.repository.CountryRepository; +import com.example.solidconnection.location.region.domain.Region; +import com.example.solidconnection.location.region.fixture.RegionFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@TestContainerSpringBootTest +@DisplayName("국가 관련 관리자 서비스 테스트") +class AdminCountryServiceTest { + + @Autowired + private AdminCountryService adminCountryService; + + @Autowired + private CountryRepository countryRepository; + + @Autowired + private CountryFixture countryFixture; + + @Autowired + private RegionFixture regionFixture; + + @Nested + class 전체_국가_조회 { + + @Test + void 국가가_없으면_빈_목록을_반환한다() { + // when + List responses = adminCountryService.getAllCountries(); + + // then + assertThat(responses).isEqualTo(List.of()); + } + + @Test + void 저장된_모든_국가를_조회한다() { + // given + Country country1 = countryFixture.미국(); + Country country2 = countryFixture.캐나다(); + Country country3 = countryFixture.일본(); + + // when + List responses = adminCountryService.getAllCountries(); + + // then + assertThat(responses) + .hasSize(3) + .extracting(AdminCountryResponse::code) + .containsExactlyInAnyOrder( + country1.getCode(), + country2.getCode(), + country3.getCode() + ); + } + } + + @Nested + class 국가_생성 { + + @Test + void 유효한_정보로_국가를_생성하면_성공한다() { + // given + Region region = regionFixture.아시아(); + AdminCountryCreateRequest request = new AdminCountryCreateRequest("KR", "대한민국", region.getCode()); + + // when + AdminCountryResponse response = adminCountryService.createCountry(request); + + // then + assertThat(response.code()).isEqualTo("KR"); + assertThat(response.koreanName()).isEqualTo("대한민국"); + assertThat(response.regionCode()).isEqualTo(region.getCode()); + + // 데이터베이스에 저장되었는지 확인 + Country savedCountry = countryRepository.findByCode(request.code()).orElseThrow(); + assertAll( + () -> assertThat(savedCountry.getKoreanName()).isEqualTo(request.koreanName()), + () -> assertThat(savedCountry.getRegionCode()).isEqualTo(request.regionCode()) + ); + } + + @Test + void 이미_존재하는_코드로_국가를_생성하면_예외_응답을_반환한다() { + // given + Country country = countryFixture.미국(); + + AdminCountryCreateRequest request = new AdminCountryCreateRequest("US", "새로운 미국", country.getRegionCode()); + + // when & then + assertThatCode(() -> adminCountryService.createCountry(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 이미_존재하는_한글명으로_국가를_생성하면_예외_응답을_반환한다() { + // given + countryFixture.일본(); + Region region = regionFixture.아시아(); + + AdminCountryCreateRequest request = new AdminCountryCreateRequest("NEW_CODE", "일본", region.getCode()); + + // when & then + assertThatCode(() -> adminCountryService.createCountry(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 존재하지_않는_지역_코드로_국가를_생성하면_예외_응답을_반환한다() { + // given + AdminCountryCreateRequest request = new AdminCountryCreateRequest("KR", "대한민국", "NOT_EXIST_REGION"); + + // when & then + assertThatCode(() -> adminCountryService.createCountry(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.REGION_NOT_FOUND.getMessage()); + } + } + + @Nested + class 국가_수정 { + + @Test + void 유효한_정보로_국가를_수정하면_성공한다() { + // given + Country country = countryFixture.미국(); + Region newRegion = regionFixture.유럽(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest("미합중국", newRegion.getCode()); + + // when + AdminCountryResponse response = adminCountryService.updateCountry(country.getCode(), request); + + // then + Country updatedCountry = countryRepository.findByCode(country.getCode()).orElseThrow(); + assertAll( + () -> assertThat(response.code()).isEqualTo(country.getCode()), + () -> assertThat(updatedCountry.getKoreanName()).isEqualTo(request.koreanName()), + () -> assertThat(updatedCountry.getRegionCode()).isEqualTo(request.regionCode()) + ); + } + + @Test + void 존재하지_않는_국가_코드로_수정하면_예외_응답을_반환한다() { + // given + Region region = regionFixture.아시아(); + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest("대한민국", region.getCode()); + + // when & then + assertThatCode(() -> adminCountryService.updateCountry("NOT_EXIST", request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_NOT_FOUND.getMessage()); + } + + @Test + void 다른_국가의_한글명으로_수정하면_예외_응답을_반환한다() { + // given + Country country1 = countryFixture.미국(); + Country country2 = countryFixture.캐나다(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest(country2.getKoreanName(), country1.getRegionCode()); + + // when & then + assertThatCode(() -> adminCountryService.updateCountry(country1.getCode(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 같은_국가의_한글명으로_수정하면_성공한다() { + // given + Country country = countryFixture.일본(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest(country.getKoreanName(), country.getRegionCode()); + + // when + AdminCountryResponse response = adminCountryService.updateCountry(country.getCode(), request); + + // then + assertThat(response.code()).isEqualTo(country.getCode()); + } + + @Test + void 존재하지_않는_지역_코드로_수정하면_예외_응답을_반환한다() { + // given + Country country = countryFixture.미국(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest("미합중국", "NOT_EXIST_REGION"); + + // when & then + assertThatCode(() -> adminCountryService.updateCountry(country.getCode(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.REGION_NOT_FOUND.getMessage()); + } + } + + @Nested + class 국가_삭제 { + + @Test + void 존재하는_국가를_삭제하면_성공한다() { + // given + Country country = countryFixture.미국(); + + // when + adminCountryService.deleteCountry(country.getCode()); + + // then + assertThat(countryRepository.findByCode(country.getCode())).isEmpty(); + } + + @Test + void 존재하지_않는_국가를_삭제하면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> adminCountryService.deleteCountry("NOT_EXIST")) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java index 620f18a4d..14200ebb8 100644 --- a/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java +++ b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; @@ -9,6 +11,7 @@ import com.example.solidconnection.admin.university.dto.AdminHostUniversitySearchCondition; import com.example.solidconnection.admin.university.dto.AdminHostUniversityUpdateRequest; import com.example.solidconnection.admin.university.service.AdminHostUniversityService; +import com.example.solidconnection.cache.manager.CustomCacheManager; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.location.country.domain.Country; @@ -17,13 +20,16 @@ import com.example.solidconnection.location.region.fixture.RegionFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.domain.UnivApplyInfo; import com.example.solidconnection.university.fixture.UnivApplyInfoFixtureBuilder; import com.example.solidconnection.university.fixture.UniversityFixture; import com.example.solidconnection.university.repository.HostUniversityRepository; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -49,6 +55,9 @@ class AdminHostUniversityServiceTest { @Autowired private UnivApplyInfoFixtureBuilder univApplyInfoFixtureBuilder; + @SpyBean + private CustomCacheManager cacheManager; + @Nested class 목록_조회 { @@ -398,4 +407,82 @@ class 삭제 { .hasMessage(ErrorCode.HOST_UNIVERSITY_HAS_REFERENCES.getMessage()); } } + + @Nested + class 캐시_무효화 { + + @Test + void 대학_생성_시_캐시가_무효화된다() { + // given + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityCreateRequest request = new AdminHostUniversityCreateRequest( + "캐시 테스트 대학", + "Cache Test University", + "캐시 테스트 대학", + "https://homepage.com", + null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + country.getCode(), + region.getCode() + ); + + // when + adminHostUniversityService.createHostUniversity(request); + + // then + then(cacheManager).should(times(1)).evictUsingPrefix("univApplyInfoTextSearch"); + then(cacheManager).should(times(1)).evictUsingPrefix("university:recommend:general"); + } + + @Test + void 대학_수정_시_캐시가_무효화된다() { + // given + HostUniversity university = universityFixture.괌_대학(); + UnivApplyInfo univApplyInfo = univApplyInfoFixtureBuilder.univApplyInfo() + .termId(1L) + .koreanName("괌 대학 지원 정보") + .university(university) + .create(); + + Country country = countryFixture.일본(); + Region region = regionFixture.아시아(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + "수정된 대학명", + "Updated University", + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + country.getCode(), + region.getCode() + ); + + // when + adminHostUniversityService.updateHostUniversity(university.getId(), request); + + // then + then(cacheManager).should(times(1)).evictUsingPrefix("univApplyInfoTextSearch"); + then(cacheManager).should(times(1)).evictUsingPrefix("university:recommend:general"); + then(cacheManager).should(times(1)).evictMultiple(List.of("univApplyInfo:" + univApplyInfo.getId())); + } + + @Test + void 대학_삭제_시_캐시가_무효화된다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + // when + adminHostUniversityService.deleteHostUniversity(university.getId()); + + // then + then(cacheManager).should(times(1)).evictUsingPrefix("univApplyInfoTextSearch"); + then(cacheManager).should(times(1)).evictUsingPrefix("university:recommend:general"); + } + } } diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminUserServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminUserServiceTest.java new file mode 100644 index 000000000..3c1d081b2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/service/AdminUserServiceTest.java @@ -0,0 +1,443 @@ +package com.example.solidconnection.admin.service; + +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.admin.dto.RestrictedUserInfoDetailResponse; +import com.example.solidconnection.admin.dto.RestrictedUserSearchCondition; +import com.example.solidconnection.admin.dto.RestrictedUserSearchResponse; +import com.example.solidconnection.admin.dto.UserInfoDetailResponse; +import com.example.solidconnection.admin.dto.UserSearchCondition; +import com.example.solidconnection.admin.dto.UserSearchResponse; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.fixture.ApplicationFixture; +import com.example.solidconnection.university.domain.LanguageTestType; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.community.board.fixture.BoardFixture; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostCategory; +import com.example.solidconnection.community.post.fixture.PostFixture; +import com.example.solidconnection.mentor.domain.Mentor; +import com.example.solidconnection.mentor.domain.UniversitySelectType; +import com.example.solidconnection.mentor.fixture.MentorApplicationFixture; +import com.example.solidconnection.mentor.fixture.MentorFixture; +import com.example.solidconnection.mentor.fixture.MentoringFixture; +import com.example.solidconnection.report.domain.TargetType; +import com.example.solidconnection.report.fixture.ReportFixture; +import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.domain.UserStatus; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.siteuser.fixture.UserBanFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.term.fixture.TermFixture; +import com.example.solidconnection.university.domain.UnivApplyInfo; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.fixture.UnivApplyInfoFixture; +import com.example.solidconnection.university.fixture.UniversityFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@TestContainerSpringBootTest +@DisplayName("어드민 유저 관리 서비스 테스트") +public class AdminUserServiceTest { + + @Autowired + private AdminUserService adminUserService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private ReportFixture reportFixture; + + @Autowired + private UserBanFixture userBanFixture; + + @Autowired + private PostFixture postFixture; + + @Autowired + private BoardFixture boardFixture; + + @Autowired + private MentorFixture mentorFixture; + + @Autowired + private MentorApplicationFixture mentorApplicationFixture; + + @Autowired + private MentoringFixture mentoringFixture; + + @Autowired + private UniversityFixture universityFixture; + + @Autowired + private UnivApplyInfoFixture univApplyInfoFixture; + + @Autowired + private ApplicationFixture applicationFixture; + + @Autowired + private TermFixture termFixture; + + @Nested + class 전체_유저_검색 { + + @Test + void 전체_유저를_조회한다() { + // given + siteUserFixture.사용자(1, "유저1"); + siteUserFixture.사용자(2, "유저2"); + siteUserFixture.사용자(3, "유저3"); + + UserSearchCondition condition = new UserSearchCondition(null, null); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchAllUsers(condition, pageable); + + // then + assertThat(result.getContent()).hasSize(3); + } + + @Test + void role로_필터링하여_조회한다() { + // given + siteUserFixture.사용자(1, "멘티1"); + siteUserFixture.사용자(2, "멘티2"); + siteUserFixture.멘토(1, "멘토1"); + + UserSearchCondition condition = new UserSearchCondition(Role.MENTEE, null); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchAllUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(2), + () -> assertThat(result.getContent()) + .allMatch(user -> user.role() == Role.MENTEE) + ); + } + + @Test + void 닉네임으로_검색한다() { + // given + siteUserFixture.사용자(1, "피카츄1"); + siteUserFixture.사용자(2, "꼬부기"); + siteUserFixture.사용자(3, "피카츄2"); + + UserSearchCondition condition = new UserSearchCondition(null, "피카"); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchAllUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(2), + () -> assertThat(result.getContent()) + .allMatch(user -> user.nickname().contains("피카")) + ); + } + + @Test + void 페이징이_정상_작동한다() { + // given + for (int i = 1; i <= 15; i++) { + siteUserFixture.사용자(i, "유저" + i); + } + + UserSearchCondition condition = new UserSearchCondition(null, null); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchAllUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(10), + () -> assertThat(result.getTotalElements()).isEqualTo(15), + () -> assertThat(result.getTotalPages()).isEqualTo(2) + ); + } + } + + @Nested + class 유저_상세_정보_조회 { + + @Test + void 멘티_유저_상세_정보를_조회한다() { + // given + SiteUser mentee = siteUserFixture.사용자(1, "멘티유저"); + long termId = termFixture.현재_학기("2025-1").getId(); + + UnivApplyInfo firstChoice = univApplyInfoFixture.괌대학_A_지원_정보(termId); + UnivApplyInfo secondChoice = univApplyInfoFixture.네바다주립대학_라스베이거스_지원_정보(termId); + + applicationFixture.지원서( + mentee, + "지원닉네임", + termId, + new Gpa(4.0, 4.5, "http://gpa-report.com/test.pdf"), + new LanguageTest(LanguageTestType.TOEIC, "900", "http://language-test.com/test.pdf"), + firstChoice.getId(), + secondChoice.getId(), + null + ); + + // when + UserInfoDetailResponse result = adminUserService.getUserInfoDetail(mentee.getId()); + + // then + assertAll( + () -> assertThat(result.mentorInfoResponse()).isNull(), + () -> assertThat(result.menteeInfoResponse()).isNotNull(), + () -> assertThat(result.menteeInfoResponse().univApplyInfo()).isNotNull() + ); + } + + @Test + void 멘토_유저_상세_정보를_조회한다() { + // given + SiteUser mentorUser = siteUserFixture.멘토(1, "멘토유저"); + HostUniversity university = universityFixture.괌_대학(); + + Mentor mentor = mentorFixture.멘토(mentorUser.getId(), university.getId()); + mentorApplicationFixture.승인된_멘토신청( + mentorUser.getId(), + UniversitySelectType.CATALOG, + university.getId() + ); + + SiteUser mentee = siteUserFixture.사용자(1, "멘티유저"); + mentoringFixture.승인된_멘토링(mentor.getId(), mentee.getId()); + + // when + UserInfoDetailResponse result = adminUserService.getUserInfoDetail(mentorUser.getId()); + + // then + assertAll( + () -> assertThat(result.mentorInfoResponse()).isNotNull(), + () -> assertThat(result.menteeInfoResponse()).isNull(), + () -> assertThat(result.mentorInfoResponse().menteeInfos()).hasSize(1), + () -> assertThat(result.mentorInfoResponse().mentorApplicationHistory()).hasSize(1) + ); + } + + @Test + void 신고된_유저는_신고_내역이_포함된다() { + // given + SiteUser reportedUser = siteUserFixture.신고된_사용자("신고된유저"); + SiteUser reporter = siteUserFixture.사용자(1, "신고자"); + + Post post = postFixture.게시글( + "신고된 게시글", + "내용", + false, + PostCategory.자유, + boardFixture.자유게시판(), + reportedUser + ); + + reportFixture.신고(reporter.getId(), reportedUser.getId(), TargetType.POST, post.getId()); + + // when + UserInfoDetailResponse result = adminUserService.getUserInfoDetail(reportedUser.getId()); + + // then + assertThat(result.reportedHistoryResponses()).hasSize(1); + } + + @Test + void 존재하지_않는_유저_조회_시_예외가_발생한다() { + // given + long notExistUserId = 999999L; + + // when & then + assertThatCode(() -> adminUserService.getUserInfoDetail(notExistUserId)) + .isInstanceOf(CustomException.class) + .hasMessage(USER_NOT_FOUND.getMessage()); + } + } + + @Nested + class 신고_차단된_유저_검색 { + + @Test + void 신고_차단된_유저만_조회한다() { + // given + siteUserFixture.사용자(1, "일반유저"); + siteUserFixture.신고된_사용자("신고된유저"); + siteUserFixture.차단된_사용자("차단된유저"); + + RestrictedUserSearchCondition condition = new RestrictedUserSearchCondition(null, null, null); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchRestrictedUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(2), + () -> assertThat(result.getContent()) + .allMatch(user -> + user.userStatus() == UserStatus.REPORTED || + user.userStatus() == UserStatus.BANNED + ) + ); + } + + @Test + void role로_필터링하여_조회한다() { + // given + siteUserFixture.신고된_사용자("신고된멘티"); + siteUserFixture.신고된_사용자_멘토(1, "신고된멘토"); + + RestrictedUserSearchCondition condition = new RestrictedUserSearchCondition(Role.MENTOR, null, null); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchRestrictedUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(1), + () -> assertThat(result.getContent().get(0).role()).isEqualTo(Role.MENTOR) + ); + } + + @Test + void userStatus로_필터링하여_조회한다() { + // given + siteUserFixture.신고된_사용자("신고된유저"); + siteUserFixture.차단된_사용자("차단된유저"); + + RestrictedUserSearchCondition condition = new RestrictedUserSearchCondition(null, UserStatus.BANNED, null); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchRestrictedUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(1), + () -> assertThat(result.getContent().get(0).userStatus()).isEqualTo(UserStatus.BANNED) + ); + } + + @Test + void 닉네임으로_검색한다() { + // given + siteUserFixture.신고된_사용자("피카츄"); + siteUserFixture.차단된_사용자("꼬부기"); + + RestrictedUserSearchCondition condition = new RestrictedUserSearchCondition(null, null, "피카"); + Pageable pageable = PageRequest.of(0, 10); + + // when + Page result = adminUserService.searchRestrictedUsers(condition, pageable); + + // then + assertAll( + () -> assertThat(result.getContent()).hasSize(1), + () -> assertThat(result.getContent().get(0).nickname()).contains("피카") + ); + } + } + + @Nested + class 신고_차단된_유저_상세_정보_조회 { + + @Test + void 신고_내역을_조회한다() { + // given + SiteUser reportedUser = siteUserFixture.신고된_사용자("신고된유저"); + SiteUser reporter1 = siteUserFixture.사용자(1, "신고자1"); + SiteUser reporter2 = siteUserFixture.사용자(2, "신고자2"); + + Post post = postFixture.게시글( + "게시글", + "내용", + false, + PostCategory.자유, + boardFixture.자유게시판(), + reportedUser + ); + + reportFixture.신고(reporter1.getId(), reportedUser.getId(), TargetType.POST, post.getId()); + reportFixture.신고(reporter2.getId(), reportedUser.getId(), TargetType.POST, post.getId()); + + // when + RestrictedUserInfoDetailResponse result = adminUserService.getRestrictedUserInfoDetail(reportedUser.getId()); + + // then + assertThat(result.reportedHistoryResponses()).hasSize(2); + } + + @Test + void 차단_내역을_조회한다() { + // given + SiteUser bannedUser = siteUserFixture.차단된_사용자("차단된유저"); + SiteUser admin = siteUserFixture.관리자(); + + userBanFixture.수동_차단_해제(bannedUser.getId(), admin.getId()); + + // when + RestrictedUserInfoDetailResponse result = adminUserService.getRestrictedUserInfoDetail(bannedUser.getId()); + + // then + assertThat(result.bannedHistoryResponses()).hasSize(1); + } + + @Test + void 신고_차단_내역을_함께_조회한다() { + // given + SiteUser user = siteUserFixture.차단된_사용자("차단된유저"); + SiteUser reporter = siteUserFixture.사용자(1, "신고자"); + SiteUser admin = siteUserFixture.관리자(); + + Post post = postFixture.게시글( + "게시글", + "내용", + false, + PostCategory.자유, + boardFixture.자유게시판(), + user + ); + + reportFixture.신고(reporter.getId(), user.getId(), TargetType.POST, post.getId()); + userBanFixture.수동_차단_해제(user.getId(), admin.getId()); + + // when + RestrictedUserInfoDetailResponse result = adminUserService.getRestrictedUserInfoDetail(user.getId()); + + // then + assertAll( + () -> assertThat(result.reportedHistoryResponses()).hasSize(1), + () -> assertThat(result.bannedHistoryResponses()).hasSize(1) + ); + } + + @Test + void 존재하지_않는_유저_조회_시_예외가_발생한다() { + // given + long notExistUserId = 999999L; + + // when & then + assertThatCode(() -> adminUserService.getRestrictedUserInfoDetail(notExistUserId)) + .isInstanceOf(CustomException.class) + .hasMessage(USER_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java index 427701399..49d11a340 100644 --- a/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthServiceTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthResult; import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; @@ -57,32 +58,33 @@ void setUp() { // 실제 client 호출하지 않도록 mocking siteUserFixture.사용자(email, authType); // when - OAuthResponse response = oAuthService.processOAuth(authType, new OAuthCodeRequest(oauthCode)); + OAuthResult oAuthResult = oAuthService.processOAuth(authType, new OAuthCodeRequest(oauthCode)); // then + OAuthResponse response = oAuthResult.response(); assertThat(response).isInstanceOf(OAuthSignInResponse.class); OAuthSignInResponse signInResponse = (OAuthSignInResponse) response; assertAll( - () -> assertThat(signInResponse.isRegistered()).isTrue(), () -> assertThat(signInResponse.accessToken()).isNotBlank(), - () -> assertThat(signInResponse.refreshToken()).isNotBlank() + () -> assertThat(oAuthResult.refreshToken()).isNotBlank() ); } @Test void 신규_회원이라면_회원가입에_필요한_정보를_응답한다() { // when - OAuthResponse response = oAuthService.processOAuth(authType, new OAuthCodeRequest(oauthCode)); + OAuthResult oAuthResult = oAuthService.processOAuth(authType, new OAuthCodeRequest(oauthCode)); // then + OAuthResponse response = oAuthResult.response(); assertThat(response).isInstanceOf(SignUpPrepareResponse.class); SignUpPrepareResponse signUpPrepareResponse = (SignUpPrepareResponse) response; assertAll( - () -> assertThat(signUpPrepareResponse.isRegistered()).isFalse(), () -> assertThat(signUpPrepareResponse.signUpToken()).isNotBlank(), () -> assertThat(signUpPrepareResponse.email()).isEqualTo(email), () -> assertThat(signUpPrepareResponse.profileImageUrl()).isEqualTo(profileImageUrl), - () -> assertThat(signUpPrepareResponse.nickname()).isEqualTo(nickname) + () -> assertThat(signUpPrepareResponse.nickname()).isEqualTo(nickname), + () -> assertThat(oAuthResult.refreshToken()).isNull() ); } } diff --git a/src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java index 46c6d565a..f52cb9e3f 100644 --- a/src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/signin/EmailSignInServiceTest.java @@ -4,7 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.example.solidconnection.auth.dto.EmailSignInRequest; -import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -35,12 +35,12 @@ class EmailSignInServiceTest { EmailSignInRequest signInRequest = new EmailSignInRequest(user.getEmail(), rawPassword); // when - SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + SignInResult signInResult = emailSignInService.signIn(signInRequest); // then assertAll( - () -> Assertions.assertThat(signInResponse.accessToken()).isNotNull(), - () -> Assertions.assertThat(signInResponse.refreshToken()).isNotNull() + () -> Assertions.assertThat(signInResult.accessToken()).isNotNull(), + () -> Assertions.assertThat(signInResult.refreshToken()).isNotNull() ); } diff --git a/src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java index 957c5c3a1..7e90de257 100644 --- a/src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/signin/SignInServiceTest.java @@ -5,7 +5,7 @@ import com.example.solidconnection.auth.domain.RefreshToken; import com.example.solidconnection.auth.domain.Subject; -import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignInResult; import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.service.TokenStorage; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -46,16 +46,16 @@ void setUp() { @Test void 성공적으로_로그인한다() { // when - SignInResponse signInResponse = signInService.signIn(user); + SignInResult signInResult = signInService.signIn(user); // then - Subject accessTokenSubject = tokenProvider.parseSubject(signInResponse.accessToken()); - Subject refreshTokenSubject = tokenProvider.parseSubject(signInResponse.refreshToken()); + Subject accessTokenSubject = tokenProvider.parseSubject(signInResult.accessToken()); + Subject refreshTokenSubject = tokenProvider.parseSubject(signInResult.refreshToken()); Optional savedRefreshToken = tokenStorage.findToken(subject, RefreshToken.class); assertAll( () -> assertThat(accessTokenSubject).isEqualTo(subject), () -> assertThat(refreshTokenSubject).isEqualTo(subject), - () -> assertThat(savedRefreshToken).hasValue(signInResponse.refreshToken())); + () -> assertThat(savedRefreshToken).hasValue(signInResult.refreshToken())); } @Test diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java index 08603d556..dc08bf2d4 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java @@ -60,7 +60,7 @@ class MentorQueryServiceTest { @BeforeEach void setUp() { - termFixture.현재_학기("2025-2"); + termFixture.현재_학기("2025-1"); university = universityFixture.그라츠_대학(); } diff --git a/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java b/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java index cdf48a024..05262b6a2 100644 --- a/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java +++ b/src/test/java/com/example/solidconnection/siteuser/fixture/SiteUserFixture.java @@ -97,6 +97,18 @@ public class SiteUserFixture { .create(); } + public SiteUser 신고된_사용자_멘토(int index, String nickname) { + return siteUserFixtureBuilder.siteUser() + .email("reported" + index + "@example.com") + .authType(AuthType.EMAIL) + .nickname(nickname) + .profileImageUrl("profileImageUrl") + .role(Role.MENTOR) + .password("reported123") + .userStatus(UserStatus.REPORTED) + .create(); + } + public SiteUser 차단된_사용자(String nickname) { return siteUserFixtureBuilder.siteUser() .email("banned@example.com") diff --git a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java index 360afc640..89180fe41 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java @@ -114,7 +114,7 @@ class MyPageServiceTest { @BeforeEach void setUp() { user = siteUserFixture.사용자(); - term = termFixture.현재_학기("2025-2"); + term = termFixture.현재_학기("2025-1"); UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); 괌대학_A_지원_정보_ID = 괌대학_A_지원_정보.getId(); 괌대학 = 괌대학_A_지원_정보.getUniversity(); diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index ca16a6f01..5eed9f7fa 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -68,7 +68,7 @@ kakao: redirect-url: "http://localhost:8080/auth/kakao" client-id: client-id token-url: "https://kauth.kakao.com/oauth/token" - user-info_url: "https://kapi.kakao.com/v2/user/me" + user-info-url: "https://kapi.kakao.com/v2/user/me" sentry: environment: test dsn: "https://test-public-key@sentry.test-domain.io/123456"