From 029684a49449effb46ff100f72d422a6d6ffdec4 Mon Sep 17 00:00:00 2001 From: seonghyeok cho <65901319+whqtker@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:52:34 +0900 Subject: [PATCH] =?UTF-8?q?[RELEASE]=20260219=20=EB=A6=B4=EB=A6=AC?= =?UTF-8?q?=EC=A6=88=20(#678)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 파견 대학 테이블명 변경 및 협정 대학 테이블 추가하는 DDL 작성 (#620) * feat: 파견 대학 테이블명 변경 및 협정 대학 테이블 추가하는 DDL 작성 * refactor: 테이블명 변경 및 추가에 따른 엔티티 생성 - 또한 목데이터 생성 로직 수정 * test: 테스트 코드에서 University -> HostUniversity로 변경 * chore: 중복 인덱스 생성 방지를 위해 인덱스 생성 제거 - FK 제약조건을 추가할 때 인덱스가 없다면 알아서 추가하기 때문 * chore: home_university 테이블에 created/updated_at 추가 * refactor: 잘못 설정되었던 테이블 간 연관 관계 재설정 (#622) * refactor: home_university와 university_info_for_apply가 FK 관계를 가지도록 * chore: FK 변경에 따른 목데이터 수정 * test: 테스트 픽스터 수정 * refactor: 대학 검색 응답 수정 (#624) * refactor: home_university와 university_info_for_apply가 FK 관계를 가지도록 * chore: FK 변경에 따른 목데이터 수정 * refactor: 필터 검색 엔드포인트 삭제 * refactor: 필터 검색 관련 서비스 로직 삭제 * refactor: 필터 검색 관련 레포지토리 메서드 삭제 * refactor: 필터 검색 관련 DTO 삭제 * test: 필터 검색 관련 테스트 코드 삭제 * refactor: 지원 대학 관련 응답에 협정 대학 이름 추가 * test: 지원 대학 응답 수정에 따른 테스트 수정 * refactor: 간접 참조 대신 연관관계 추가 - N+1 방지를 위해 fetch join도 추가 * test: 간접 참조 방식에서 연관 관계 설정으로 따른 테스트 코드 수정 * chore: 목데이터에서 지원 대학 테이블에 협정 대학 ID를 설정하도록 * test: home university fixture 추가 * refactor: home university에 대한 fetch join 추가 * refactor: s3 버전 업그레이드 및 로직 수정 (#608) * refactor: s3 sdk 버전 업그레이드 - 의존성 수정 - 버전 업그레이드에 따른 코드 수정 * refactor: 이미지 이외의 파일 관리를 위해 ImgType 의미 명확하도록 수정 - ImgType에서 UploadType으로 변경 - 해당되는 파일 모두 수정 * refactor: s3 테스트 코드 추가 * fix: s3 access-key, secret-key 최신화, 버킷 명칭 올바르게 수정 * fix: ChatService Test 변경점 반영, S3ServiceTest 단위 테스트로 변경 - images->files로 디렉토리 경로 수정 * fix: 이중 비동기 실행문제 해결 - @Async에 전적으로 위임 * refactor: S3Service error 메시지 NPE 가능성 제거 * refactor: 수정사항 반영 - UploadType -> UploadPath로 명칭변경 - 컨벤션 수정(미사용 변수 삭제, 들여쓰기, 명칭변경) * fix: 테스트 코드 오류 수정 - 내부 로직에서 사용하는 fileUploadService 정의 * refactor: 수정사항 반영 - 파일 확장자 상수화 - 확장자 확인로직, 채팅이면 모든 파일 허용, 이미지 확인까지 모두 enum에서 관리 - MultipartFile이 비동기 과정에서 유실되지 않도록 byte로 변환해서 전달 - UrlPrefixResponse PascalCase로 변경 * refactor: 컨벤션 수정 - 사용하지 않는 import문 삭제 * refactor: 리프레시 토큰 만료시 쿠키 삭제 (#628) * refactor: 리프레시 토큰 만료시 쿠키 삭제 * refactor: 인증 전용 예외 생성 * refactor: 멘토링 조회 응답에 mentoringId 필드 추가 (#638) * feat: WebSocket 로깅 인터셉터 작성 (#635) * feat: WebSocket 로깅 인터셉터 작성 * refactor: Principal 명시적 형 변환 대신 null 체크하여 형 변환 * feat: 어드민에서 파견 대학을 관리하도록 (#633) * feat: 파견 대학 CRUD 관련 ErrorCode 추가 - HOST_UNIVERSITY_HAS_REFERENCES : 파견 대학 삭제 시 해당 대학을 참조하는 UnivApplyInfo가 존재하는 경우 * feat: 파견 대학 관련 정보를 업데이트하는 도메인 메서드 작성 * feat: 조회 관련 Repository 메서드 구현 * feat: 파견 대학 검색 관련 QueryDSL로 구현 * feat: 어드민 파견 대학 CRUD 관련 DTO 작성 * feat: country 조회 관련 로직 추가 및 ErrorCode 추가 * feat: 어드민 파견 대학 CRUD 관련 서비스 로직 작성 * feat: 어드민 파견 대학 관련 컨트롤러 작성 * test: 어드민 파견 대학 관리 관련 테스트 작성 * refactor: 엔드포인트의 path variable 이름 변경 - id -> host-university-id * refactor: PageResponse 응답 객체를 사용하도록 * test: 응답 변경에 따른 테스트 코드 수정 * fix: host_university 테이블의 korean_name 필드에 unique key 추가 (#645) * fix: host_university 테이블의 korean_name 필드에 unique key 쿠가 * test: test용 hostUniversityRepository 생성 * test: 고유한 korean_name을 가진 host university 객체를 사용하도록 * fix: 멘토 지원서 승인 시 유저 Role 을 Mentor로 승격 (#639) * fix: 멘토 지원서 승인 시 유저 Role 을 Mentor로 승격 * fix: 멘토 지원서 승인 시 멘토 생성 * fix: 멘토의 introduction, passTip null 허용하도록 수정 - not null 인 필드에 빈문자열로 값을 채우는 것 보다, null 허용이 더 의미 있다 판단하여 null 을 허용하도록 하였습니다. * fix: 사용하지 않는 멘토 생성 api 제거 - 멘토 생성의 주체가 어드민으로 변경되어 Mentor 도메인의 Mentor 생성 api 를 제거 * feat: 멘토 지원서 승인 예외처리 추가 - 중복 멘토 생성 예외 처리 및 테스트 추가 * refactor: Mentor 생성 시 null 전달 제거 * refactor: 멘토 지원서 승낙 시, 검증 후 승격 및 멘토 생성 * chore: 스크립트 버전 수정 (#651) * chore: 스크립트 버전 수정 * test: korean_name 컬럼 UK 관련 테스트 코드 수정 * feat: test skill 추가 (#647) * feat: serena MCP 추가 * feat: test skill 추가 * feat: hook 추가 - 응답 대기시 알람발송 - 컨벤션 어겼을 때 훅 작동 * feat: 안쓰는 파일 제거 * fix: 게시글 중복 생성 방지 (#649) * fix: 게시글 중복 생성 방지 - Redis 패키지 및 로직 정리 * fix: 게시글 중복 생성 방지 - 게시글 중복 요청 방지 Redis 로직 추가 * refactor: 게시글 중복 생성 방지 * chore: testcontainer 버전 업 (#659) * chore: windows에서도 hook이 동작하도록 (#655) * refactor: 오래된 이미지 삭제 후 이미지 pull하도록 변경 (#653) refactor: 오래된 이미지 삭제 후 이미지 pull하도록 변경 (#653) - 추가로 이미지는 5개 -> 2개 보관하도록 변경 * refactor: 멘토 도메인 응답의 사용자 id를 siteUserId로 통일 (#665) * refactor: 멘토 관련 id응답은 모두 site-user-id가 되도록 수정 * test: 멘토 관련 테스트 코드 수정 * refactor: 채팅 도메인 응답의 사용자 관련 id를 siteUserId로 통일 (#666) * refactor: 채팅 관련 응답에서 사용자 관련 Id를 siteUserId로 통일 * refactor: siteUserId를 포함하도록 서비스 코드 수정 * test: 사용자 id로 응답 통일 관련 테스트 수정 * feat: 전체 뉴스를 조회하는 API 구현 (#674) * feat: 전체 news 조회 API 구현 - 기존 API에 author-id를 선택적으로 받도록 * test: 전체 news 조회 관련 테스트 코드 작성 * refactor: 날짜 오름차순으로 news 조회하는 JPA 메서드 추가 * refactor: 뉴스 조회 API를 하나로 통합 - 서비스 계층에서 siteUserId == null을 기준으로 분기하도록 * refactor: 컨트롤러 계층에서 분기문 제거 - 분기를 서비스 계층에게 위임했음 * test: 뉴스 조회 관련 테스트 코드 수정 * chore: 누락된 제약 조건을 추가하는 스크립트 작성 (#676) --------- Co-authored-by: Yeon <84384499+lsy1307@users.noreply.github.com> Co-authored-by: 황규혁 <126947828+Gyuhyeok99@users.noreply.github.com> Co-authored-by: hyungjun <115551339+sukangpunch@users.noreply.github.com> Co-authored-by: 정재희 --- .claude/hooks/notify.ps1 | 9 + .claude/hooks/notify.py | 19 + .claude/hooks/post-edit-check.py | 48 +++ .claude/settings.json | 29 ++ .claude/skills/test/SKILL.md | 247 +++++++++++ .gitignore | 3 + .serena/.gitignore | 1 + .serena/project.yml | 112 +++++ build.gradle | 3 +- claude.md | 245 +---------- .../AdminMentorApplicationService.java | 31 +- .../AdminHostUniversityController.java | 71 ++++ .../dto/AdminHostUniversityCreateRequest.java | 46 ++ .../AdminHostUniversityDetailResponse.java | 40 ++ .../dto/AdminHostUniversityResponse.java | 30 ++ .../AdminHostUniversitySearchCondition.java | 9 + .../dto/AdminHostUniversityUpdateRequest.java | 46 ++ .../service/AdminHostUniversityService.java | 152 +++++++ .../auth/controller/AuthController.java | 4 +- .../auth/exception/AuthException.java | 15 + .../auth/service/AuthService.java | 3 +- .../cache/ThunderingHerdCachingAspect.java | 8 +- .../chat/config/StompWebSocketConfig.java | 9 +- .../config/WebSocketLoggingInterceptor.java | 55 +++ .../chat/dto/ChatMessageResponse.java | 6 +- .../chat/dto/ChatMessageSendResponse.java | 6 +- .../chat/dto/ChatParticipantResponse.java | 6 +- .../chat/service/ChatService.java | 27 +- .../common/config/redis/RedisConfig.java | 2 +- .../common/constant/FileConstants.java | 25 ++ .../common/exception/CustomException.java | 11 +- .../exception/CustomExceptionHandler.java | 23 + .../common/exception/ErrorCode.java | 4 + .../post/service/PostCommandService.java | 18 +- .../post/service/PostQueryService.java | 9 +- .../post/service/PostRedisManager.java | 58 +++ .../post/service/UpdateViewCountService.java | 10 +- .../country/repository/CountryRepository.java | 3 + .../mentor/controller/MentorController.java | 6 +- .../controller/MentorMyPageController.java | 12 - .../solidconnection/mentor/domain/Mentor.java | 21 +- .../mentor/dto/MatchedMentorResponse.java | 6 +- .../mentor/dto/MentorDetailResponse.java | 6 +- .../mentor/dto/MentorMyPageCreateRequest.java | 20 - .../mentor/dto/MentorMyPageResponse.java | 6 +- .../mentor/dto/MentorPreviewResponse.java | 6 +- .../dto/MentoringForMentorResponse.java | 2 + .../MentorBatchQueryRepository.java | 16 +- .../mentor/repository/MentorRepository.java | 2 +- ...MentorApplicationFilterRepositoryImpl.java | 18 +- .../service/MentorApplicationService.java | 4 +- .../mentor/service/MentorMyPageService.java | 48 +-- .../mentor/service/MentorQueryService.java | 18 +- .../service/MentoringCommandService.java | 8 +- .../mentor/service/MentoringQueryService.java | 6 +- .../news/service/NewsCommandService.java | 6 +- .../service => redis}/RedisConstants.java | 7 +- .../post/service => redis}/RedisService.java | 12 +- .../s3/config/AmazonS3Config.java | 20 +- .../s3/controller/S3Controller.java | 22 +- .../solidconnection/s3/domain/ImgType.java | 21 - .../solidconnection/s3/domain/UploadPath.java | 46 ++ ...ixResponse.java => UrlPrefixResponse.java} | 2 +- .../s3/service/FileUploadService.java | 55 ++- .../solidconnection/s3/service/S3Service.java | 87 ++-- .../scheduler/UpdateViewCountScheduler.java | 2 +- .../score/service/ScoreService.java | 6 +- .../siteuser/domain/SiteUser.java | 4 + .../siteuser/service/MyPageService.java | 13 +- .../controller/UnivApplyInfoController.java | 11 - .../university/domain/HomeUniversity.java | 26 ++ .../{University.java => HostUniversity.java} | 30 +- .../university/domain/UnivApplyInfo.java | 7 +- .../dto/UnivApplyInfoDetailResponse.java | 4 +- .../dto/UnivApplyInfoFilterSearchRequest.java | 15 - .../dto/UnivApplyInfoPreviewResponse.java | 8 +- .../repository/HomeUniversityRepository.java | 13 + .../repository/HostUniversityRepository.java | 19 + .../LikedUnivApplyInfoRepository.java | 7 +- .../repository/UnivApplyInfoRepository.java | 5 + .../repository/UniversityRepository.java | 15 - .../HostUniversityFilterRepository.java | 15 + .../HostUniversityFilterRepositoryImpl.java | 88 ++++ .../custom/UnivApplyInfoFilterRepository.java | 3 - .../UnivApplyInfoFilterRepositoryImpl.java | 76 +--- .../service/LikedUnivApplyInfoService.java | 3 +- .../service/UnivApplyInfoQueryService.java | 31 +- .../UnivApplyInfoRecommendService.java | 15 +- .../solidconnection/util/RedisUtils.java | 18 +- src/main/resources/data.sql | 48 ++- ..._university_and_create_home_university.sql | 25 ++ ..._home_university_fk_to_univ_apply_info.sql | 12 + ...straint_to_host_university_korean_name.sql | 2 + ..._mentor_introduction_pass_tip_nullable.sql | 5 + .../AdminHostUniversityServiceTest.java | 401 ++++++++++++++++++ .../AdminMentorApplicationServiceTest.java | 80 +++- .../service/ApplicationQueryServiceTest.java | 24 +- .../ApplicationSubmissionServiceTest.java | 10 +- .../auth/service/AuthServiceTest.java | 4 +- .../chat/service/ChatServiceTest.java | 18 +- .../post/service/PostCommandServiceTest.java | 12 +- .../post/service/PostQueryServiceTest.java | 8 +- .../PostCreateConcurrencyTest.java | 149 +++++++ .../PostViewCountConcurrencyTest.java | 22 +- .../MentorBatchQueryRepositoryTest.java | 6 +- .../service/MentorApplicationServiceTest.java | 12 +- .../service/MentorMyPageServiceTest.java | 120 +----- .../service/MentorQueryServiceTest.java | 28 +- .../service/MentoringCommandServiceTest.java | 4 +- .../service/MentoringQueryServiceTest.java | 8 +- .../news/service/NewsCommandServiceTest.java | 14 +- .../s3/service/S3ServiceTest.java | 114 +++++ .../score/service/ScoreServiceTest.java | 6 +- .../siteuser/service/MyPageServiceTest.java | 33 +- .../fixture/HomeUniversityFixture.java | 18 + .../fixture/HomeUniversityFixtureBuilder.java | 29 ++ .../fixture/UnivApplyInfoFixture.java | 18 +- .../fixture/UnivApplyInfoFixtureBuilder.java | 15 +- .../university/fixture/UniversityFixture.java | 31 +- .../fixture/UniversityFixtureBuilder.java | 14 +- .../HostUniversityRepositoryForTest.java | 7 + ...eralUnivApplyInfoRecommendServiceTest.java | 2 +- .../UnivApplyInfoQueryServiceTest.java | 95 +---- .../UnivApplyInfoRecommendServiceTest.java | 38 +- 124 files changed, 2658 insertions(+), 1134 deletions(-) create mode 100644 .claude/hooks/notify.ps1 create mode 100644 .claude/hooks/notify.py create mode 100755 .claude/hooks/post-edit-check.py create mode 100644 .claude/settings.json create mode 100644 .claude/skills/test/SKILL.md create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java create mode 100644 src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java create mode 100644 src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java create mode 100644 src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java create mode 100644 src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java create mode 100644 src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java create mode 100644 src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java create mode 100644 src/main/java/com/example/solidconnection/auth/exception/AuthException.java create mode 100644 src/main/java/com/example/solidconnection/chat/config/WebSocketLoggingInterceptor.java create mode 100644 src/main/java/com/example/solidconnection/common/constant/FileConstants.java create mode 100644 src/main/java/com/example/solidconnection/community/post/service/PostRedisManager.java delete mode 100644 src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageCreateRequest.java rename src/main/java/com/example/solidconnection/{community/post/service => redis}/RedisConstants.java (77%) rename src/main/java/com/example/solidconnection/{community/post/service => redis}/RedisService.java (76%) delete mode 100644 src/main/java/com/example/solidconnection/s3/domain/ImgType.java create mode 100644 src/main/java/com/example/solidconnection/s3/domain/UploadPath.java rename src/main/java/com/example/solidconnection/s3/dto/{urlPrefixResponse.java => UrlPrefixResponse.java} (83%) create mode 100644 src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java rename src/main/java/com/example/solidconnection/university/domain/{University.java => HostUniversity.java} (59%) delete mode 100644 src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoFilterSearchRequest.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java delete mode 100644 src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java create mode 100644 src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java create mode 100644 src/main/resources/db/migration/V42__rename_university_to_host_university_and_create_home_university.sql create mode 100644 src/main/resources/db/migration/V43__move_home_university_fk_to_univ_apply_info.sql create mode 100644 src/main/resources/db/migration/V44__add_unique_constraint_to_host_university_korean_name.sql create mode 100644 src/main/resources/db/migration/V45__modify_mentor_introduction_pass_tip_nullable.sql create mode 100644 src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/concurrency/PostCreateConcurrencyTest.java create mode 100644 src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java create mode 100644 src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java create mode 100644 src/test/java/com/example/solidconnection/university/repository/HostUniversityRepositoryForTest.java diff --git a/.claude/hooks/notify.ps1 b/.claude/hooks/notify.ps1 new file mode 100644 index 000000000..20b49a249 --- /dev/null +++ b/.claude/hooks/notify.ps1 @@ -0,0 +1,9 @@ +Add-Type -AssemblyName System.Windows.Forms +$n = New-Object System.Windows.Forms.NotifyIcon +$n.Icon = [System.Drawing.SystemIcons]::Information +$n.Visible = $true +$n.BalloonTipTitle = "Claude Code" +$n.BalloonTipText = "Awaiting your input" +$n.ShowBalloonTip(5000) +Start-Sleep -Milliseconds 5100 +$n.Dispose() diff --git a/.claude/hooks/notify.py b/.claude/hooks/notify.py new file mode 100644 index 000000000..d839ea66f --- /dev/null +++ b/.claude/hooks/notify.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import os +import platform +import subprocess + +system = platform.system() +script_dir = os.path.dirname(os.path.abspath(__file__)) + +if system == "Darwin": + subprocess.run([ + "osascript", "-e", + 'display notification "Awaiting your input" with title "Claude Code"' + ]) +elif system == "Windows": + ps1_path = os.path.join(script_dir, "notify.ps1") + subprocess.run([ + "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", + "-File", ps1_path + ]) diff --git a/.claude/hooks/post-edit-check.py b/.claude/hooks/post-edit-check.py new file mode 100755 index 000000000..0b5c1b14c --- /dev/null +++ b/.claude/hooks/post-edit-check.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import json +import sys +import re + +data = json.load(sys.stdin) +file_path = data.get("tool_input", {}).get("file_path", "") + +if not file_path.endswith(".java") or not file_path: + sys.exit(0) + +try: + with open(file_path) as f: + content = f.read() + lines = content.split("\n") +except Exception: + sys.exit(0) + +warnings = [] + +# 1. 와일드카드 import 체크 +for i, line in enumerate(lines, 1): + if re.match(r"\s*import\s+.*\.\*;", line): + warnings.append(f"L{i}: 와일드카드 import 발견 -> 명시적 import 필요") + +# 2. 파일 끝 줄바꿈 체크 +if content and not content.endswith("\n"): + warnings.append("파일 끝 줄바꿈 누락") + +# 3. Entity 클래스의 @Column 체크 +if "@Entity" in content: + field_pattern = re.compile(r"^\s+private\s+\w+(?:<[^>]+>)?\s+\w+;") + relation_annotations = { + "@Column", "@Id", "@ManyToOne", "@OneToMany", + "@JoinColumn", "@OneToOne", "@ManyToMany", + "@Transient", "@Version", "@Embedded", "@EmbeddedId", + } + for i, line in enumerate(lines): + if field_pattern.match(line): + preceding = "\n".join(lines[max(0, i - 5):i]) + has_annotation = any(ann in preceding for ann in relation_annotations) + if not has_annotation: + warnings.append(f"L{i + 1}: Entity 필드에 @Column 누락 가능성: {line.strip()}") + +if warnings: + print(f"[컨벤션 체크 - {file_path.split('/')[-1]}]") + for w in warnings: + print(f" - {w}") diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..f6c5b8ec9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,29 @@ +{ + "env": { + "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" + }, + "hooks": { + "Notification": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/notify.py 2>/dev/null || python .claude/hooks/notify.py" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python3 .claude/hooks/post-edit-check.py 2>/dev/null || python .claude/hooks/post-edit-check.py" + } + ] + } + ] + } +} diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md new file mode 100644 index 000000000..c23122578 --- /dev/null +++ b/.claude/skills/test/SKILL.md @@ -0,0 +1,247 @@ +--- +name: test +description: 테스트 코드를 작성하거나 수정할 때 이 프로젝트의 테스트 컨벤션과 패턴을 참고합니다 +--- + +# 테스트 코드 작성 가이드 + +## 테스트 기본 설정 + +모든 통합 테스트는 `@TestContainerSpringBootTest` 어노테이션을 사용합니다. + +```java +@TestContainerSpringBootTest +@DisplayName("채팅 서비스 테스트") +class ChatServiceTest { + // 테스트 코드 +} +``` + +**제공 기능:** +- MySQL, Redis 자동 실행 +- Spring Boot 컨텍스트 로드 +- 테스트 후 자동 DB 초기화 +- JUnit 5 기반 + +## Fixture 패턴 + +테스트 데이터는 Fixture로 생성합니다 (FixtureBuilder + Fixture 패턴). + +**위치:** `src/test/java/com/example/solidconnection/[domain]/fixture/` + +``` +fixture/ +├── [Entity]FixtureBuilder.java # Builder 패턴 구현 +└── [Entity]Fixture.java # 편의 메서드 제공 +``` + +### 예제: ChatRoomFixtureBuilder + +```java +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixtureBuilder { + + private final ChatRoomRepository chatRoomRepository; + + private boolean isGroup; + private Long mentoringId; + + public ChatRoomFixtureBuilder chatRoom() { + return new ChatRoomFixtureBuilder(chatRoomRepository); + } + + public ChatRoomFixtureBuilder isGroup(boolean isGroup) { + this.isGroup = isGroup; + return this; + } + + public ChatRoomFixtureBuilder mentoringId(long mentoringId) { + this.mentoringId = mentoringId; + return this; + } + + public ChatRoom create() { + ChatRoom chatRoom = new ChatRoom(mentoringId, isGroup); + return chatRoomRepository.save(chatRoom); // DB 저장 + } +} +``` + +### 예제: ChatRoomFixture + +```java +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixture { + + private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; + + // 편의 메서드: 기본값으로 생성 + public ChatRoom 채팅방(boolean isGroup) { + return chatRoomFixtureBuilder.chatRoom() + .isGroup(isGroup) + .create(); + } + + public ChatRoom 멘토링_채팅방(long mentoringId) { + return chatRoomFixtureBuilder.chatRoom() + .mentoringId(mentoringId) + .isGroup(false) + .create(); + } +} +``` + +**편의 메서드 작성 팁:** + +- 한국어 메서드명 사용 (가독성) +- 자주 사용되는 기본값 조합만 제공 +- Builder를 조합하여 필요한 데이터 설정 + +### 테스트에서 사용 + +```java +@TestContainerSpringBootTest +class ChatServiceTest { + + @Autowired + private ChatRoomFixture chatRoomFixture; + + @Test + void 채팅방을_생성할_수_있다() { + // 편의 메서드 사용 + ChatRoom room = chatRoomFixture.채팅방(false); + + // Builder 직접 사용 + ChatRoom customRoom = chatRoomFixture.chatRoomFixtureBuilder.chatRoom() + .isGroup(true) + .mentoringId(100L) + .create(); + } +} +``` + +## 테스트 네이밍 컨벤션 + +### 테스트 메서드 네이밍 규칙 + +테스트 메서드명은 **한국어로 명확하게** 작성하며, 다음 패턴을 따릅니다: + +#### 1. 정상 동작 테스트 + +```java +// 패턴: 어떤_것을_하면_어떤_결과가_나온다 +@Test +void 채팅방이_없으면_빈_목록을_반환한다() { ... } + +@Test +void 최신_메시지_순으로_정렬되어_조회한다() { ... } + +@Test +void 참여자는_메시지를_전송할_수_있다() { ... } + +@Test +void 페이징이_정상_작동한다() { ... } +``` + +#### 2. 예외 테스트 + +```java +// 패턴: 어떤_것을_하면_예외_응답을_반환한다 +@Test +void 참여하지_않은_채팅방에_접근하면_예외_응답을_반환한다() { ... } + +@Test +void 존재하지_않는_사용자로_메시지를_전송하면_예외_응답을_반환한다() { ... } + +@Test +void 권한이_없으면_예외_응답을_반환한다() { ... } + +@Test +void 필수_파라미터가_없으면_예외_응답을_반환한다() { ... } +``` + +## BDD 테스트 작성 + +테스트는 Given-When-Then 구조로 작성합니다. + +```java +@Test +@DisplayName("최신 메시지순으로 채팅방 목록을 조회한다") +void 최신_메시지_순으로_조회한다() { + // Given: 테스트 사전 조건 + SiteUser user = siteUserFixture.사용자(); + ChatRoom room1 = chatRoomFixture.채팅방(false); + ChatRoom room2 = chatRoomFixture.채팅방(false); + chatMessageFixture.메시지("오래된 메시지", user.getId(), room1); + chatMessageFixture.메시지("최신 메시지", user.getId(), room2); + + // When: 실제 동작 + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // Then: 결과 검증 + assertAll( + () -> assertThat(response.chatRooms()).hasSize(2), + () -> assertThat(response.chatRooms().get(0).id()).isEqualTo(room2.getId()) + ); +} +``` + +## 테스트 그룹화 (@Nested) + +기능별로 테스트를 그룹화합니다. + +```java +@TestContainerSpringBootTest +class ChatServiceTest { + + @Nested + @DisplayName("채팅방 목록 조회") + class 채팅방_목록을_조회한다 { + + @Test + void 빈_목록을_반환한다() { ... } + + @Test + void 최신_메시지_순으로_조회한다() { ... } + } + + @Nested + @DisplayName("채팅 메시지 전송") + class 채팅_메시지를_전송한다 { + + @BeforeEach + void setUp() { + // 이 그룹에만 적용되는 초기 설정 + } + + @Test + void 참여자는_메시지를_전송할_수_있다() { ... } + } +} +``` + +## 자주 사용하는 Assertion + +```java +// 기본 검증 +assertThat(value).isEqualTo(expected); +assertThat(value).isNotNull(); + +// 컬렉션 +assertThat(list).hasSize(3); +assertThat(list).isEmpty(); +assertThat(list).contains(item); + +// 예외 검증 +assertThatCode(() -> method()) + .isInstanceOf(CustomException.class) + .hasMessage("error message"); + +// 복수 검증 +assertAll( + () -> assertThat(a).isEqualTo(1), + () -> assertThat(b).isEqualTo(2) +); +``` diff --git a/.gitignore b/.gitignore index d5df4047a..7a58382b4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ out/ ### VS Code ### .vscode/ +### Claude Code ### +.claude/settings.local.json + ### YML ### application-secret.yml application-prod.yml diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 000000000..14d86ad62 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 000000000..2f196bdbc --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,112 @@ +# the name by which the project can be referenced within Serena +project_name: "solid-connect-server" + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# powershell python python_jedi r rego +# ruby ruby_solargraph rust scala swift +# terraform toml typescript typescript_vts vue +# yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- java + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore in all projects +# same syntax as gitignore, so you can use * and ** +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" diff --git a/build.gradle b/build.gradle index 72f4eccba..cea20dd7a 100644 --- a/build.gradle +++ b/build.gradle @@ -65,8 +65,9 @@ dependencies { testImplementation 'org.awaitility:awaitility:4.2.0' // Etc + implementation platform('software.amazon.awssdk:bom:2.41.4') + implementation 'software.amazon.awssdk:s3' implementation 'org.hibernate.validator:hibernate-validator' - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782' implementation 'org.springframework.boot:spring-boot-starter-websocket' // Database Proxy diff --git a/claude.md b/claude.md index ddd2cb9b4..af2e96a4d 100644 --- a/claude.md +++ b/claude.md @@ -196,248 +196,11 @@ public class UserCreateResponse { ... } ## 테스트 코드 작성 -### 테스트 기본 설정 +테스트 작성 시 `/test` skill을 참고하세요. (테스트 관련 작업 시 자동으로 로드됩니다) -모든 통합 테스트는 `@TestContainerSpringBootTest` 어노테이션을 사용합니다. - -```java -@TestContainerSpringBootTest -@DisplayName("채팅 서비스 테스트") -class ChatServiceTest { - // 테스트 코드 -} -``` - -**제공 기능:** -- MySQL, Redis 자동 실행 -- Spring Boot 컨텍스트 로드 -- 테스트 후 자동 DB 초기화 -- JUnit 5 기반 - -### Fixture 패턴 - -테스트 데이터는 Fixture로 생성합니다 (FixtureBuilder + Fixture 패턴). - -**위치:** `src/test/java/com/example/solidconnection/[domain]/fixture/` - -``` -fixture/ -├── [Entity]FixtureBuilder.java # Builder 패턴 구현 -└── [Entity]Fixture.java # 편의 메서드 제공 -``` - -#### 예제: ChatRoomFixtureBuilder - -```java -@TestComponent -@RequiredArgsConstructor -public class ChatRoomFixtureBuilder { - - private final ChatRoomRepository chatRoomRepository; - - private boolean isGroup; - private Long mentoringId; - - public ChatRoomFixtureBuilder chatRoom() { - return new ChatRoomFixtureBuilder(chatRoomRepository); - } - - public ChatRoomFixtureBuilder isGroup(boolean isGroup) { - this.isGroup = isGroup; - return this; - } - - public ChatRoomFixtureBuilder mentoringId(long mentoringId) { - this.mentoringId = mentoringId; - return this; - } - - public ChatRoom create() { - ChatRoom chatRoom = new ChatRoom(mentoringId, isGroup); - return chatRoomRepository.save(chatRoom); // DB 저장 - } -} -``` - -#### 예제: ChatRoomFixture - -```java -@TestComponent -@RequiredArgsConstructor -public class ChatRoomFixture { - - private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; - - // 편의 메서드: 기본값으로 생성 - public ChatRoom 채팅방(boolean isGroup) { - return chatRoomFixtureBuilder.chatRoom() - .isGroup(isGroup) - .create(); - } - - public ChatRoom 멘토링_채팅방(long mentoringId) { - return chatRoomFixtureBuilder.chatRoom() - .mentoringId(mentoringId) - .isGroup(false) - .create(); - } -} -``` - -**편의 메서드 작성 팁:** - -- 한국어 메서드명 사용 (가독성) -- 자주 사용되는 기본값 조합만 제공 -- Builder를 조합하여 필요한 데이터 설정 - - -#### 테스트에서 사용 - -```java -@TestContainerSpringBootTest -class ChatServiceTest { - - @Autowired - private ChatRoomFixture chatRoomFixture; - - @Test - void 채팅방을_생성할_수_있다() { - // 편의 메서드 사용 - ChatRoom room = chatRoomFixture.채팅방(false); - - // Builder 직접 사용 - ChatRoom customRoom = chatRoomFixture.chatRoomFixtureBuilder.chatRoom() - .isGroup(true) - .mentoringId(100L) - .create(); - } -} -``` - -## 테스트 네이밍 컨벤션 - -### 테스트 메서드 네이밍 규칙 - -테스트 메서드명은 **한국어로 명확하게** 작성하며, 다음 패턴을 따릅니다: - -#### 1. 정상 동작 테스트 - -```java -// 패턴: 어떤_것을_하면_어떤_결과가_나온다 -@Test -void 채팅방이_없으면_빈_목록을_반환한다() { ... } - -@Test -void 최신_메시지_순으로_정렬되어_조회한다() { ... } - -@Test -void 참여자는_메시지를_전송할_수_있다() { ... } - -@Test -void 페이징이_정상_작동한다() { ... } -``` - -#### 2. 예외 테스트 - -```java -// 패턴: 어떤_것을_하면_예외_응답을_반환한다 -@Test -void 참여하지_않은_채팅방에_접근하면_예외_응답을_반환한다() { ... } - -@Test -void 존재하지_않는_사용자로_메시지를_전송하면_예외_응답을_반환한다() { ... } - -@Test -void 권한이_없으면_예외_응답을_반환한다() { ... } - -@Test -void 필수_파라미터가_없으면_예외_응답을_반환한다() { ... } -``` - - -### BDD 테스트 작성 - -테스트는 Given-When-Then 구조로 작성합니다. - -```java -@Test -@DisplayName("최신 메시지순으로 채팅방 목록을 조회한다") -void 최신_메시지_순으로_조회한다() { - // Given: 테스트 사전 조건 - SiteUser user = siteUserFixture.사용자(); - ChatRoom room1 = chatRoomFixture.채팅방(false); - ChatRoom room2 = chatRoomFixture.채팅방(false); - chatMessageFixture.메시지("오래된 메시지", user.getId(), room1); - chatMessageFixture.메시지("최신 메시지", user.getId(), room2); - - // When: 실제 동작 - ChatRoomListResponse response = chatService.getChatRooms(user.getId()); - - // Then: 결과 검증 - assertAll( - () -> assertThat(response.chatRooms()).hasSize(2), - () -> assertThat(response.chatRooms().get(0).id()).isEqualTo(room2.getId()) - ); -} -``` - -### 테스트 그룹화 (@Nested) - -기능별로 테스트를 그룹화합니다. - -```java -@TestContainerSpringBootTest -class ChatServiceTest { - - @Nested - @DisplayName("채팅방 목록 조회") - class 채팅방_목록을_조회한다 { - - @Test - void 빈_목록을_반환한다() { ... } - - @Test - void 최신_메시지_순으로_조회한다() { ... } - } - - @Nested - @DisplayName("채팅 메시지 전송") - class 채팅_메시지를_전송한다 { - - @BeforeEach - void setUp() { - // 이 그룹에만 적용되는 초기 설정 - } - - @Test - void 참여자는_메시지를_전송할_수_있다() { ... } - } -} -``` - -### 자주 사용하는 Assertion - -```java -// 기본 검증 -assertThat(value).isEqualTo(expected); -assertThat(value).isNotNull(); - -// 컬렉션 -assertThat(list).hasSize(3); -assertThat(list).isEmpty(); -assertThat(list).contains(item); - -// 예외 검증 -assertThatCode(() -> method()) - .isInstanceOf(CustomException.class) - .hasMessage("error message"); - -// 복수 검증 -assertAll( - () -> assertThat(a).isEqualTo(1), - () -> assertThat(b).isEqualTo(2) -); -``` +- `@TestContainerSpringBootTest` 기반 통합 테스트 +- FixtureBuilder + Fixture 패턴으로 테스트 데이터 생성 +- 한국어 메서드명, Given-When-Then 구조, @Nested 그룹화 --- diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java b/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java index 86d8a0398..8289a0f7f 100644 --- a/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.admin.service; +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; @@ -9,13 +10,15 @@ import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition; import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse; import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.mentor.domain.MentorApplication; import com.example.solidconnection.mentor.domain.MentorApplicationStatus; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; +import com.example.solidconnection.mentor.repository.MentorRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.university.domain.University; -import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepository; import java.util.List; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; @@ -29,8 +32,9 @@ public class AdminMentorApplicationService { private final MentorApplicationRepository mentorApplicationRepository; - private final UniversityRepository universityRepository; + private final HostUniversityRepository hostUniversityRepository; private final SiteUserRepository siteUserRepository; + private final MentorRepository mentorRepository; @Transactional(readOnly = true) public Page searchMentorApplications( @@ -45,7 +49,26 @@ public void approveMentorApplication(Long mentorApplicationId) { MentorApplication mentorApplication = mentorApplicationRepository.findById(mentorApplicationId) .orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND)); + SiteUser siteUser = siteUserRepository.findById(mentorApplication.getSiteUserId()) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + validateUserCanCreateMentor(siteUser.getId()); + mentorApplication.approve(); + siteUser.becomeMentor(); + + Mentor mentor = new Mentor( + siteUser.getId(), + mentorApplication.getUniversityId(), + mentorApplication.getTermId() + ); + + mentorRepository.save(mentor); + } + + private void validateUserCanCreateMentor(long siteUserId) { + if (mentorRepository.existsBySiteUserId(siteUserId)) { + throw new CustomException(MENTOR_ALREADY_EXISTS); + } } @Transactional @@ -82,7 +105,7 @@ public void assignUniversity( mentorApplication.validateCanAssignUniversity(); - University university = universityRepository.getUniversityById(universityId); + HostUniversity university = hostUniversityRepository.getHostUniversityById(universityId); mentorApplication.assignUniversity(university.getId()); } diff --git a/src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java b/src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java new file mode 100644 index 000000000..57035537d --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java @@ -0,0 +1,71 @@ +package com.example.solidconnection.admin.university.controller; + +import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; +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.admin.university.service.AdminHostUniversityService; +import com.example.solidconnection.common.response.PageResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +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; + +@RequiredArgsConstructor +@RequestMapping("/admin/host-universities") +@RestController +public class AdminHostUniversityController { + + private final AdminHostUniversityService adminHostUniversityService; + + @GetMapping + public ResponseEntity> getHostUniversities( + AdminHostUniversitySearchCondition condition, + @PageableDefault(size = 20) Pageable pageable + ) { + return ResponseEntity.ok(PageResponse.of(adminHostUniversityService.getHostUniversities(condition, pageable))); + } + + @GetMapping("/{host-university-id}") + public ResponseEntity getHostUniversity( + @PathVariable("host-university-id") Long hostUniversityId + ) { + AdminHostUniversityDetailResponse response = adminHostUniversityService.getHostUniversity(hostUniversityId); + return ResponseEntity.ok(response); + } + + @PostMapping + public ResponseEntity createHostUniversity( + @Valid @RequestBody AdminHostUniversityCreateRequest request + ) { + AdminHostUniversityDetailResponse response = adminHostUniversityService.createHostUniversity(request); + return ResponseEntity.ok(response); + } + + @PutMapping("/{host-university-id}") + public ResponseEntity updateHostUniversity( + @PathVariable("host-university-id") Long hostUniversityId, + @Valid @RequestBody AdminHostUniversityUpdateRequest request + ) { + AdminHostUniversityDetailResponse response = adminHostUniversityService.updateHostUniversity(hostUniversityId, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{host-university-id}") + public ResponseEntity deleteHostUniversity( + @PathVariable("host-university-id") Long hostUniversityId + ) { + adminHostUniversityService.deleteHostUniversity(hostUniversityId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java new file mode 100644 index 000000000..6b77061b6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.admin.university.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminHostUniversityCreateRequest( + @NotBlank(message = "한글 대학명은 필수입니다") + @Size(max = 100, message = "한글 대학명은 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "영문 대학명은 필수입니다") + @Size(max = 100, message = "영문 대학명은 100자 이하여야 합니다") + String englishName, + + @NotBlank(message = "표시 대학명은 필수입니다") + @Size(max = 100, message = "표시 대학명은 100자 이하여야 합니다") + String formatName, + + @Size(max = 500, message = "홈페이지 URL은 500자 이하여야 합니다") + String homepageUrl, + + @Size(max = 500, message = "영어 강좌 URL은 500자 이하여야 합니다") + String englishCourseUrl, + + @Size(max = 500, message = "숙소 URL은 500자 이하여야 합니다") + String accommodationUrl, + + @NotBlank(message = "로고 이미지 URL은 필수입니다") + @Size(max = 500, message = "로고 이미지 URL은 500자 이하여야 합니다") + String logoImageUrl, + + @NotBlank(message = "배경 이미지 URL은 필수입니다") + @Size(max = 500, message = "배경 이미지 URL은 500자 이하여야 합니다") + String backgroundImageUrl, + + @Size(max = 1000, message = "상세 정보는 1000자 이하여야 합니다") + String detailsForLocal, + + @NotBlank(message = "국가 코드는 필수입니다") + String countryCode, + + @NotBlank(message = "지역 코드는 필수입니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java new file mode 100644 index 000000000..1630f5066 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.admin.university.dto; + +import com.example.solidconnection.university.domain.HostUniversity; + +public record AdminHostUniversityDetailResponse( + Long id, + String koreanName, + String englishName, + String formatName, + String homepageUrl, + String englishCourseUrl, + String accommodationUrl, + String logoImageUrl, + String backgroundImageUrl, + String detailsForLocal, + String countryCode, + String countryKoreanName, + String regionCode, + String regionKoreanName +) { + + public static AdminHostUniversityDetailResponse from(HostUniversity hostUniversity) { + return new AdminHostUniversityDetailResponse( + hostUniversity.getId(), + hostUniversity.getKoreanName(), + hostUniversity.getEnglishName(), + hostUniversity.getFormatName(), + hostUniversity.getHomepageUrl(), + hostUniversity.getEnglishCourseUrl(), + hostUniversity.getAccommodationUrl(), + hostUniversity.getLogoImageUrl(), + hostUniversity.getBackgroundImageUrl(), + hostUniversity.getDetailsForLocal(), + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getCode() : null, + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getKoreanName() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getCode() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getKoreanName() : null + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java new file mode 100644 index 000000000..12975c0a5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.admin.university.dto; + +import com.example.solidconnection.university.domain.HostUniversity; + +public record AdminHostUniversityResponse( + Long id, + String koreanName, + String englishName, + String formatName, + String logoImageUrl, + String countryCode, + String countryKoreanName, + String regionCode, + String regionKoreanName +) { + + public static AdminHostUniversityResponse from(HostUniversity hostUniversity) { + return new AdminHostUniversityResponse( + hostUniversity.getId(), + hostUniversity.getKoreanName(), + hostUniversity.getEnglishName(), + hostUniversity.getFormatName(), + hostUniversity.getLogoImageUrl(), + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getCode() : null, + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getKoreanName() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getCode() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getKoreanName() : null + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java new file mode 100644 index 000000000..cbf13ec56 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.admin.university.dto; + +public record AdminHostUniversitySearchCondition( + String keyword, + String countryCode, + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java new file mode 100644 index 000000000..cb2e64a74 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.admin.university.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminHostUniversityUpdateRequest( + @NotBlank(message = "한글 대학명은 필수입니다") + @Size(max = 100, message = "한글 대학명은 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "영문 대학명은 필수입니다") + @Size(max = 100, message = "영문 대학명은 100자 이하여야 합니다") + String englishName, + + @NotBlank(message = "표시 대학명은 필수입니다") + @Size(max = 100, message = "표시 대학명은 100자 이하여야 합니다") + String formatName, + + @Size(max = 500, message = "홈페이지 URL은 500자 이하여야 합니다") + String homepageUrl, + + @Size(max = 500, message = "영어 강좌 URL은 500자 이하여야 합니다") + String englishCourseUrl, + + @Size(max = 500, message = "숙소 URL은 500자 이하여야 합니다") + String accommodationUrl, + + @NotBlank(message = "로고 이미지 URL은 필수입니다") + @Size(max = 500, message = "로고 이미지 URL은 500자 이하여야 합니다") + String logoImageUrl, + + @NotBlank(message = "배경 이미지 URL은 필수입니다") + @Size(max = 500, message = "배경 이미지 URL은 500자 이하여야 합니다") + String backgroundImageUrl, + + @Size(max = 1000, message = "상세 정보는 1000자 이하여야 합니다") + String detailsForLocal, + + @NotBlank(message = "국가 코드는 필수입니다") + String countryCode, + + @NotBlank(message = "지역 코드는 필수입니다") + String regionCode +) { + +} 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 new file mode 100644 index 000000000..c03e9f526 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java @@ -0,0 +1,152 @@ +package com.example.solidconnection.admin.university.service; + +import static com.example.solidconnection.common.exception.ErrorCode.COUNTRY_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS; +import static com.example.solidconnection.common.exception.ErrorCode.HOST_UNIVERSITY_HAS_REFERENCES; +import static com.example.solidconnection.common.exception.ErrorCode.REGION_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND; + +import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; +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.common.exception.CustomException; +import com.example.solidconnection.location.country.domain.Country; +import com.example.solidconnection.location.country.repository.CountryRepository; +import com.example.solidconnection.location.region.domain.Region; +import com.example.solidconnection.location.region.repository.RegionRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepository; +import com.example.solidconnection.university.repository.UnivApplyInfoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AdminHostUniversityService { + + private final HostUniversityRepository hostUniversityRepository; + private final CountryRepository countryRepository; + private final RegionRepository regionRepository; + private final UnivApplyInfoRepository univApplyInfoRepository; + + @Transactional(readOnly = true) + public Page getHostUniversities( + AdminHostUniversitySearchCondition condition, + Pageable pageable + ) { + Page hostUniversityPage = hostUniversityRepository.findAllBySearchCondition( + condition.keyword(), + condition.countryCode(), + condition.regionCode(), + pageable + ); + return hostUniversityPage.map(AdminHostUniversityResponse::from); + } + + @Transactional(readOnly = true) + public AdminHostUniversityDetailResponse getHostUniversity(Long id) { + HostUniversity hostUniversity = hostUniversityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + return AdminHostUniversityDetailResponse.from(hostUniversity); + } + + @Transactional + public AdminHostUniversityDetailResponse createHostUniversity(AdminHostUniversityCreateRequest request) { + validateKoreanNameNotExists(request.koreanName()); + + Country country = findCountryByCode(request.countryCode()); + Region region = findRegionByCode(request.regionCode()); + + HostUniversity hostUniversity = new HostUniversity( + null, + request.koreanName(), + request.englishName(), + request.formatName(), + request.homepageUrl(), + request.englishCourseUrl(), + request.accommodationUrl(), + request.logoImageUrl(), + request.backgroundImageUrl(), + request.detailsForLocal(), + country, + region + ); + + HostUniversity savedHostUniversity = hostUniversityRepository.save(hostUniversity); + return AdminHostUniversityDetailResponse.from(savedHostUniversity); + } + + private void validateKoreanNameNotExists(String koreanName) { + hostUniversityRepository.findByKoreanName(koreanName) + .ifPresent(existingUniversity -> { + throw new CustomException(HOST_UNIVERSITY_ALREADY_EXISTS); + }); + } + + @Transactional + public AdminHostUniversityDetailResponse updateHostUniversity(Long id, AdminHostUniversityUpdateRequest request) { + HostUniversity hostUniversity = hostUniversityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + + validateKoreanNameNotDuplicated(request.koreanName(), id); + + Country country = findCountryByCode(request.countryCode()); + Region region = findRegionByCode(request.regionCode()); + + hostUniversity.update( + request.koreanName(), + request.englishName(), + request.formatName(), + request.homepageUrl(), + request.englishCourseUrl(), + request.accommodationUrl(), + request.logoImageUrl(), + request.backgroundImageUrl(), + request.detailsForLocal(), + country, + region + ); + + return AdminHostUniversityDetailResponse.from(hostUniversity); + } + + private void validateKoreanNameNotDuplicated(String koreanName, Long excludeId) { + hostUniversityRepository.findByKoreanName(koreanName) + .ifPresent(existingUniversity -> { + if (!existingUniversity.getId().equals(excludeId)) { + throw new CustomException(HOST_UNIVERSITY_ALREADY_EXISTS); + } + }); + } + + private Country findCountryByCode(String countryCode) { + return countryRepository.findByCode(countryCode) + .orElseThrow(() -> new CustomException(COUNTRY_NOT_FOUND)); + } + + private Region findRegionByCode(String regionCode) { + return regionRepository.findById(regionCode) + .orElseThrow(() -> new CustomException(REGION_NOT_FOUND)); + } + + @Transactional + public void deleteHostUniversity(Long id) { + HostUniversity hostUniversity = hostUniversityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + + validateNoReferences(id); + + hostUniversityRepository.delete(hostUniversity); + } + + private void validateNoReferences(Long hostUniversityId) { + if (univApplyInfoRepository.existsByUniversityId(hostUniversityId)) { + throw new CustomException(HOST_UNIVERSITY_HAS_REFERENCES); + } + } +} 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 9fbd2f225..9308a6872 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -117,9 +117,7 @@ public ResponseEntity quit( } @PostMapping("/reissue") - public ResponseEntity reissueToken( - HttpServletRequest request - ) { + public ResponseEntity reissueToken(HttpServletRequest request) { String refreshToken = refreshTokenCookieManager.getRefreshToken(request); ReissueResponse reissueResponse = authService.reissue(refreshToken); return ResponseEntity.ok(reissueResponse); diff --git a/src/main/java/com/example/solidconnection/auth/exception/AuthException.java b/src/main/java/com/example/solidconnection/auth/exception/AuthException.java new file mode 100644 index 000000000..ef9799567 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/exception/AuthException.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.auth.exception; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; + +public class AuthException extends CustomException { + + public AuthException(ErrorCode errorCode) { + super(errorCode); + } + + public AuthException(ErrorCode errorCode, String detail) { + super(errorCode, detail); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 1c9478e80..e8e150865 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -5,6 +5,7 @@ import com.example.solidconnection.auth.domain.AccessToken; import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.exception.AuthException; import com.example.solidconnection.auth.token.TokenBlackListService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -55,7 +56,7 @@ public void quit(long siteUserId, String token) { public ReissueResponse reissue(String requestedRefreshToken) { // 리프레시 토큰 확인 if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) { - throw new CustomException(REFRESH_TOKEN_EXPIRED); + throw new AuthException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 SiteUser siteUser = authTokenProvider.parseSiteUser(requestedRefreshToken); diff --git a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java index b6a9fe0b0..6f0abc7f2 100644 --- a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java +++ b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java @@ -1,9 +1,9 @@ package com.example.solidconnection.cache; -import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_CHANNEL; -import static com.example.solidconnection.community.post.service.RedisConstants.LOCK_TIMEOUT_MS; -import static com.example.solidconnection.community.post.service.RedisConstants.MAX_WAIT_TIME_MS; -import static com.example.solidconnection.community.post.service.RedisConstants.REFRESH_LIMIT_PERCENT; +import static com.example.solidconnection.redis.RedisConstants.CREATE_CHANNEL; +import static com.example.solidconnection.redis.RedisConstants.LOCK_TIMEOUT_MS; +import static com.example.solidconnection.redis.RedisConstants.MAX_WAIT_TIME_MS; +import static com.example.solidconnection.redis.RedisConstants.REFRESH_LIMIT_PERCENT; import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.cache.manager.CacheManager; diff --git a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java index 51259a0e1..c7631172d 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java @@ -5,6 +5,7 @@ import com.example.solidconnection.chat.config.StompProperties.OutboundProperties; import com.example.solidconnection.security.config.CorsProperties; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; @@ -24,6 +25,7 @@ public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { private final CorsProperties corsProperties; private final WebSocketHandshakeInterceptor webSocketHandshakeInterceptor; private final CustomHandshakeHandler customHandshakeHandler; + private final Optional webSocketLoggingInterceptor; @Override public void registerStompEndpoints(StompEndpointRegistry registry) { @@ -39,7 +41,12 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { @Override public void configureClientInboundChannel(ChannelRegistration registration) { InboundProperties inboundProperties = stompProperties.threadPool().inbound(); - registration.interceptors(stompHandler).taskExecutor().corePoolSize(inboundProperties.corePoolSize()).maxPoolSize(inboundProperties.maxPoolSize()).queueCapacity(inboundProperties.queueCapacity()); + webSocketLoggingInterceptor.ifPresent(registration::interceptors); + registration.interceptors(stompHandler) + .taskExecutor() + .corePoolSize(inboundProperties.corePoolSize()) + .maxPoolSize(inboundProperties.maxPoolSize()) + .queueCapacity(inboundProperties.queueCapacity()); } @Override diff --git a/src/main/java/com/example/solidconnection/chat/config/WebSocketLoggingInterceptor.java b/src/main/java/com/example/solidconnection/chat/config/WebSocketLoggingInterceptor.java new file mode 100644 index 000000000..ea137ade7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/WebSocketLoggingInterceptor.java @@ -0,0 +1,55 @@ +package com.example.solidconnection.chat.config; + +import com.example.solidconnection.security.authentication.TokenAuthentication; +import com.example.solidconnection.security.userdetails.SiteUserDetails; +import java.security.Principal; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@Profile("dev") +public class WebSocketLoggingInterceptor implements ChannelInterceptor { + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + StompCommand command = accessor.getCommand(); + + if (command != null) { + Long userId = extractUserId(accessor); + String destination = accessor.getDestination(); + logStompMessage(command, destination, userId); + } + + return message; + } + + private void logStompMessage(StompCommand command, String destination, Long userId) { + switch (command) { + case CONNECT -> log.info("[WEBSOCKET] CONNECT userId = {}", userId); + case SUBSCRIBE -> log.info("[WEBSOCKET] SUBSCRIBE {} userId = {}", destination, userId); + case SEND -> log.info("[WEBSOCKET] SEND {} userId = {}", destination, userId); + case DISCONNECT -> log.info("[WEBSOCKET] DISCONNECT userId = {}", userId); + default -> { + } + } + } + + private Long extractUserId(StompHeaderAccessor accessor) { + Principal user = accessor.getUser(); + if (user instanceof TokenAuthentication tokenAuth) { + Object principal = tokenAuth.getPrincipal(); + if (principal instanceof SiteUserDetails details) { + return details.getSiteUser().getId(); + } + } + return null; + } +} diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java index b9551b9b2..fa536d495 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java @@ -6,13 +6,13 @@ public record ChatMessageResponse( long id, String content, - long senderId, // siteUserId + long siteUserId, ZonedDateTime createdAt, List attachments ) { - public static ChatMessageResponse of(long id, String content, long senderId, + public static ChatMessageResponse of(long id, String content, long siteUserId, ZonedDateTime createdAt, List attachments) { - return new ChatMessageResponse(id, content, senderId, createdAt, attachments); + return new ChatMessageResponse(id, content, siteUserId, createdAt, attachments); } } diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java index 8e976148a..4137a1c71 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java @@ -7,12 +7,12 @@ public record ChatMessageSendResponse( long messageId, String content, - long senderId, + long siteUserId, MessageType messageType, List attachments ) { - public static ChatMessageSendResponse from(ChatMessage chatMessage) { + public static ChatMessageSendResponse of(ChatMessage chatMessage, long siteUserId) { MessageType messageType = chatMessage.getChatAttachments().isEmpty() ? MessageType.TEXT : MessageType.IMAGE; @@ -30,7 +30,7 @@ public static ChatMessageSendResponse from(ChatMessage chatMessage) { return new ChatMessageSendResponse( chatMessage.getId(), chatMessage.getContent(), - chatMessage.getSenderId(), + siteUserId, messageType, attachments ); diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java index 18276b561..2e8d1e7ed 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java @@ -1,12 +1,12 @@ package com.example.solidconnection.chat.dto; public record ChatParticipantResponse( - long partnerId, // siteUserId + long siteUserId, String nickname, String profileUrl ) { - public static ChatParticipantResponse of(long partnerId, String nickname, String profileUrl) { - return new ChatParticipantResponse(partnerId, nickname, profileUrl); + public static ChatParticipantResponse of(long siteUserId, String nickname, String profileUrl) { + return new ChatParticipantResponse(siteUserId, nickname, profileUrl); } } diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 57f8cad65..11560f434 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -27,6 +27,7 @@ import com.example.solidconnection.mentor.repository.MentorRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -39,6 +40,7 @@ import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; @Service public class ChatService { @@ -226,7 +228,7 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long chatMessageRepository.save(chatMessage); - ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); + ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.of(chatMessage, siteUserId); simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } @@ -240,22 +242,25 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)); - ChatMessage chatMessage = new ChatMessage( - "", - senderId, - chatRoom - ); + ChatMessage chatMessage = new ChatMessage("", senderId, chatRoom); + + // 이미지 판별을 위한 확장자 리스트 + List imageExtensions = Arrays.asList("jpg", "jpeg", "png", "webp"); for (String imageUrl : chatImageSendRequest.imageUrls()) { - String thumbnailUrl = generateThumbnailUrl(imageUrl); + String extension = StringUtils.getFilenameExtension(imageUrl); - ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null); + boolean isImage = extension != null && imageExtensions.contains(extension.toLowerCase()); + + String thumbnailUrl = isImage ? generateThumbnailUrl(imageUrl) : null; + + ChatAttachment attachment = new ChatAttachment(isImage, imageUrl, thumbnailUrl, null); chatMessage.addAttachment(attachment); } chatMessageRepository.save(chatMessage); - ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); + ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.of(chatMessage, siteUserId); simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } @@ -268,11 +273,9 @@ private String generateThumbnailUrl(String originalUrl) { String thumbnailFileName = nameWithoutExt + "_thumb" + extension; - String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/") + return originalUrl.replace("chat/files/", "chat/thumbnails/") .replace(fileName, thumbnailFileName); - return thumbnailUrl; - } catch (Exception e) { return originalUrl; } diff --git a/src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java index a59558993..f94443580 100644 --- a/src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java +++ b/src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java @@ -1,6 +1,6 @@ package com.example.solidconnection.common.config.redis; -import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_CHANNEL; +import static com.example.solidconnection.redis.RedisConstants.CREATE_CHANNEL; import com.example.solidconnection.cache.CacheUpdateListener; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/example/solidconnection/common/constant/FileConstants.java b/src/main/java/com/example/solidconnection/common/constant/FileConstants.java new file mode 100644 index 000000000..1f20b502e --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/constant/FileConstants.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.common.constant; + +import java.util.List; +import java.util.stream.Stream; + +public final class FileConstants { + private FileConstants() {} + + public static final List IMAGE_EXTENSIONS = List.of( + "jpg", "jpeg", "png", "webp", "avif", "heic", "heif", "tiff" + ); + + public static final List DOCUMENT_EXTENSIONS = List.of( + "doc", "docx", "xls", "xlsx", "ppt", "pptx", "hwp", "hwpx", "pdf", "txt" + ); + + public static final List ARCHIVE_EXTENSIONS = List.of( + "zip", "7z", "rar" + ); + + public static final List ALL_ALLOWED_EXTENSIONS = Stream.of( + IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS, ARCHIVE_EXTENSIONS) + .flatMap(List::stream) + .toList(); +} diff --git a/src/main/java/com/example/solidconnection/common/exception/CustomException.java b/src/main/java/com/example/solidconnection/common/exception/CustomException.java index fab42924c..eb596f8cf 100644 --- a/src/main/java/com/example/solidconnection/common/exception/CustomException.java +++ b/src/main/java/com/example/solidconnection/common/exception/CustomException.java @@ -5,16 +5,19 @@ @Getter public class CustomException extends RuntimeException { + private final ErrorCode errorCode; private final int code; private final String message; public CustomException(ErrorCode errorCode) { - code = errorCode.getCode(); - message = errorCode.getMessage(); + this.errorCode = errorCode; + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); } public CustomException(ErrorCode errorCode, String detail) { - code = errorCode.getCode(); - message = errorCode.getMessage() + " : " + detail; + this.errorCode = errorCode; + this.code = errorCode.getCode(); + this.message = errorCode.getMessage() + " : " + detail; } } diff --git a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java index 5700c3044..09057005f 100644 --- a/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java +++ b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java @@ -5,12 +5,17 @@ import static com.example.solidconnection.common.exception.ErrorCode.JSON_PARSING_FAILED; import static com.example.solidconnection.common.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.common.exception.ErrorCode.NOT_DEFINED_ERROR; +import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; +import com.example.solidconnection.auth.controller.RefreshTokenCookieManager; +import com.example.solidconnection.auth.exception.AuthException; import com.example.solidconnection.common.response.ErrorResponse; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import io.jsonwebtoken.JwtException; +import jakarta.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; @@ -21,8 +26,26 @@ @Slf4j @ControllerAdvice +@RequiredArgsConstructor public class CustomExceptionHandler { + private final RefreshTokenCookieManager refreshTokenCookieManager; + + @ExceptionHandler(AuthException.class) + protected ResponseEntity handleAuthException( + AuthException ex, + HttpServletResponse response + ) { + log.error("인증 예외 발생 : {}", ex.getMessage()); + if (ex.getErrorCode().equals(REFRESH_TOKEN_EXPIRED)) { + refreshTokenCookieManager.deleteCookie(response); + } + ErrorResponse errorResponse = new ErrorResponse(ex); + return ResponseEntity + .status(ex.getCode()) + .body(errorResponse); + } + @ExceptionHandler(CustomException.class) protected ResponseEntity handleCustomException(CustomException ex) { log.error("커스텀 예외 발생 : {}", ex.getMessage()); 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 d00ce52b3..48e11d846 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,9 @@ 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(), "이미 존재하는 지역입니다."), + HOST_UNIVERSITY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 파견 대학입니다."), + HOST_UNIVERSITY_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 파견 대학을 참조하는 대학 지원 정보가 존재합니다."), + COUNTRY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "국가를 찾을 수 없습니다."), COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."), GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."), LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."), @@ -96,6 +99,7 @@ public enum ErrorCode { INVALID_BOARD_CODE(HttpStatus.BAD_REQUEST.value(), "잘못된 게시판 코드입니다."), INVALID_POST_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 게시글입니다."), // todo: NOT_FOUND로 통일 필요 INVALID_POST_ACCESS(HttpStatus.BAD_REQUEST.value(), "자신의 게시글만 제어할 수 있습니다."), + DUPLICATE_POST_CREATE_REQUEST(HttpStatus.BAD_REQUEST.value(), "게시글이 이미 생성 중입니다."), CAN_NOT_DELETE_OR_UPDATE_QUESTION(HttpStatus.BAD_REQUEST.value(), "질문글은 수정이나 삭제할 수 없습니다."), CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES(HttpStatus.BAD_REQUEST.value(), "5개 이상의 파일을 업로드할 수 없습니다."), INVALID_COMMENT_ID(HttpStatus.BAD_REQUEST.value(), "존재하지 않는 댓글입니다."), diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java index 4b3b8d15a..46a40c8ae 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java @@ -2,6 +2,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; +import static com.example.solidconnection.common.exception.ErrorCode.DUPLICATE_POST_CREATE_REQUEST; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_POST_ACCESS; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_POST_CATEGORY; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; @@ -18,12 +19,11 @@ import com.example.solidconnection.community.post.dto.PostUpdateRequest; import com.example.solidconnection.community.post.dto.PostUpdateResponse; import com.example.solidconnection.community.post.repository.PostRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.util.RedisUtils; import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -40,18 +40,23 @@ public class PostCommandService { private final BoardRepository boardRepository; private final SiteUserRepository siteUserRepository; private final S3Service s3Service; - private final RedisService redisService; - private final RedisUtils redisUtils; + private final PostRedisManager postRedisManager; @Transactional public PostCreateResponse createPost(long siteUserId, PostCreateRequest postCreateRequest, List imageFile) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + // 유효성 검증 validatePostCategory(postCreateRequest.postCategory()); validateFileSize(imageFile); + // 중복 생성 방지 + if (!postRedisManager.isPostCreationAllowed(siteUserId)) { + throw new CustomException(DUPLICATE_POST_CREATE_REQUEST); + } + // 객체 생성 Board board = boardRepository.getByCode(postCreateRequest.boardCode()); Post post = postCreateRequest.toEntity(siteUser, board); @@ -88,7 +93,7 @@ private void savePostImages(List imageFile, Post post) { if (imageFile.isEmpty()) { return; } - List uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, ImgType.COMMUNITY); + List uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, UploadPath.COMMUNITY); for (UploadedFileUrlResponse uploadedFileUrlResponse : uploadedFileUrlResponseList) { PostImage postImage = new PostImage(uploadedFileUrlResponse.fileUrl()); postImage.setPost(post); @@ -104,8 +109,7 @@ public PostDeleteResponse deletePostById(long siteUserId, Long postId) { validateQuestion(post); removePostImages(post); - // cache out - redisService.deleteKey(redisUtils.getPostViewCountRedisKey(postId)); + postRedisManager.deleteViewCountCache(postId); postRepository.deleteById(post.getId()); return new PostDeleteResponse(postId); diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java index 413ec400d..9beea5e8d 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java @@ -23,7 +23,6 @@ import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.siteuser.repository.UserBlockRepository; -import com.example.solidconnection.util.RedisUtils; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -42,8 +41,7 @@ public class PostQueryService { private final SiteUserRepository siteUserRepository; private final UserBlockRepository userBlockRepository; private final CommentService commentService; - private final RedisService redisService; - private final RedisUtils redisUtils; + private final PostRedisManager postRedisManager; @Transactional(readOnly = true) public List findPostsByCodeAndPostCategoryOrderByCreatedAtDesc(String code, String category, Long siteUserId) { @@ -81,10 +79,7 @@ public PostFindResponse findPostById(long siteUserId, Long postId) { List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); List commentFindResultDTOList = commentService.findCommentsByPostId(siteUser.getId(), postId); - // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), postId))) { - redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); - } + postRedisManager.incrementViewCountIfFirstAccess(siteUser.getId(), postId); return PostFindResponse.from( post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostRedisManager.java b/src/main/java/com/example/solidconnection/community/post/service/PostRedisManager.java new file mode 100644 index 000000000..e074b45f4 --- /dev/null +++ b/src/main/java/com/example/solidconnection/community/post/service/PostRedisManager.java @@ -0,0 +1,58 @@ +package com.example.solidconnection.community.post.service; + +import static com.example.solidconnection.redis.RedisConstants.POST_CREATE_PREFIX; +import static com.example.solidconnection.redis.RedisConstants.VALIDATE_POST_CREATE_TTL; +import static com.example.solidconnection.redis.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX; +import static com.example.solidconnection.redis.RedisConstants.VALIDATE_VIEW_COUNT_TTL; +import static com.example.solidconnection.redis.RedisConstants.VIEW_COUNT_KEY_PREFIX; + +import com.example.solidconnection.redis.RedisService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PostRedisManager { + + private final RedisService redisService; + + public Long getPostIdFromPostViewCountRedisKey(String key) { + return Long.parseLong(key.substring(VIEW_COUNT_KEY_PREFIX.getValue().length())); + } + + public Long getAndDeleteViewCount(String key) { + return redisService.getAndDelete(key); + } + + public void deleteViewCountCache(Long postId) { + String key = getPostViewCountRedisKey(postId); + redisService.deleteKey(key); + } + + public void incrementViewCountIfFirstAccess(long siteUserId, Long postId) { + String validateKey = getValidatePostViewCountRedisKey(siteUserId, postId); + boolean isFirstAccess = redisService.isPresent(validateKey, VALIDATE_VIEW_COUNT_TTL.getValue()); + + if (isFirstAccess) { + String viewCountKey = getPostViewCountRedisKey(postId); + redisService.increaseViewCount(viewCountKey); + } + } + + public String getPostViewCountRedisKey(Long postId) { + return VIEW_COUNT_KEY_PREFIX.getValue() + postId; + } + + public String getValidatePostViewCountRedisKey(long siteUserId, Long postId) { + return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + siteUserId; + } + + public boolean isPostCreationAllowed(Long siteUserId) { + String key = getPostCreateRedisKey(siteUserId); + return redisService.isPresent(key, VALIDATE_POST_CREATE_TTL.getValue()); + } + + public String getPostCreateRedisKey(Long siteUserId) { + return POST_CREATE_PREFIX.getValue() + siteUserId; + } +} diff --git a/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java b/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java index 42a5f8b95..4957b50d7 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.community.post.repository.PostRepository; -import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -17,14 +16,13 @@ public class UpdateViewCountService { private final PostRepository postRepository; - private final RedisService redisService; - private final RedisUtils redisUtils; + private final PostRedisManager postRedisManager; @Transactional @Async public void updateViewCount(String key) { - Long postId = redisUtils.getPostIdFromPostViewCountRedisKey(key); - Post post = postRepository.getById(postId); - postRepository.increaseViewCount(postId, redisService.getAndDelete(key)); + Long postId = postRedisManager.getPostIdFromPostViewCountRedisKey(key); + Long viewCount = postRedisManager.getAndDeleteViewCount(key); + postRepository.increaseViewCount(postId, viewCount); } } diff --git a/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java b/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java index 8b997a2de..7aaf8ff56 100644 --- a/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java @@ -2,12 +2,15 @@ import com.example.solidconnection.location.country.domain.Country; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface CountryRepository extends JpaRepository { + Optional findByCode(String code); + List findAllByKoreanNameIn(List koreanNames); @Query(""" diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java index 0ce04e1f7..37f40fe8d 100644 --- a/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentorController.java @@ -25,12 +25,12 @@ public class MentorController { private final MentorQueryService mentorQueryService; - @GetMapping("/{mentor-id}") + @GetMapping("/{site-user-id}") public ResponseEntity getMentorDetails( @AuthorizedUser long siteUserId, - @PathVariable("mentor-id") Long mentorId + @PathVariable("site-user-id") Long mentorSiteUserId ) { - MentorDetailResponse response = mentorQueryService.getMentorDetails(mentorId, siteUserId); + MentorDetailResponse response = mentorQueryService.getMentorDetails(mentorSiteUserId, siteUserId); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java index 76c951a74..dd9289a3e 100644 --- a/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentorMyPageController.java @@ -1,7 +1,6 @@ package com.example.solidconnection.mentor.controller; import com.example.solidconnection.common.resolver.AuthorizedUser; -import com.example.solidconnection.mentor.dto.MentorMyPageCreateRequest; import com.example.solidconnection.mentor.dto.MentorMyPageResponse; import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; import com.example.solidconnection.mentor.service.MentorMyPageService; @@ -11,7 +10,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -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; @@ -42,14 +40,4 @@ public ResponseEntity updateMentorMyPage( mentorMyPageService.updateMentorMyPage(siteUserId, mentorMyPageUpdateRequest); return ResponseEntity.ok().build(); } - - @RequireRoleAccess(roles = Role.MENTOR) - @PostMapping - public ResponseEntity createMentorMyPage( - @AuthorizedUser long siteUserId, - @Valid @RequestBody MentorMyPageCreateRequest request - ) { - mentorMyPageService.createMentorMyPage(siteUserId, request); - return ResponseEntity.ok().build(); - } } diff --git a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java index 30dbfec20..8d1934cca 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/Mentor.java @@ -33,10 +33,10 @@ public class Mentor extends BaseEntity { @Column private boolean hasBadge = false; - @Column(length = 1000, nullable = false) + @Column(length = 1000) private String introduction; - @Column(length = 1000, nullable = false) + @Column(length = 1000) private String passTip; @Column @@ -67,6 +67,16 @@ public Mentor( this.termId = termId; } + public Mentor( + long siteUserId, + Long universityId, + long termId + ) { + this.siteUserId = siteUserId; + this.universityId = universityId; + this.termId = termId; + } + public void increaseMenteeCount() { this.menteeCount++; } @@ -96,11 +106,4 @@ public void updateChannels(List channels) { } } } - - public void createChannels(List channels) { - for(Channel channel : channels) { - channel.updateMentor(this); - this.channels.add(channel); - } - } } diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MatchedMentorResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MatchedMentorResponse.java index fc3582ff3..7a229767e 100644 --- a/src/main/java/com/example/solidconnection/mentor/dto/MatchedMentorResponse.java +++ b/src/main/java/com/example/solidconnection/mentor/dto/MatchedMentorResponse.java @@ -4,7 +4,7 @@ import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.fasterxml.jackson.annotation.JsonInclude; import java.util.List; @@ -27,11 +27,11 @@ public record MatchedMentorResponse( ) { public static MatchedMentorResponse of(Mentor mentor, SiteUser mentorUser, - University university, boolean isApplied, Long roomId, + HostUniversity university, boolean isApplied, Long roomId, String termName ) { return new MatchedMentorResponse( - mentor.getId(), + mentor.getSiteUserId(), roomId, mentorUser.getNickname(), mentorUser.getProfileImageUrl(), diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java index a65e784f0..6795db565 100644 --- a/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorDetailResponse.java @@ -2,7 +2,7 @@ import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import java.util.List; public record MentorDetailResponse( @@ -21,11 +21,11 @@ public record MentorDetailResponse( ) { public static MentorDetailResponse of(Mentor mentor, SiteUser mentorUser, - University university, boolean isApplied, + HostUniversity university, boolean isApplied, String termName ) { return new MentorDetailResponse( - mentor.getId(), + mentor.getSiteUserId(), mentorUser.getNickname(), mentorUser.getProfileImageUrl(), university.getCountry().getKoreanName(), diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageCreateRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageCreateRequest.java deleted file mode 100644 index be0269b29..000000000 --- a/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageCreateRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.solidconnection.mentor.dto; - -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.util.List; - -public record MentorMyPageCreateRequest( - @NotBlank(message = "자기소개를 입력해주세요.") - String introduction, - - @NotBlank(message = "합격 레시피를 입력해주세요.") - String passTip, - - @NotNull - @Valid - List channels -) { - -} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.java index 4c5533ebb..c438f689c 100644 --- a/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.java +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorMyPageResponse.java @@ -2,7 +2,7 @@ import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import java.util.List; public record MentorMyPageResponse( @@ -19,9 +19,9 @@ public record MentorMyPageResponse( List channels ) { - public static MentorMyPageResponse of(Mentor mentor, SiteUser siteUser, University university, String termName) { + public static MentorMyPageResponse of(Mentor mentor, SiteUser siteUser, HostUniversity university, String termName) { return new MentorMyPageResponse( - mentor.getId(), + mentor.getSiteUserId(), siteUser.getProfileImageUrl(), siteUser.getNickname(), university.getCountry().getKoreanName(), diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java index e6a84d2eb..88eccbdc0 100644 --- a/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorPreviewResponse.java @@ -2,7 +2,7 @@ import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import java.util.List; public record MentorPreviewResponse( @@ -20,11 +20,11 @@ public record MentorPreviewResponse( ) { public static MentorPreviewResponse of(Mentor mentor, SiteUser mentorUser, - University university, boolean isApplied, + HostUniversity university, boolean isApplied, String termName ) { return new MentorPreviewResponse( - mentor.getId(), + mentor.getSiteUserId(), mentorUser.getNickname(), mentorUser.getProfileImageUrl(), university.getCountry().getKoreanName(), diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentoringForMentorResponse.java b/src/main/java/com/example/solidconnection/mentor/dto/MentoringForMentorResponse.java index 46791d071..2780da0d6 100644 --- a/src/main/java/com/example/solidconnection/mentor/dto/MentoringForMentorResponse.java +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentoringForMentorResponse.java @@ -6,6 +6,7 @@ import java.time.ZonedDateTime; public record MentoringForMentorResponse( + long mentoringId, Long roomId, String profileImageUrl, String nickname, @@ -16,6 +17,7 @@ public record MentoringForMentorResponse( public static MentoringForMentorResponse of(Mentoring mentoring, SiteUser partner, Long roomId) { return new MentoringForMentorResponse( + mentoring.getId(), roomId, partner.getProfileImageUrl(), partner.getNickname(), diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java index 27d6a80d3..34e65940f 100644 --- a/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepository.java @@ -9,8 +9,8 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.repository.TermRepository; -import com.example.solidconnection.university.domain.University; -import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepository; import java.util.Collections; import java.util.List; import java.util.Map; @@ -26,7 +26,7 @@ public class MentorBatchQueryRepository { // 연관관계가 설정되지 않은 private final SiteUserRepository siteUserRepository; private final MentoringRepository mentoringRepository; - private final UniversityRepository universityRepository; + private final HostUniversityRepository hostUniversityRepository; private final TermRepository termRepository; public Map getMentorIdToSiteUserMap(List mentors) { @@ -47,16 +47,16 @@ public Map getMentorIdToSiteUserMap(List mentors) { )); } - public Map getMentorIdToUniversityMap(List mentors) { + public Map getMentorIdToUniversityMap(List mentors) { List universityIds = mentors.stream().map(Mentor::getUniversityId).distinct().toList(); - List universities = universityRepository.findAllById(universityIds); - Map universityIdToUniversityMap = universities.stream() - .collect(Collectors.toMap(University::getId, Function.identity())); + List universities = hostUniversityRepository.findAllById(universityIds); + Map universityIdToUniversityMap = universities.stream() + .collect(Collectors.toMap(HostUniversity::getId, Function.identity())); return mentors.stream().collect(Collectors.toMap( Mentor::getId, mentor -> { - University university = universityIdToUniversityMap.get(mentor.getUniversityId()); + HostUniversity university = universityIdToUniversityMap.get(mentor.getUniversityId()); if (university == null) { // mentor.university_id에 해당하는 대학이 없으면 정합성 문제가 발생한 것 throw new CustomException(DATA_INTEGRITY_VIOLATION, "mentor.university_id 에 해당하는 university 존재하지 않음"); } diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java index 672f9325e..0fcd2dcd6 100644 --- a/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java @@ -21,7 +21,7 @@ public interface MentorRepository extends JpaRepository { @Query(""" SELECT m FROM Mentor m - JOIN University u ON m.universityId = u.id + JOIN HostUniversity u ON m.universityId = u.id WHERE u.region = :region """) Slice findAllByRegion(@Param("region") Region region, Pageable pageable); diff --git a/src/main/java/com/example/solidconnection/mentor/repository/custom/MentorApplicationFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/mentor/repository/custom/MentorApplicationFilterRepositoryImpl.java index 38dc0b6e4..f6b504652 100644 --- a/src/main/java/com/example/solidconnection/mentor/repository/custom/MentorApplicationFilterRepositoryImpl.java +++ b/src/main/java/com/example/solidconnection/mentor/repository/custom/MentorApplicationFilterRepositoryImpl.java @@ -4,7 +4,7 @@ import static com.example.solidconnection.location.region.domain.QRegion.region; import static com.example.solidconnection.mentor.domain.QMentorApplication.mentorApplication; import static com.example.solidconnection.siteuser.domain.QSiteUser.siteUser; -import static com.example.solidconnection.university.domain.QUniversity.university; +import static com.example.solidconnection.university.domain.QHostUniversity.hostUniversity; import static org.springframework.util.StringUtils.hasText; import com.example.solidconnection.admin.dto.MentorApplicationResponse; @@ -48,7 +48,7 @@ public class MentorApplicationFilterRepositoryImpl implements MentorApplicationF mentorApplication.id, region.koreanName, country.koreanName, - university.koreanName, + hostUniversity.koreanName, mentorApplication.universitySelectType, mentorApplication.mentorProofUrl, mentorApplication.mentorApplicationStatus, @@ -77,9 +77,9 @@ public Page searchMentorApplications(MentorAppl .select(MENTOR_APPLICATION_SEARCH_RESPONSE_PROJECTION) .from(mentorApplication) .join(siteUser).on(mentorApplication.siteUserId.eq(siteUser.id)) - .leftJoin(university).on(mentorApplication.universityId.eq(university.id)) - .leftJoin(region).on(university.region.eq(region)) - .leftJoin(country).on(university.country.eq(country)) + .leftJoin(hostUniversity).on(mentorApplication.universityId.eq(hostUniversity.id)) + .leftJoin(region).on(hostUniversity.region.eq(region)) + .leftJoin(country).on(hostUniversity.country.eq(country)) .where( verifyMentorStatusEq(condition.mentorApplicationStatus()), keywordContains(condition.keyword()), @@ -105,9 +105,9 @@ private JPAQuery createCountQuery(MentorApplicationSearchCondition conditi if (hasText(keyword)) { query.join(siteUser).on(mentorApplication.siteUserId.eq(siteUser.id)) - .leftJoin(university).on(mentorApplication.universityId.eq(university.id)) - .leftJoin(region).on(university.region.eq(region)) - .leftJoin(country).on(university.country.eq(country)); + .leftJoin(hostUniversity).on(mentorApplication.universityId.eq(hostUniversity.id)) + .leftJoin(region).on(hostUniversity.region.eq(region)) + .leftJoin(country).on(hostUniversity.country.eq(country)); } return query.where( @@ -128,7 +128,7 @@ private BooleanExpression keywordContains(String keyword) { } return siteUser.nickname.containsIgnoreCase(keyword) - .or(university.koreanName.containsIgnoreCase(keyword)) + .or(hostUniversity.koreanName.containsIgnoreCase(keyword)) .or(region.koreanName.containsIgnoreCase(keyword)) .or(country.koreanName.containsIgnoreCase(keyword)); } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java index e4e187808..57edfdded 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -5,7 +5,7 @@ import com.example.solidconnection.mentor.domain.MentorApplicationStatus; import com.example.solidconnection.mentor.dto.MentorApplicationRequest; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -45,7 +45,7 @@ public void submitMentorApplication( .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); Term term = termRepository.findByName(mentorApplicationRequest.term()) .orElseThrow(() -> new CustomException(TERM_NOT_FOUND)); - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadPath.MENTOR_PROOF); MentorApplication mentorApplication = new MentorApplication( siteUser.getId(), mentorApplicationRequest.country(), diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java index 7f226f380..ed24fd794 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorMyPageService.java @@ -1,8 +1,6 @@ package com.example.solidconnection.mentor.service; import static com.example.solidconnection.common.exception.ErrorCode.CHANNEL_REGISTRATION_LIMIT_EXCEEDED; -import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS; -import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.TERM_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND; @@ -11,10 +9,7 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Channel; import com.example.solidconnection.mentor.domain.Mentor; -import com.example.solidconnection.mentor.domain.MentorApplication; -import com.example.solidconnection.mentor.domain.MentorApplicationStatus; import com.example.solidconnection.mentor.dto.ChannelRequest; -import com.example.solidconnection.mentor.dto.MentorMyPageCreateRequest; import com.example.solidconnection.mentor.dto.MentorMyPageResponse; import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; @@ -23,8 +18,8 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.repository.TermRepository; -import com.example.solidconnection.university.domain.University; -import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepository; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -40,7 +35,7 @@ public class MentorMyPageService { private final MentorRepository mentorRepository; private final SiteUserRepository siteUserRepository; - private final UniversityRepository universityRepository; + private final HostUniversityRepository hostUniversityRepository; private final TermRepository termRepository; private final MentorApplicationRepository mentorApplicationRepository; @@ -52,7 +47,7 @@ public MentorMyPageResponse getMentorMyPage(long siteUserId) { .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); Term term = termRepository.findById(mentor.getTermId()) .orElseThrow(() -> new CustomException(TERM_NOT_FOUND)); - University university = universityRepository.findById(mentor.getUniversityId()) + HostUniversity university = hostUniversityRepository.findById(mentor.getUniversityId()) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); return MentorMyPageResponse.of(mentor, siteUser, university, term.getName()); } @@ -65,50 +60,17 @@ public void updateMentorMyPage(long siteUserId, MentorMyPageUpdateRequest reques mentor.updateIntroduction(request.introduction()); mentor.updatePassTip(request.passTip()); - updateChannel(request.channels(), mentor); - } - private void updateChannel(List channelRequests, Mentor mentor) { - List newChannels = buildChannels(channelRequests); + List newChannels = buildChannels(request.channels()); mentor.updateChannels(newChannels); } - @Transactional - public void createMentorMyPage(long siteUserId, MentorMyPageCreateRequest request) { - validateUserCanCreateMentor(siteUserId); - validateChannelRegistrationLimit(request.channels()); - MentorApplication mentorApplication = mentorApplicationRepository.findBySiteUserIdAndMentorApplicationStatus(siteUserId, MentorApplicationStatus.APPROVED) - .orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND)); - - Mentor mentor = new Mentor( - request.introduction(), - request.passTip(), - siteUserId, - mentorApplication.getUniversityId(), - mentorApplication.getTermId() - ); - - createChannels(request.channels(), mentor); - mentorRepository.save(mentor); - } - - private void validateUserCanCreateMentor(long siteUserId) { - if (mentorRepository.existsBySiteUserId(siteUserId)) { - throw new CustomException(MENTOR_ALREADY_EXISTS); - } - } - private void validateChannelRegistrationLimit(List channelRequests) { if (channelRequests.size() > CHANNEL_REGISTRATION_LIMIT) { throw new CustomException(CHANNEL_REGISTRATION_LIMIT_EXCEEDED); } } - private void createChannels(List channelRequests, Mentor mentor) { - List newChannels = buildChannels(channelRequests); - mentor.createChannels(newChannels); - } - private List buildChannels(List channelRequests) { int sequence = CHANNEL_SEQUENCE_START_NUMBER; List newChannels = new ArrayList<>(); diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java index a66b8d45d..656a12d23 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorQueryService.java @@ -19,8 +19,8 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.repository.TermRepository; -import com.example.solidconnection.university.domain.University; -import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepository; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,21 +38,21 @@ public class MentorQueryService { private final MentoringRepository mentoringRepository; private final SiteUserRepository siteUserRepository; private final MentorBatchQueryRepository mentorBatchQueryRepository; - private final UniversityRepository universityRepository; + private final HostUniversityRepository hostUniversityRepository; private final RegionRepository regionRepository; private final TermRepository termRepository; @Transactional(readOnly = true) - public MentorDetailResponse getMentorDetails(long mentorId, long currentUserId) { - Mentor mentor = mentorRepository.findById(mentorId) + public MentorDetailResponse getMentorDetails(long mentorSiteUserId, long currentUserId) { + Mentor mentor = mentorRepository.findBySiteUserId(mentorSiteUserId) .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); - University university = universityRepository.findById(mentor.getUniversityId()) + HostUniversity university = hostUniversityRepository.findById(mentor.getUniversityId()) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); SiteUser mentorUser = siteUserRepository.findById(mentor.getSiteUserId()) .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); Term term = termRepository.findById(mentor.getTermId()) .orElseThrow(() -> new CustomException(TERM_NOT_FOUND)); - boolean isApplied = mentoringRepository.existsByMentorIdAndMenteeId(mentorId, currentUserId); + boolean isApplied = mentoringRepository.existsByMentorIdAndMenteeId(mentor.getId(), currentUserId); return MentorDetailResponse.of(mentor, mentorUser, university, isApplied, term.getName()); } @@ -77,14 +77,14 @@ private Slice filterMentorsByRegion(String regionKoreanName, Pageable pa private List buildMentorPreviewsWithBatchQuery(List mentors, long currentUserId) { Map mentorIdToSiteUser = mentorBatchQueryRepository.getMentorIdToSiteUserMap(mentors); - Map mentorIdToUniversity = mentorBatchQueryRepository.getMentorIdToUniversityMap(mentors); + Map mentorIdToUniversity = mentorBatchQueryRepository.getMentorIdToUniversityMap(mentors); Map mentorIdToIsApplied = mentorBatchQueryRepository.getMentorIdToIsApplied(mentors, currentUserId); Map termIdToName = mentorBatchQueryRepository.getTermIdToNameMap(mentors); List mentorPreviews = new ArrayList<>(); for (Mentor mentor : mentors) { SiteUser mentorUser = mentorIdToSiteUser.get(mentor.getId()); - University university = mentorIdToUniversity.get(mentor.getId()); + HostUniversity university = mentorIdToUniversity.get(mentor.getId()); boolean isApplied = mentorIdToIsApplied.get(mentor.getId()); String termName = termIdToName.get(mentor.getTermId()); MentorPreviewResponse response = MentorPreviewResponse.of(mentor, mentorUser, university, isApplied, termName); diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java index 884526c1b..f764bf2d9 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java @@ -31,13 +31,17 @@ public class MentoringCommandService { @Transactional public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) { - long mentorId = mentoringApplyRequest.mentorId(); + long mentorSiteUserId = mentoringApplyRequest.mentorId(); + + Mentor mentor = mentorRepository.findBySiteUserId(mentorSiteUserId) + .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); + long mentorId = mentor.getId(); if (mentoringRepository.existsByMentorIdAndMenteeId(mentorId, siteUserId)) { throw new CustomException(ALREADY_EXIST_MENTORING); } - Mentoring mentoring = new Mentoring(mentoringApplyRequest.mentorId(), siteUserId, VerifyStatus.PENDING); + Mentoring mentoring = new Mentoring(mentorId, siteUserId, VerifyStatus.PENDING); return MentoringApplyResponse.from(mentoringRepository.save(mentoring)); } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java index e307d9e57..c21cc19a3 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringQueryService.java @@ -18,7 +18,7 @@ import com.example.solidconnection.mentor.repository.MentoringRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -58,7 +58,7 @@ public SliceResponse getMatchedMentors(long siteUserId, P private List buildMatchedMentorsWithBatchQuery(List mentors, long currentUserId) { Map mentorIdToSiteUser = mentorBatchQueryRepository.getMentorIdToSiteUserMap(mentors); - Map mentorIdToUniversity = mentorBatchQueryRepository.getMentorIdToUniversityMap(mentors); + Map mentorIdToUniversity = mentorBatchQueryRepository.getMentorIdToUniversityMap(mentors); Map mentorIdToIsApplied = mentorBatchQueryRepository.getMentorIdToIsApplied(mentors, currentUserId); Map termIdToName = mentorBatchQueryRepository.getTermIdToNameMap(mentors); @@ -67,7 +67,7 @@ private List buildMatchedMentorsWithBatchQuery(List matchedMentors = new ArrayList<>(); for (Mentor mentor : mentors) { SiteUser mentorUser = mentorIdToSiteUser.get(mentor.getId()); - University university = mentorIdToUniversity.get(mentor.getId()); + HostUniversity university = mentorIdToUniversity.get(mentor.getId()); boolean isApplied = mentorIdToIsApplied.get(mentor.getId()); Long roomId = mentorIdToRoomId.get(mentor.getId()); String termName = termIdToName.get(mentor.getTermId()); diff --git a/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java b/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java index ca1b262fe..916da99e3 100644 --- a/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java +++ b/src/main/java/com/example/solidconnection/news/service/NewsCommandService.java @@ -11,7 +11,7 @@ import com.example.solidconnection.news.dto.NewsCreateRequest; import com.example.solidconnection.news.dto.NewsUpdateRequest; import com.example.solidconnection.news.repository.NewsRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.Role; @@ -41,7 +41,7 @@ public NewsCommandResponse createNews(long siteUserId, NewsCreateRequest newsCre private String getImageUrl(MultipartFile imageFile) { if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadPath.NEWS); return uploadedFile.fileUrl(); } return newsProperties.defaultThumbnailUrl(); @@ -73,7 +73,7 @@ private void updateThumbnail(News news, MultipartFile imageFile, Boolean resetTo deleteCustomImage(news.getThumbnailUrl()); news.updateThumbnailUrl(newsProperties.defaultThumbnailUrl()); } else if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadPath.NEWS); deleteCustomImage(news.getThumbnailUrl()); news.updateThumbnailUrl(uploadedFile.fileUrl()); } diff --git a/src/main/java/com/example/solidconnection/community/post/service/RedisConstants.java b/src/main/java/com/example/solidconnection/redis/RedisConstants.java similarity index 77% rename from src/main/java/com/example/solidconnection/community/post/service/RedisConstants.java rename to src/main/java/com/example/solidconnection/redis/RedisConstants.java index 46260596c..58d6258cf 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/RedisConstants.java +++ b/src/main/java/com/example/solidconnection/redis/RedisConstants.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.community.post.service; +package com.example.solidconnection.redis; import lombok.Getter; @@ -15,7 +15,10 @@ public enum RedisConstants { REFRESH_LOCK_PREFIX("refresh_lock:"), LOCK_TIMEOUT_MS("10000"), MAX_WAIT_TIME_MS("3000"), - CREATE_CHANNEL("create_channel"); + CREATE_CHANNEL("create_channel"), + + POST_CREATE_PREFIX("post_create_lock:"), + VALIDATE_POST_CREATE_TTL("5"); private final String value; diff --git a/src/main/java/com/example/solidconnection/community/post/service/RedisService.java b/src/main/java/com/example/solidconnection/redis/RedisService.java similarity index 76% rename from src/main/java/com/example/solidconnection/community/post/service/RedisService.java rename to src/main/java/com/example/solidconnection/redis/RedisService.java index 7b701fc2b..602ae9a38 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/redis/RedisService.java @@ -1,7 +1,6 @@ -package com.example.solidconnection.community.post.service; +package com.example.solidconnection.redis; -import static com.example.solidconnection.community.post.service.RedisConstants.VALIDATE_VIEW_COUNT_TTL; -import static com.example.solidconnection.community.post.service.RedisConstants.VIEW_COUNT_TTL; +import static com.example.solidconnection.redis.RedisConstants.VIEW_COUNT_TTL; import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -34,12 +33,13 @@ public void deleteKey(String key) { } public Long getAndDelete(String key) { - return Long.valueOf(redisTemplate.opsForValue().getAndDelete(key)); + String value = redisTemplate.opsForValue().getAndDelete(key); + return value != null ? Long.valueOf(value) : null; } - public boolean isPresent(String key) { + public boolean isPresent(String key, String ttl) { return Boolean.TRUE.equals(redisTemplate.opsForValue() - .setIfAbsent(key, "1", Long.parseLong(VALIDATE_VIEW_COUNT_TTL.getValue()), TimeUnit.SECONDS)); + .setIfAbsent(key, "1", Long.parseLong(ttl), TimeUnit.SECONDS)); } public boolean isKeyExists(String key) { 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 3b19cecfa..69d3426a2 100644 --- a/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java +++ b/src/main/java/com/example/solidconnection/s3/config/AmazonS3Config.java @@ -1,12 +1,12 @@ package com.example.solidconnection.s3.config; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; 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.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; @Configuration public class AmazonS3Config { @@ -21,12 +21,12 @@ public class AmazonS3Config { private String region; @Bean - public AmazonS3Client amazonS3Client() { - BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); - return (AmazonS3Client) AmazonS3ClientBuilder - .standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(credentials)) + public S3Client s3Client() { + AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); + + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build(); } } diff --git a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java index 98b0574f9..8e98c863b 100644 --- a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java @@ -1,9 +1,9 @@ package com.example.solidconnection.s3.controller; import com.example.solidconnection.common.resolver.AuthorizedUser; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; -import com.example.solidconnection.s3.dto.urlPrefixResponse; +import com.example.solidconnection.s3.dto.UrlPrefixResponse; import com.example.solidconnection.s3.service.S3Service; import java.util.List; import lombok.RequiredArgsConstructor; @@ -39,7 +39,7 @@ public class S3Controller { public ResponseEntity uploadPreProfileImage( @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.PROFILE); return ResponseEntity.ok(profileImageUrl); } @@ -48,7 +48,7 @@ public ResponseEntity uploadPostProfileImage( @AuthorizedUser long siteUserId, @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.PROFILE); s3Service.deleteExProfile(siteUserId); return ResponseEntity.ok(profileImageUrl); } @@ -57,7 +57,7 @@ public ResponseEntity uploadPostProfileImage( public ResponseEntity uploadGpaImage( @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.GPA); return ResponseEntity.ok(profileImageUrl); } @@ -65,20 +65,20 @@ public ResponseEntity uploadGpaImage( public ResponseEntity uploadLanguageImage( @RequestParam("file") MultipartFile imageFile ) { - UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); + UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadPath.LANGUAGE_TEST); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/chat") - public ResponseEntity> uploadChatImage( - @RequestParam("files") List imageFiles + public ResponseEntity> uploadChatFile( + @RequestParam("files") List files ) { - List chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT); + List chatImageUrls = s3Service.uploadFiles(files, UploadPath.CHAT); return ResponseEntity.ok(chatImageUrls); } @GetMapping("/s3-url-prefix") - public ResponseEntity getS3UrlPrefix() { - return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded)); + public ResponseEntity getS3UrlPrefix() { + return ResponseEntity.ok(new UrlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded)); } } diff --git a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java b/src/main/java/com/example/solidconnection/s3/domain/ImgType.java deleted file mode 100644 index b26d5fc10..000000000 --- a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.solidconnection.s3.domain; - -import lombok.Getter; - -@Getter -public enum ImgType { - PROFILE("profile"), - GPA("gpa"), - LANGUAGE_TEST("language"), - COMMUNITY("community"), - NEWS("news"), - CHAT("chat"), - MENTOR_PROOF("mentor-proof"), - ; - - private final String type; - - ImgType(String type) { - this.type = type; - } -} diff --git a/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java b/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java new file mode 100644 index 000000000..f4f4d4d6e --- /dev/null +++ b/src/main/java/com/example/solidconnection/s3/domain/UploadPath.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.s3.domain; + +import com.example.solidconnection.common.constant.FileConstants; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import lombok.Getter; + +@Getter +public enum UploadPath { + PROFILE("profile"), + GPA("gpa"), + LANGUAGE_TEST("language"), + COMMUNITY("community"), + NEWS("news"), + CHAT("chat/files"), + MENTOR_PROOF("mentor-proof"), + ; + + private final String type; + + UploadPath(String type) { + this.type = type; + } + + public boolean isResizable(long fileSize, String extension, long maxSizeBytes) { + if (!isImage(extension)) return false; + + if (this == CHAT) return false; + + return fileSize >= maxSizeBytes; + } + public void validateExtension(String extension) { + if (extension == null || !FileConstants.ALL_ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) { + throw new CustomException(ErrorCode.NOT_ALLOWED_FILE_EXTENSIONS, + "허용된 형식: " + getAllowedExtensionsMessage()); + } + } + + public boolean isImage(String extension) { + return extension != null && FileConstants.IMAGE_EXTENSIONS.contains(extension.toLowerCase()); + } + + public String getAllowedExtensionsMessage() { + return String.join(", ", FileConstants.ALL_ALLOWED_EXTENSIONS); + } +} diff --git a/src/main/java/com/example/solidconnection/s3/dto/urlPrefixResponse.java b/src/main/java/com/example/solidconnection/s3/dto/UrlPrefixResponse.java similarity index 83% rename from src/main/java/com/example/solidconnection/s3/dto/urlPrefixResponse.java rename to src/main/java/com/example/solidconnection/s3/dto/UrlPrefixResponse.java index ab4d2f68b..42e163e1f 100644 --- a/src/main/java/com/example/solidconnection/s3/dto/urlPrefixResponse.java +++ b/src/main/java/com/example/solidconnection/s3/dto/UrlPrefixResponse.java @@ -1,6 +1,6 @@ package com.example.solidconnection.s3.dto; -public record urlPrefixResponse( +public record UrlPrefixResponse( String s3Default, String s3Uploaded, String cloudFrontDefault, diff --git a/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java b/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java index 51ef4caa9..1594c25dc 100644 --- a/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java +++ b/src/main/java/com/example/solidconnection/s3/service/FileUploadService.java @@ -3,48 +3,45 @@ import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION; import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.SdkClientException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.CannedAccessControlList; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; import com.example.solidconnection.common.exception.CustomException; -import java.io.IOException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; @Component -@EnableAsync @Slf4j +@RequiredArgsConstructor public class FileUploadService { - private final AmazonS3Client amazonS3; - - public FileUploadService(AmazonS3Client amazonS3) { - this.amazonS3 = amazonS3; - } + private final S3Client s3Client; @Async - public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) { - // 메타데이터 생성 - String contentType = multipartFile.getContentType(); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(multipartFile.getSize()); - + public void uploadFile(String bucket, String fileName, byte[] content, String contentType) { try { - amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata) - .withCannedAcl(CannedAccessControlList.PublicRead)); - log.info("이미지 업로드 정상적 완료 thread: {}", Thread.currentThread().getName()); - } catch (AmazonServiceException e) { - log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage()); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .contentType(contentType) + .contentLength((long) content.length) + .build(); + + s3Client.putObject(putObjectRequest, RequestBody.fromBytes(content)); + + log.info("파일 업로드 정상 완료 thread: {}", Thread.currentThread().getName()); + } catch (S3Exception e) { + String errorMessage = (e.awsErrorDetails() != null) + ? e.awsErrorDetails().errorMessage() + : e.getMessage(); + log.error("S3 서비스 예외 발생 : {}", errorMessage); throw new CustomException(S3_SERVICE_EXCEPTION); - } catch (SdkClientException | IOException e) { - log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); + } catch (SdkException e) { + log.error("S3 클라이언트 또는 SDK 예외 발생 : {}", e.getMessage()); throw new CustomException(S3_CLIENT_EXCEPTION); } } diff --git a/src/main/java/com/example/solidconnection/s3/service/S3Service.java b/src/main/java/com/example/solidconnection/s3/service/S3Service.java index 4c4110693..6dc3004e6 100644 --- a/src/main/java/com/example/solidconnection/s3/service/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/service/S3Service.java @@ -2,45 +2,39 @@ import static com.example.solidconnection.common.exception.ErrorCode.FILE_NOT_EXIST; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_FILE_EXTENSIONS; -import static com.example.solidconnection.common.exception.ErrorCode.NOT_ALLOWED_FILE_EXTENSIONS; import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION; import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.SdkClientException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import jakarta.transaction.Transactional; +import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; @Service @RequiredArgsConstructor public class S3Service { - private static final Logger log = LoggerFactory.getLogger(S3Service.class); private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5; - private final AmazonS3Client amazonS3; + private final S3Client s3Client; private final SiteUserRepository siteUserRepository; private final FileUploadService fileUploadService; - private final ThreadPoolTaskExecutor asyncExecutor; @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -55,47 +49,56 @@ public class S3Service { * - 5mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다. * - 5mb 미만의 파일은 바로 업로드한다. * */ - public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) { - // 파일 검증 - validateImgFile(multipartFile); - // 파일 이름 생성 + public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, UploadPath uploadPath) { + validateFile(multipartFile, uploadPath); + UUID randomUUID = UUID.randomUUID(); - String fileName = imageFile.getType() + "/" + randomUUID; - // 파일업로드 비동기로 진행 - if (multipartFile.getSize() >= MAX_FILE_SIZE_MB) { - asyncExecutor.submit(() -> { - fileUploadService.uploadFile(bucket, "origin/" + fileName, multipartFile); - }); - } else { - asyncExecutor.submit(() -> { - fileUploadService.uploadFile(bucket, fileName, multipartFile); - }); + String extension = getFileExtension(Objects.requireNonNull(multipartFile.getOriginalFilename())); + String baseFileName = randomUUID + "." + extension; + String fileName = uploadPath.getType() + "/" + baseFileName; + + final boolean shouldResize = uploadPath.isResizable( + multipartFile.getSize(), extension, MAX_FILE_SIZE_MB); + + final String originalPath = shouldResize ? "original/" + fileName : fileName; + final String returnPath = shouldResize + ? "resize/" + fileName.substring(0, fileName.lastIndexOf('.')) + ".webp" + : fileName; + + byte[] bytes = extractBytes(multipartFile); + String contentType = multipartFile.getContentType(); + + fileUploadService.uploadFile(bucket, originalPath, bytes, contentType); + + return new UploadedFileUrlResponse(returnPath); + } + + private byte[] extractBytes(MultipartFile file) { + try { + return file.getBytes(); + } catch (IOException e) { + throw new CustomException(S3_CLIENT_EXCEPTION); } - return new UploadedFileUrlResponse(fileName); } - public List uploadFiles(List multipartFile, ImgType imageFile) { + public List uploadFiles(List multipartFile, UploadPath uploadPath) { List uploadedFileUrlResponseList = new ArrayList<>(); for (MultipartFile file : multipartFile) { - UploadedFileUrlResponse uploadedFileUrlResponse = uploadFile(file, imageFile); + UploadedFileUrlResponse uploadedFileUrlResponse = uploadFile(file, uploadPath); uploadedFileUrlResponseList.add(uploadedFileUrlResponse); } return uploadedFileUrlResponseList; } - private void validateImgFile(MultipartFile file) { + private void validateFile(MultipartFile file, UploadPath uploadPath) { if (file == null || file.isEmpty()) { throw new CustomException(FILE_NOT_EXIST); } String fileName = Objects.requireNonNull(file.getOriginalFilename()); String fileExtension = getFileExtension(fileName).toLowerCase(); - - List allowedExtensions = Arrays.asList("jpg", "jpeg", "png", "webp", "pdf", "word", "docx"); - if (!allowedExtensions.contains(fileExtension)) { - throw new CustomException(NOT_ALLOWED_FILE_EXTENSIONS, "허용된 형식: " + allowedExtensions); - } + uploadPath.validateExtension(fileExtension); } private String getFileExtension(String fileName) { @@ -125,12 +128,14 @@ public void deletePostImage(String url) { private void deleteFile(String fileName) { try { - amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); - } catch (AmazonServiceException e) { - log.error("파일 삭제 중 s3 서비스 예외 발생 : {}", e.getMessage()); + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(bucket) + .key(fileName) + .build(); + s3Client.deleteObject(deleteObjectRequest); + } catch (S3Exception e) { throw new CustomException(S3_SERVICE_EXCEPTION); - } catch (SdkClientException e) { - log.error("파일 삭제 중 s3 클라이언트 예외 발생 : {}", e.getMessage()); + } catch (SdkException e) { throw new CustomException(S3_CLIENT_EXCEPTION); } } diff --git a/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java b/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java index 9a5561728..202d8f05c 100644 --- a/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java +++ b/src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java @@ -1,6 +1,6 @@ package com.example.solidconnection.scheduler; -import static com.example.solidconnection.community.post.service.RedisConstants.VIEW_COUNT_KEY_PATTERN; +import static com.example.solidconnection.redis.RedisConstants.VIEW_COUNT_KEY_PATTERN; import com.example.solidconnection.community.post.service.UpdateViewCountService; import com.example.solidconnection.util.RedisUtils; diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index f16951d49..f1f055c81 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -5,7 +5,7 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.score.domain.GpaScore; @@ -40,7 +40,7 @@ public class ScoreService { public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, MultipartFile file) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.GPA); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadPath.GPA); Gpa gpa = new Gpa(gpaScoreRequest.gpa(), gpaScoreRequest.gpaCriteria(), uploadedFile.fileUrl()); GpaScore newGpaScore = new GpaScore(gpa, siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); @@ -51,7 +51,7 @@ public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, Mul public Long submitLanguageTestScore(long siteUserId, LanguageTestScoreRequest languageTestScoreRequest, MultipartFile file) { SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.LANGUAGE_TEST); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadPath.LANGUAGE_TEST); LanguageTest languageTest = new LanguageTest(languageTestScoreRequest.languageTestType(), languageTestScoreRequest.languageTestScore(), uploadedFile.fileUrl()); LanguageTestScore newScore = new LanguageTestScore(languageTest, siteUser); diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index a82291d75..f6c3f3b33 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -130,4 +130,8 @@ public void updatePassword(String newEncodedPassword) { public void updateUserStatus(UserStatus status) { this.userStatus = status; } + + public void becomeMentor() { + this.role = Role.MENTOR; + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java index 6e8b88b66..ef6d985e5 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/MyPageService.java @@ -14,7 +14,7 @@ import com.example.solidconnection.location.country.repository.CountryRepository; import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.mentor.repository.MentorRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.AuthType; @@ -24,9 +24,9 @@ import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.dto.PasswordUpdateRequest; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.repository.LikedUnivApplyInfoRepository; -import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.repository.HostUniversityRepository; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; @@ -48,7 +48,7 @@ public class MyPageService { private final LikedUnivApplyInfoRepository likedUnivApplyInfoRepository; private final CountryRepository countryRepository; private final MentorRepository mentorRepository; - private final UniversityRepository universityRepository; + private final HostUniversityRepository hostUniversityRepository; private final S3Service s3Service; private final InterestedCountryService interestedCountryService; private final InterestedRegionService interestedRegionService; @@ -69,7 +69,7 @@ public MyPageResponse getMyPageInfo(long siteUserId) { } else if (siteUser.getRole() == Role.MENTOR) { Mentor mentor = mentorRepository.findBySiteUserId(siteUser.getId()) .orElseThrow(() -> new CustomException(MENTOR_NOT_FOUND)); - University university = universityRepository.findById(mentor.getUniversityId()) + HostUniversity university = hostUniversityRepository.findById(mentor.getUniversityId()) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); universityKoreanName = university.getKoreanName(); } @@ -90,9 +90,8 @@ public void updateMyPageInfo(long siteUserId, MultipartFile imageFile, String ni user.setNickname(nickname); user.setNicknameModifiedAt(LocalDateTime.now()); } - if (imageFile != null && !imageFile.isEmpty()) { - UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.PROFILE); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadPath.PROFILE); if (!isDefaultProfileImage(user.getProfileImageUrl())) { s3Service.deleteExProfile(user.getId()); } diff --git a/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java b/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java index 0a258ef12..c6ea2a2ae 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UnivApplyInfoController.java @@ -3,20 +3,17 @@ import com.example.solidconnection.common.resolver.AuthorizedUser; import com.example.solidconnection.university.dto.IsLikeResponse; import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse; -import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest; import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse; import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses; import com.example.solidconnection.university.dto.UnivApplyInfoRecommendsResponse; import com.example.solidconnection.university.service.LikedUnivApplyInfoService; import com.example.solidconnection.university.service.UnivApplyInfoQueryService; import com.example.solidconnection.university.service.UnivApplyInfoRecommendService; -import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; 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.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -87,14 +84,6 @@ public ResponseEntity getUnivApplyInfoDetails( return ResponseEntity.ok(univApplyInfoDetailResponse); } - @GetMapping("/search/filter") - public ResponseEntity searchUnivApplyInfoByFilter( - @Valid @ModelAttribute UnivApplyInfoFilterSearchRequest request - ) { - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request); - return ResponseEntity.ok(response); - } - @GetMapping("/search/text") public ResponseEntity searchUnivApplyInfoByText( @RequestParam(required = false) String value diff --git a/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java b/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java new file mode 100644 index 000000000..ef9cc01bd --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/domain/HomeUniversity.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.university.domain; + +import com.example.solidconnection.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class HomeUniversity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 100) + private String name; +} diff --git a/src/main/java/com/example/solidconnection/university/domain/University.java b/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java similarity index 59% rename from src/main/java/com/example/solidconnection/university/domain/University.java rename to src/main/java/com/example/solidconnection/university/domain/HostUniversity.java index 0b55c3bee..6e9f4b4c6 100644 --- a/src/main/java/com/example/solidconnection/university/domain/University.java +++ b/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java @@ -18,13 +18,13 @@ @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class University extends BaseEntity { +public class HostUniversity extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 100) + @Column(nullable = false, unique = true, length = 100) private String koreanName; @Column(nullable = false, length = 100) @@ -56,4 +56,30 @@ public class University extends BaseEntity { @ManyToOne private Region region; + + public void update( + String koreanName, + String englishName, + String formatName, + String homepageUrl, + String englishCourseUrl, + String accommodationUrl, + String logoImageUrl, + String backgroundImageUrl, + String detailsForLocal, + Country country, + Region region + ) { + this.koreanName = koreanName; + this.englishName = englishName; + this.formatName = formatName; + this.homepageUrl = homepageUrl; + this.englishCourseUrl = englishCourseUrl; + this.accommodationUrl = accommodationUrl; + this.logoImageUrl = logoImageUrl; + this.backgroundImageUrl = backgroundImageUrl; + this.detailsForLocal = detailsForLocal; + this.country = country; + this.region = region; + } } diff --git a/src/main/java/com/example/solidconnection/university/domain/UnivApplyInfo.java b/src/main/java/com/example/solidconnection/university/domain/UnivApplyInfo.java index 7d2fe526d..64d264f35 100644 --- a/src/main/java/com/example/solidconnection/university/domain/UnivApplyInfo.java +++ b/src/main/java/com/example/solidconnection/university/domain/UnivApplyInfo.java @@ -10,6 +10,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @@ -36,6 +37,10 @@ public class UnivApplyInfo extends BaseEntity { @Column(nullable = false, name = "term_id") private long termId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "home_university_id") + private HomeUniversity homeUniversity; + @Column(nullable = false, length = 100) private String koreanName; @@ -81,7 +86,7 @@ public class UnivApplyInfo extends BaseEntity { private Set languageRequirements = new HashSet<>(); @ManyToOne(fetch = FetchType.LAZY) - private University university; + private HostUniversity university; public void addLanguageRequirements(LanguageRequirement languageRequirements) { this.languageRequirements.add(languageRequirements); diff --git a/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoDetailResponse.java b/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoDetailResponse.java index 9d4fa3bbf..e2425c237 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoDetailResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoDetailResponse.java @@ -1,7 +1,7 @@ package com.example.solidconnection.university.dto; import com.example.solidconnection.university.domain.UnivApplyInfo; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import java.util.List; public record UnivApplyInfoDetailResponse( @@ -33,7 +33,7 @@ public record UnivApplyInfoDetailResponse( String englishCourseUrl) { public static UnivApplyInfoDetailResponse of( - University university, + HostUniversity university, UnivApplyInfo univApplyInfo, String termName ) { diff --git a/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoFilterSearchRequest.java b/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoFilterSearchRequest.java deleted file mode 100644 index a49079319..000000000 --- a/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoFilterSearchRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.solidconnection.university.dto; - -import com.example.solidconnection.university.domain.LanguageTestType; -import jakarta.validation.constraints.NotNull; -import java.util.List; - -public record UnivApplyInfoFilterSearchRequest( - - @NotNull(message = "어학 시험 종류를 선택해주세요.") - LanguageTestType languageTestType, - String testScore, - List countryCode -) { - -} diff --git a/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoPreviewResponse.java index 37deee8f2..1a6d1ce93 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UnivApplyInfoPreviewResponse.java @@ -9,6 +9,7 @@ public record UnivApplyInfoPreviewResponse( long id, String term, String koreanName, + String homeUniversityName, String region, String country, String logoImageUrl, @@ -16,17 +17,22 @@ public record UnivApplyInfoPreviewResponse( int studentCapacity, List languageRequirements) { - public static UnivApplyInfoPreviewResponse from(UnivApplyInfo univApplyInfo, String termName) { + public static UnivApplyInfoPreviewResponse of(UnivApplyInfo univApplyInfo, String termName) { List languageRequirementResponses = new ArrayList<>( univApplyInfo.getLanguageRequirements().stream() .map(LanguageRequirementResponse::from) .toList()); Collections.sort(languageRequirementResponses); + String homeUniversityName = univApplyInfo.getHomeUniversity() != null + ? univApplyInfo.getHomeUniversity().getName() + : null; + return new UnivApplyInfoPreviewResponse( univApplyInfo.getId(), termName, univApplyInfo.getKoreanName(), + homeUniversityName, univApplyInfo.getUniversity().getRegion().getKoreanName(), univApplyInfo.getUniversity().getCountry().getKoreanName(), univApplyInfo.getUniversity().getLogoImageUrl(), diff --git a/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java new file mode 100644 index 000000000..0cfc0593c --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/HomeUniversityRepository.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.university.repository; + +import com.example.solidconnection.university.domain.HomeUniversity; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HomeUniversityRepository extends JpaRepository { + + List findAllByIdIn(List ids); + + Optional findByName(String name); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java new file mode 100644 index 000000000..3fa80629a --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.university.repository; + +import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.custom.HostUniversityFilterRepository; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HostUniversityRepository extends JpaRepository, HostUniversityFilterRepository { + + default HostUniversity getHostUniversityById(Long id) { + return findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + } + + Optional findByKoreanName(String koreanName); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java b/src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java index 684703b58..e5a94d69a 100644 --- a/src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java @@ -17,8 +17,13 @@ public interface LikedUnivApplyInfoRepository extends JpaRepository findBySiteUserIdAndUnivApplyInfoId(long siteUserId, long univApplyInfoId); @Query(""" - SELECT u + SELECT DISTINCT u FROM UnivApplyInfo u + LEFT JOIN FETCH u.languageRequirements lr + LEFT JOIN FETCH u.homeUniversity hu + LEFT JOIN FETCH u.university univ + LEFT JOIN FETCH univ.country c + LEFT JOIN FETCH univ.region r JOIN LikedUnivApplyInfo l ON u.id = l.univApplyInfoId WHERE l.siteUserId = :siteUserId """) 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 5bb56cfc0..e0b71f8a9 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java @@ -19,6 +19,7 @@ public interface UnivApplyInfoRepository extends JpaRepository findAllByIds(@Param("ids") List ids); + + boolean existsByUniversityId(Long universityId); } diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java deleted file mode 100644 index 15210c18d..000000000 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.solidconnection.university.repository; - -import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND; - -import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.university.domain.University; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UniversityRepository extends JpaRepository { - - default University getUniversityById(Long id) { - return findById(id) - .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); - } -} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java new file mode 100644 index 000000000..1e6cbc01f --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.university.domain.HostUniversity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface HostUniversityFilterRepository { + + Page findAllBySearchCondition( + String keyword, + String countryCode, + String regionCode, + Pageable pageable + ); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java new file mode 100644 index 000000000..e53ff4f2c --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.location.country.domain.QCountry; +import com.example.solidconnection.location.region.domain.QRegion; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.domain.QHostUniversity; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +public class HostUniversityFilterRepositoryImpl implements HostUniversityFilterRepository { + + private final JPAQueryFactory queryFactory; + + @Autowired + public HostUniversityFilterRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findAllBySearchCondition( + String keyword, + String countryCode, + String regionCode, + Pageable pageable + ) { + QHostUniversity hostUniversity = QHostUniversity.hostUniversity; + QCountry country = QCountry.country; + QRegion region = QRegion.region; + + List content = queryFactory + .selectFrom(hostUniversity) + .leftJoin(hostUniversity.country, country).fetchJoin() + .leftJoin(hostUniversity.region, region).fetchJoin() + .where( + keywordContains(hostUniversity, keyword), + countryCodeEq(country, countryCode), + regionCodeEq(region, regionCode) + ) + .orderBy(hostUniversity.id.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(hostUniversity.count()) + .from(hostUniversity) + .leftJoin(hostUniversity.country, country) + .leftJoin(hostUniversity.region, region) + .where( + keywordContains(hostUniversity, keyword), + countryCodeEq(country, countryCode), + regionCodeEq(region, regionCode) + ); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + private BooleanExpression keywordContains(QHostUniversity hostUniversity, String keyword) { + if (keyword == null || keyword.isBlank()) { + return null; + } + return hostUniversity.koreanName.contains(keyword) + .or(hostUniversity.englishName.containsIgnoreCase(keyword)); + } + + private BooleanExpression countryCodeEq(QCountry country, String countryCode) { + if (countryCode == null || countryCode.isBlank()) { + return null; + } + return country.code.eq(countryCode); + } + + private BooleanExpression regionCodeEq(QRegion region, String regionCode) { + if (regionCode == null || regionCode.isBlank()) { + return null; + } + return region.code.eq(regionCode); + } +} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java index 5e2a0b906..31dfe3553 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepository.java @@ -1,6 +1,5 @@ package com.example.solidconnection.university.repository.custom; -import com.example.solidconnection.university.domain.LanguageTestType; import com.example.solidconnection.university.domain.UnivApplyInfo; import java.util.List; @@ -8,7 +7,5 @@ public interface UnivApplyInfoFilterRepository { List findAllByRegionCodeAndKeywordsAndTermId(String regionCode, List keywords, Long term); - List findAllByFilter(LanguageTestType testType, String testScore, Long termId, List countryKoreanNames); - List findAllByText(String text, Long termId); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java index 7e8da9e40..cfb5e3a7a 100644 --- a/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java +++ b/src/main/java/com/example/solidconnection/university/repository/custom/UnivApplyInfoFilterRepositoryImpl.java @@ -2,10 +2,10 @@ import com.example.solidconnection.location.country.domain.QCountry; import com.example.solidconnection.location.region.domain.QRegion; -import com.example.solidconnection.university.domain.LanguageTestType; +import com.example.solidconnection.university.domain.QHomeUniversity; +import com.example.solidconnection.university.domain.QHostUniversity; import com.example.solidconnection.university.domain.QLanguageRequirement; import com.example.solidconnection.university.domain.QUnivApplyInfo; -import com.example.solidconnection.university.domain.QUniversity; import com.example.solidconnection.university.domain.UnivApplyInfo; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.BooleanExpression; @@ -33,7 +33,8 @@ public UnivApplyInfoFilterRepositoryImpl(EntityManager em) { @Override public List findAllByRegionCodeAndKeywordsAndTermId(String regionCode, List keywords, Long termId) { QUnivApplyInfo univApplyInfo = QUnivApplyInfo.univApplyInfo; - QUniversity university = QUniversity.university; + QHostUniversity university = QHostUniversity.hostUniversity; + QHomeUniversity homeUniversity = QHomeUniversity.homeUniversity; QCountry country = QCountry.country; QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement; @@ -41,6 +42,7 @@ public List findAllByRegionCodeAndKeywordsAndTermId(String region .selectFrom(univApplyInfo) .join(univApplyInfo.university, university).fetchJoin() .join(university.country, country).fetchJoin() + .leftJoin(univApplyInfo.homeUniversity, homeUniversity).fetchJoin() .leftJoin(univApplyInfo.languageRequirements, languageRequirement).fetchJoin() .where( regionCodeEq(country, regionCode) @@ -58,7 +60,7 @@ private BooleanExpression regionCodeEq(QCountry country, String regionCode) { return country.regionCode.eq(regionCode); } - private BooleanExpression countryOrUniversityContainsKeyword(QCountry country, QUniversity university, List keywords) { + private BooleanExpression countryOrUniversityContainsKeyword(QCountry country, QHostUniversity university, List keywords) { if (keywords == null || keywords.isEmpty()) { return Expressions.TRUE; } @@ -74,51 +76,6 @@ private BooleanExpression createKeywordCondition(StringPath namePath, List findAllByFilter( - LanguageTestType testType, String testScore, Long termId, List countryCodes - ) { - QUniversity university = QUniversity.university; - QUnivApplyInfo univApplyInfo = QUnivApplyInfo.univApplyInfo; - QCountry country = QCountry.country; - QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement; - - List filteredUnivApplyInfo = queryFactory.selectFrom(univApplyInfo) - .join(univApplyInfo.university, university) - .join(university.country, country) - .join(univApplyInfo.languageRequirements, languageRequirement) - .fetchJoin() - .where( - languageTestTypeEq(languageRequirement, testType), - termIdEq(univApplyInfo, termId), - countryCodesIn(country, countryCodes) - ) - .distinct() - .fetch(); - - if (testScore == null || testScore.isBlank()) { - return filteredUnivApplyInfo; - } - - /* - * 시험 유형에 따라 성적 비교 방식이 다르다. - * 입력된 점수가 대학에서 요구하는 최소 점수보다 높은지를 '쿼리로' 비교하기엔 쿼리가 지나치게 복잡해진다. - * 따라서 이 부분만 자바 코드로 필터링한다. - * */ - return filteredUnivApplyInfo.stream() - .filter(uai -> isGivenScoreOverMinPassScore(uai, testType, testScore)) - .toList(); - } - - private BooleanExpression languageTestTypeEq( - QLanguageRequirement languageRequirement, LanguageTestType givenTestType - ) { - if (givenTestType == null) { - return null; - } - return languageRequirement.languageTestType.eq(givenTestType); - } - private BooleanExpression termIdEq(QUnivApplyInfo univApplyInfo, Long givenTermId) { if (givenTermId == null) { return null; @@ -126,27 +83,11 @@ private BooleanExpression termIdEq(QUnivApplyInfo univApplyInfo, Long givenTermI return univApplyInfo.termId.eq(givenTermId); } - private BooleanExpression countryCodesIn(QCountry country, List givenCountryCodes) { - if (givenCountryCodes == null || givenCountryCodes.isEmpty()) { - return null; - } - return country.code.in(givenCountryCodes); - } - - private boolean isGivenScoreOverMinPassScore( - UnivApplyInfo univApplyInfo, LanguageTestType givenTestType, String givenTestScore - ) { - return univApplyInfo.getLanguageRequirements().stream() - .filter(languageRequirement -> languageRequirement.getLanguageTestType().equals(givenTestType)) - .findFirst() - .map(requirement -> givenTestType.compare(givenTestScore, requirement.getMinScore())) - .orElse(-1) >= 0; - } - @Override public List findAllByText(String text, Long termId) { QUnivApplyInfo univApplyInfo = QUnivApplyInfo.univApplyInfo; - QUniversity university = QUniversity.university; + QHostUniversity university = QHostUniversity.hostUniversity; + QHomeUniversity homeUniversity = QHomeUniversity.homeUniversity; QLanguageRequirement languageRequirement = QLanguageRequirement.languageRequirement; QCountry country = QCountry.country; QRegion region = QRegion.region; @@ -155,6 +96,7 @@ public List findAllByText(String text, Long termId) { .join(univApplyInfo.university, university).fetchJoin() .join(university.country, country).fetchJoin() .join(region).on(country.regionCode.eq(region.code)) + .leftJoin(univApplyInfo.homeUniversity, homeUniversity).fetchJoin() .leftJoin(univApplyInfo.languageRequirements, languageRequirement).fetchJoin() .where(termIdEq(univApplyInfo, termId)); diff --git a/src/main/java/com/example/solidconnection/university/service/LikedUnivApplyInfoService.java b/src/main/java/com/example/solidconnection/university/service/LikedUnivApplyInfoService.java index c353edaf0..83ee9feb6 100644 --- a/src/main/java/com/example/solidconnection/university/service/LikedUnivApplyInfoService.java +++ b/src/main/java/com/example/solidconnection/university/service/LikedUnivApplyInfoService.java @@ -18,7 +18,6 @@ import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,7 +45,7 @@ public List getLikedUnivApplyInfos(long siteUserId return univApplyInfos.stream() .map(univApplyInfo -> { String termName = termMap.getOrDefault(univApplyInfo.getTermId(), "Unknown"); - return UnivApplyInfoPreviewResponse.from(univApplyInfo, termName); + return UnivApplyInfoPreviewResponse.of(univApplyInfo, termName); }) .toList(); } diff --git a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java index c7713fd27..5c11911ad 100644 --- a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java +++ b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoQueryService.java @@ -7,10 +7,9 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.repository.TermRepository; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.domain.UnivApplyInfo; -import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse; -import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest; import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse; import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses; import com.example.solidconnection.university.repository.UnivApplyInfoRepository; @@ -35,41 +34,23 @@ public class UnivApplyInfoQueryService { public UnivApplyInfoDetailResponse getUnivApplyInfoDetail(Long univApplyInfoId) { UnivApplyInfo univApplyInfo = univApplyInfoRepository.getUnivApplyInfoById(univApplyInfoId); - University university = univApplyInfo.getUniversity(); + HostUniversity university = univApplyInfo.getUniversity(); Term term = termRepository.findById(univApplyInfo.getTermId()) .orElseThrow(() -> new CustomException(TERM_NOT_FOUND)); return UnivApplyInfoDetailResponse.of(university, univApplyInfo, term.getName()); } - @Transactional(readOnly = true) - public UnivApplyInfoPreviewResponses searchUnivApplyInfoByFilter(UnivApplyInfoFilterSearchRequest request) { - Term term = termRepository.findByIsCurrentTrue() - .orElseThrow(() -> new CustomException(CURRENT_TERM_NOT_FOUND)); - - List responses = univApplyInfoRepository - .findAllByFilter(request.languageTestType(), request.testScore(), term.getId(), request.countryCode()) - .stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.from( - univApplyInfo, - term.getName() - )) - .toList(); - return new UnivApplyInfoPreviewResponses(responses); - } - @Transactional(readOnly = true) @ThunderingHerdCaching(key = "univApplyInfoTextSearch:{0}", cacheManager = "customCacheManager", ttlSec = 86400) public UnivApplyInfoPreviewResponses searchUnivApplyInfoByText(String text) { Term term = termRepository.findByIsCurrentTrue() .orElseThrow(() -> new CustomException(CURRENT_TERM_NOT_FOUND)); - List responses = univApplyInfoRepository.findAllByText(text, term.getId()) - .stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.from( - univApplyInfo, - term.getName() - )) + List univApplyInfos = univApplyInfoRepository.findAllByText(text, term.getId()); + + List responses = univApplyInfos.stream() + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) .toList(); return new UnivApplyInfoPreviewResponses(responses); } diff --git a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java index a6a2b85fc..872524d63 100644 --- a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java @@ -51,11 +51,8 @@ public UnivApplyInfoRecommendsResponse getPersonalRecommends(long siteUserId) { } return new UnivApplyInfoRecommendsResponse(trimmedRecommends.stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.from( - univApplyInfo, - term.getName() - )) - .toList()); + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) + .toList()); } private List getGeneralRecommendsExcludingSelected(List alreadyPicked) { @@ -76,11 +73,9 @@ public UnivApplyInfoRecommendsResponse getGeneralRecommends() { .orElseThrow(() -> new CustomException(CURRENT_TERM_NOT_FOUND)); List generalRecommends = new ArrayList<>(generalUnivApplyInfoRecommendService.getGeneralRecommends()); + return new UnivApplyInfoRecommendsResponse(generalRecommends.stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.from( - univApplyInfo, - term.getName() - )) - .toList()); + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) + .toList()); } } diff --git a/src/main/java/com/example/solidconnection/util/RedisUtils.java b/src/main/java/com/example/solidconnection/util/RedisUtils.java index df4d7572d..a18fe1791 100644 --- a/src/main/java/com/example/solidconnection/util/RedisUtils.java +++ b/src/main/java/com/example/solidconnection/util/RedisUtils.java @@ -1,9 +1,7 @@ package com.example.solidconnection.util; -import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_LOCK_PREFIX; -import static com.example.solidconnection.community.post.service.RedisConstants.REFRESH_LOCK_PREFIX; -import static com.example.solidconnection.community.post.service.RedisConstants.VALIDATE_VIEW_COUNT_KEY_PREFIX; -import static com.example.solidconnection.community.post.service.RedisConstants.VIEW_COUNT_KEY_PREFIX; +import static com.example.solidconnection.redis.RedisConstants.CREATE_LOCK_PREFIX; +import static com.example.solidconnection.redis.RedisConstants.REFRESH_LOCK_PREFIX; import java.util.Collections; import java.util.Comparator; @@ -39,18 +37,6 @@ public Long getExpirationTime(String key) { return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); } - public String getPostViewCountRedisKey(Long postId) { - return VIEW_COUNT_KEY_PREFIX.getValue() + postId; - } - - public String getValidatePostViewCountRedisKey(long siteUserId, Long postId) { - return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + siteUserId; - } - - public Long getPostIdFromPostViewCountRedisKey(String key) { - return Long.parseLong(key.substring(VIEW_COUNT_KEY_PREFIX.getValue().length())); - } - public String generateCacheKey(String keyPattern, Object[] args) { for (int i = 0; i < args.length; i++) { // 키 패턴에 {i}가 포함된 경우에만 해당 인덱스의 파라미터를 삽입 diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 3e17fb27c..3fa29bd0f 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -48,7 +48,9 @@ VALUES ('test@test.email', 'yonso', 'https://github.com/nayonsoso.png', 'CONSIDERING', 'MENTEE', '$2a$10$psmwlxPfqWnIlq9JrlQJkuXr1XtjRNsyVOgcTWYZub5jFfn0TML76', 'EMAIL'); -- 12341234 -INSERT INTO university(id, country_code, region_code, english_name, format_name, korean_name, +INSERT INTO home_university (id, name) VALUES (1, '인하대학교'); + +INSERT INTO host_university(id, country_code, region_code, english_name, format_name, korean_name, accommodation_url, english_course_url, homepage_url, details_for_local, logo_image_url, background_image_url) VALUES (1, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대학', @@ -146,81 +148,81 @@ VALUES (1, 'US', 'AMERICAS', 'University of Guam', 'university_of_guam', '괌대 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/logo.png', 'https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/bunkyo_gakuin_university/1.png'); -INSERT INTO university_info_for_apply(term_id, university_id, korean_name, semester_requirement, student_capacity, +INSERT INTO university_info_for_apply(term_id, university_id, home_university_id, korean_name, semester_requirement, student_capacity, semester_available_for_dispatch, tuition_fee_type, details_for_major, details_for_apply, details_for_language, details_for_english_course, details_for_accommodation, details) -VALUES (1, 1, '괌대학(A형)', 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, +VALUES (1, 1, 1, '괌대학(A형)', 2, 1, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, NULL), - (1, 1, '괌대학(B형)', 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, + (1, 1, 1, '괌대학(B형)', 2, 2, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함', NULL, NULL, '등록금 관련 정보: https://www.uog.edu/financial-aid/cost-to-attend'), - (1, 2, '네바다주립대학 라스베이거스(B형)', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + (1, 2, 1, '네바다주립대학 라스베이거스(B형)', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 지원가능전공: 공학계열 관련 전공자
- 파견대학에 지원하는 전공과 본교 전공이 일치해야함', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS : 모든 영역에서 5.5 이상', NULL, NULL, ' - The Engineering International Programs (EIP) Programs 안의 글로벌 하이브리드 프로그램으로 선발됨
※ 하이브리드 프로그램: 정규 과목 + 비정규 General Education Courses 과목 수강으로 구성, 정규(약 6학점) / 비정규 (약 135시간 이상) 수업 수강 (세부사항 변동 가능)
- 기숙사가 있지만 기숙사 확정이 늦게 발표되고 전원보장이 어려워, 외부숙소로 진행될 수도 있음, 한 학기 기숙사 비용: 약 $4,500~$6,000
- 한 학기 등록금: 약 $7,500
- International Program and Service Fees $2,500'), - (1, 3, '메모리얼 대학 세인트존스(A형)', 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + (1, 3, 1, '메모리얼 대학 세인트존스(A형)', 2, 4, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, NULL), - (1, 3, '메모리얼 대학 세인트존스(B형)', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + (1, 3, 1, '메모리얼 대학 세인트존스(B형)', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- 지원불가능전공: Medicine, Pharmacy, Social work, Nursing
- Computer Science, Music 지원 제한적', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- TOEFL iBT : 읽기/쓰기 20점, 듣기/말하기 17점 이상
- IELTS : 모든 영역에서 6.0 이상
- 외국어 성적 유효기간이 파견대학의 학기 시작하는 날까지 유효해야함 ', NULL, NULL, '국제학생 등록금 적용 (학점당 $2,080)'), - (1, 4, '서던퀸스랜드대학(B형)', 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', + (1, 4, 1, '서던퀸스랜드대학(B형)', 2, 5, 'ONE_SEMESTER', 'OVERSEAS_UNIVERSITY_PAYMENT', '- 타전공 지원 및 수강 가능
- 미술 계열, 간호학, 약학, 교육학 등 제한 있음
- 학과별 지원 자격요건이 있는 경우 모두 충족해야 하며, 사전 승인 필요', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- IELTS: 각 영역 최소 5.5 이상
- 외국어 성적 유효기간이 파견대학의 지원시까지 유효해야함 ', NULL, NULL, '서던퀸스랜드대학은 Trimester로 운영되므로 학사일정을 반드시 참고하길 바람
- In-state 등록금 납부
(등록금 관련 정보 : https://www.unisq.edu.au/international/partnerships/study-abroad-exchange/fees-scholarships)'), - (1, 5, '시드니대학', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', + (1, 5, 1, '시드니대학', 2, 5, 'IRRELEVANT', 'OVERSEAS_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
- MECO, CAEL, LAWS unit 수강 여석 제한 있음', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기/듣기/말하기 17점, 쓰기 19점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, 'OSHC(Overseas Student Health Cover) 국제학생 보험가입 의무 (2023년 기준 AUD 348/학기, 학기마다 비용 상이)'), - (1, 6, '커틴대학(A형)', 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + (1, 6, 1, '커틴대학(A형)', 2, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능
지원 불가능 전공: Physiotherapy, Medicine, Nursing, Occupational Therapy ', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야함
- IELTS: 모든 영역에서 6.0 이상
- TOEFL IBT: 읽기 13점, 쓰기 21점, 듣기 13점, 말하기 18점 이상
- 어학성적은 파견학기 시작시까지 유효하여야함', NULL, NULL, '※ 24-1학기에 한하여 ''Destination Australia Cheung Kong Exchange Program Scholarship'' 지급 예정 (신청자 중 가장 총점이 우수한 학생 1명에게 AUD$6000 지급, 상세 내용은 국제처 홈페이지 해외대학정보 공지글 참고)'), - (1, 7, '서던덴마크대학교', 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + (1, 7, 1, '서던덴마크대학교', 4, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 주전공과 지원전공이 반드시 일치할 필요는 없으나 본교에서 기초과목을 이수하여야 함
- 교환학생에게 제공되는 수업만 수강 가능
- Faculty of Engineering 내에서 2/3이상의 수업을 수강하여야 함
- 30 ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~10월 1일)', NULL, NULL, '- 교외 숙소', NULL), - (1, 8, '코펜하겐 IT대학', 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + (1, 8, 1, '코펜하겐 IT대학', 2, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 본교 기초과목 이수사항에 따라 지원이 제한될 수 있으나 소속전공과 정확하게 일치 하지 않아도 지원은 가능(연관 전공이어야 함)
- 최소 7.5 ECTS, 최대 30ECTS 수강 가능
- 교차 수강 가능(선수과목이 지정되어있는 과목은 사전에 이수하여야 수강이 가능함)', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 1일)', NULL, NULL, '- 제공(학교 운영 기숙사 아님)
- 선착순 배정', NULL), - (1, 9, '노이울름 대학', 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, + (1, 9, 1, '노이울름 대학', 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '타전공 지원 및 수강 가능', NULL, '영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점; 듣기 17점, 말하기 20점, 쓰기 17점
- TOEIC: 읽기 385점, 듣기 400점, 말하기 160점, 쓰기 150점
외국어 성적 유효기간이 파견대학의 학기 시작하는 시점까지 유효해야 함', NULL, NULL, NULL), - (1, 10, '헐대학', 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + (1, 10, 1, '헐대학', 4, 3, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '제한학과 많음. (Factsheet참조및Factsheet언급된 제한학과 외에도 학기마다 제한학과 발생가능성있음). 지원 전 권역 담당자랑 사전상담 요망. 학기당 30ECTS수강해야 LA승인남. 성적처리 늦은 편이라 8차 학기 수학자는 성적처리 늦은 거 감안하고 추가 이에 따른 불편함이 있음을 인지후 지원요망. ', '지원 전 권역 담당자와 사전상담 요망', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL iBT : 듣기 및 쓰기 18점, 읽기 18점, 말하기 20점, 쓰기 18점 이상
- IELTS : 모든 영역에서 6.0이상', NULL, NULL, '영국 생활비 및 숙소비용 유럽권 지역 중 상대적으로 매우 높은편. 지원전 반드시 사전고려 요망'), - (1, 11, '그라츠 대학', 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', + (1, 11, 1, '그라츠 대학', 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '학교인근 외부 숙소는 있지만, 외부업체운영숙소라 대학관할아님', NULL), - (1, 12, '그라츠공과대학', 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', + (1, 12, 1, '그라츠공과대학', 2, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '-주전공 혹은 제2전공(혹은 연계전공과) 유관학과여아 함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', '- 영어 점수는 다음의 세부영역 점수를 각각 만족해야 함
- TOEFL IBT: 읽기 18점 이상, 쓰기 17점 이상, 말하기 20점 이상, 듣기 17점 이상
- IELTS: 쓰기 5.5점 이상, 말하기 6점 이상
''- TOEIC의 경우 S/W 점수 합산 310점 이상 ', NULL, '자체기숙사는 없음. 교환학생이 많이 지원한 학기에는 예약이 어려울 수도 있음(선착순 경우많음). 더블룸 기준약 한달에 € 340 per month (기숙사 종류게 따라 가격 차이 유) 예산잡으면됨.', NULL), - (1, 13, '린츠 카톨릭 대학교', 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', + (1, 13, 1, '린츠 카톨릭 대학교', 3, 2, 'ONE_SEMESTER', 'HOME_UNIVERSITY_PAYMENT', '- 지원가능전공: History, Philosophy, Art History, theology
(영어과목 수가 그리 많지는 않으므로, 사전 확인필요)
''- 학기당 최소 15ECTS 수강신청해야 함', '봄학기에는 영어과목이 극히 제한적으로 열린다고 함. 지원 전 권역 담당자와 사전상담 요망', NULL, NULL, '학교에서 몇가지 기숙사 옵션 합격시 연결예정.', NULL), - (1, 14, '빈 공과대학교', 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + (1, 14, 1, '빈 공과대학교', 3, 2, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '지원전공과 일치하지 않아도 지원가능하나 유사전공자만 지원가능하며, 본전공과 일치하지않으면 입학 및 수강에 불리할 수 있음
''-학기당 최소 15.ECTS 수강신청해야함', '선발인원 중 차순위 합격자는 학기제한(1개 학기)이 있을 수 있음', NULL, NULL, '기숙사없음', NULL), - (1, 15, 'IPSA', 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + (1, 15, 1, 'IPSA', 4, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', '- 소속전공과 지원전공이 일치 또는 유사하여야 함 : 전공이 제한적이므로 반드시 홈페이지에서 지원 가능 전공을 확인할 것
- 최대 30ECTS 수강', '- 어학성적표가 해당 대학 신청서 제출 시 유효하여야 함(~11월 15일)', NULL, NULL, '- 미제공', NULL), - (1, 16, '메이지대학', 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', + (1, 16, 1, '메이지대학', 2, 3, 'IRRELEVANT', 'HOME_UNIVERSITY_PAYMENT', 'https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004d1.pdf', '*해당 학교 일정 상 10월초까지 서류제출 필요', '학부별로 기준 상이, 관련페이지 참조', NULL, NULL, NULL), - (1, 17, '바이카여자대학', 2, 1, 'IRRELEVANT', NULL, + (1, 17, 1, '바이카여자대학', 2, 1, 'IRRELEVANT', NULL, '교환학생 지원가능 : Department of Global English, Department of Japanese culture, Department of Media and Information, Department of Psychology.', '여학생만 신청가능', NULL, NULL, - '기숙사 없음, 계약된 외부 기숙사 사용-“Maison de Claire Ibaraki” 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), - (1, 18, '분쿄가쿠인대학', 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, + '기숙사 없음, 계약된 외부 기숙사 사용-"Maison de Claire Ibaraki" 62,300엔/월, 2식 포함, 계약시 66,000엔 청구 (2023년 6월기준)', NULL), + (1, 18, 1, '분쿄가쿠인대학', 2, 3, 'ONE_YEAR', 'HOME_UNIVERSITY_PAYMENT', NULL, NULL, NULL, NULL, '기숙사 보유, off campus, 식사 미제공, 45,000~50,000엔/월', NULL); INSERT INTO language_requirement(language_test_type, min_score, university_info_for_apply_id) diff --git a/src/main/resources/db/migration/V42__rename_university_to_host_university_and_create_home_university.sql b/src/main/resources/db/migration/V42__rename_university_to_host_university_and_create_home_university.sql new file mode 100644 index 000000000..d6b9b6044 --- /dev/null +++ b/src/main/resources/db/migration/V42__rename_university_to_host_university_and_create_home_university.sql @@ -0,0 +1,25 @@ +RENAME TABLE university TO host_university; + +ALTER TABLE university_info_for_apply + DROP FOREIGN KEY FKd0257hco6uy2utd1xccjh3fal; + +ALTER TABLE university_info_for_apply + ADD CONSTRAINT fk_university_info_for_apply_host_university + FOREIGN KEY (university_id) REFERENCES host_university (id) ON DELETE NO ACTION; + +CREATE TABLE IF NOT EXISTS home_university +( + id BIGINT AUTO_INCREMENT NOT NULL, + name VARCHAR(100) NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + CONSTRAINT `PRIMARY` PRIMARY KEY (id), + CONSTRAINT uk_home_university_name UNIQUE (name) +); + +ALTER TABLE host_university + ADD COLUMN home_university_id BIGINT NULL; + +ALTER TABLE host_university + ADD CONSTRAINT fk_host_university_home_university + FOREIGN KEY (home_university_id) REFERENCES home_university (id) ON DELETE NO ACTION; diff --git a/src/main/resources/db/migration/V43__move_home_university_fk_to_univ_apply_info.sql b/src/main/resources/db/migration/V43__move_home_university_fk_to_univ_apply_info.sql new file mode 100644 index 000000000..36ba62a90 --- /dev/null +++ b/src/main/resources/db/migration/V43__move_home_university_fk_to_univ_apply_info.sql @@ -0,0 +1,12 @@ +ALTER TABLE host_university + DROP FOREIGN KEY fk_host_university_home_university; + +ALTER TABLE host_university + DROP COLUMN home_university_id; + +ALTER TABLE university_info_for_apply + ADD COLUMN home_university_id BIGINT NULL; + +ALTER TABLE university_info_for_apply + ADD CONSTRAINT fk_university_info_for_apply_home_university + FOREIGN KEY (home_university_id) REFERENCES home_university (id) ON DELETE NO ACTION; diff --git a/src/main/resources/db/migration/V44__add_unique_constraint_to_host_university_korean_name.sql b/src/main/resources/db/migration/V44__add_unique_constraint_to_host_university_korean_name.sql new file mode 100644 index 000000000..636aa28a6 --- /dev/null +++ b/src/main/resources/db/migration/V44__add_unique_constraint_to_host_university_korean_name.sql @@ -0,0 +1,2 @@ +ALTER TABLE host_university + ADD CONSTRAINT uk_host_university_korean_name UNIQUE (korean_name); diff --git a/src/main/resources/db/migration/V45__modify_mentor_introduction_pass_tip_nullable.sql b/src/main/resources/db/migration/V45__modify_mentor_introduction_pass_tip_nullable.sql new file mode 100644 index 000000000..e81eb049c --- /dev/null +++ b/src/main/resources/db/migration/V45__modify_mentor_introduction_pass_tip_nullable.sql @@ -0,0 +1,5 @@ +ALTER TABLE mentor + MODIFY introduction VARCHAR(1000) NULL; + +ALTER TABLE mentor + MODIFY pass_tip VARCHAR(1000) NULL; diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java new file mode 100644 index 000000000..620f18a4d --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java @@ -0,0 +1,401 @@ +package com.example.solidconnection.admin.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; +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.admin.university.service.AdminHostUniversityService; +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.region.domain.Region; +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.fixture.UnivApplyInfoFixtureBuilder; +import com.example.solidconnection.university.fixture.UniversityFixture; +import com.example.solidconnection.university.repository.HostUniversityRepository; +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; + +@TestContainerSpringBootTest +@DisplayName("파견 대학 관리 서비스 테스트") +class AdminHostUniversityServiceTest { + + @Autowired + private AdminHostUniversityService adminHostUniversityService; + + @Autowired + private HostUniversityRepository hostUniversityRepository; + + @Autowired + private UniversityFixture universityFixture; + + @Autowired + private CountryFixture countryFixture; + + @Autowired + private RegionFixture regionFixture; + + @Autowired + private UnivApplyInfoFixtureBuilder univApplyInfoFixtureBuilder; + + @Nested + class 목록_조회 { + + @Test + void 대학이_없으면_빈_목록을_반환한다() { + // given + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).isEmpty(); + assertThat(response.getTotalElements()).isZero(); + } + + @Test + void 키워드로_한글명을_검색한다() { + // given + universityFixture.괌_대학(); + HostUniversity target = universityFixture.메이지_대학(); + + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition("메이지", null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(1); + assertThat(response.getContent().get(0).koreanName()).isEqualTo(target.getKoreanName()); + } + + @Test + void 키워드로_영문명을_검색한다() { + // given + universityFixture.괌_대학(); + HostUniversity target = universityFixture.메이지_대학(); + + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition("Meiji", null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(1); + assertThat(response.getContent().get(0).englishName()).isEqualTo(target.getEnglishName()); + } + + @Test + void 국가_코드로_필터링한다() { + // given + universityFixture.괌_대학(); + universityFixture.네바다주립_대학_라스베이거스(); + universityFixture.메이지_대학(); + + Country usa = countryFixture.미국(); + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, usa.getCode(), null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(2); + assertThat(response.getContent()) + .extracting(r -> r.countryCode()) + .containsOnly(usa.getCode()); + } + + @Test + void 지역_코드로_필터링한다() { + // given + universityFixture.괌_대학(); + universityFixture.서던덴마크_대학(); + universityFixture.그라츠_대학(); + + Region europe = regionFixture.유럽(); + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, null, europe.getCode()); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(2); + assertThat(response.getContent()) + .extracting(r -> r.regionCode()) + .containsOnly(europe.getCode()); + } + + @Test + void 페이징이_정상_작동한다() { + // given + universityFixture.괌_대학(); + universityFixture.네바다주립_대학_라스베이거스(); + universityFixture.메이지_대학(); + + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 2)); + + // then + assertThat(response.getContent()).hasSize(2); + assertThat(response.getTotalElements()).isEqualTo(3); + assertThat(response.getTotalPages()).isEqualTo(2); + assertThat(response.hasNext()).isTrue(); + } + } + + @Nested + class 상세_조회 { + + @Test + void 존재하는_대학을_조회하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.getHostUniversity(university.getId()); + + // then + assertThat(response.id()).isEqualTo(university.getId()); + assertThat(response.koreanName()).isEqualTo(university.getKoreanName()); + assertThat(response.englishName()).isEqualTo(university.getEnglishName()); + } + + @Test + void 존재하지_않는_대학을_조회하면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> adminHostUniversityService.getHostUniversity(999L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.UNIVERSITY_NOT_FOUND.getMessage()); + } + } + + @Nested + class 생성 { + + @Test + void 유효한_정보로_대학을_생성하면_성공한다() { + // given + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityCreateRequest request = new AdminHostUniversityCreateRequest( + "테스트 대학", + "Test University", + "테스트 대학", + "https://homepage.com", + "https://english-course.com", + "https://accommodation.com", + "https://logo.com/image.png", + "https://background.com/image.png", + "상세 정보", + country.getCode(), + region.getCode() + ); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.createHostUniversity(request); + + // then + assertThat(response.koreanName()).isEqualTo(request.koreanName()); + assertThat(response.englishName()).isEqualTo(request.englishName()); + + HostUniversity savedUniversity = hostUniversityRepository.findById(response.id()).orElseThrow(); + assertThat(savedUniversity.getKoreanName()).isEqualTo(request.koreanName()); + } + + @Test + void 이미_존재하는_한글명으로_생성하면_예외_응답을_반환한다() { + // given + HostUniversity existing = universityFixture.괌_대학(); + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityCreateRequest request = new AdminHostUniversityCreateRequest( + existing.getKoreanName(), + "New English Name", + "표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + country.getCode(), + region.getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.createHostUniversity(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); + } + } + + @Nested + class 수정 { + + @Test + void 유효한_정보로_대학을_수정하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + Country country = countryFixture.일본(); + Region region = regionFixture.아시아(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + "수정된 대학명", + "Updated University", + "수정된 표시명", + "https://new-homepage.com", + null, null, + "https://new-logo.com/image.png", + "https://new-background.com/image.png", + "수정된 상세 정보", + country.getCode(), + region.getCode() + ); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.updateHostUniversity( + university.getId(), request); + + // then + assertThat(response.koreanName()).isEqualTo(request.koreanName()); + assertThat(response.countryCode()).isEqualTo(country.getCode()); + + HostUniversity updatedUniversity = hostUniversityRepository.findById(university.getId()).orElseThrow(); + assertThat(updatedUniversity.getKoreanName()).isEqualTo(request.koreanName()); + } + + @Test + void 존재하지_않는_대학을_수정하면_예외_응답을_반환한다() { + // given + 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 & then + assertThatCode(() -> adminHostUniversityService.updateHostUniversity(999L, request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.UNIVERSITY_NOT_FOUND.getMessage()); + } + + @Test + void 다른_대학의_한글명으로_수정하면_예외_응답을_반환한다() { + // given + HostUniversity university1 = universityFixture.괌_대학(); + HostUniversity university2 = universityFixture.메이지_대학(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + university2.getKoreanName(), + "Updated University", + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + university1.getCountry().getCode(), + university1.getRegion().getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.updateHostUniversity(university1.getId(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 같은_대학의_한글명으로_수정하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + university.getKoreanName(), + "Updated English Name", + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + university.getCountry().getCode(), + university.getRegion().getCode() + ); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.updateHostUniversity( + university.getId(), request); + + // then + assertThat(response.koreanName()).isEqualTo(university.getKoreanName()); + assertThat(response.englishName()).isEqualTo(request.englishName()); + } + } + + @Nested + class 삭제 { + + @Test + void 존재하는_대학을_삭제하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + // when + adminHostUniversityService.deleteHostUniversity(university.getId()); + + // then + assertThat(hostUniversityRepository.findById(university.getId())).isEmpty(); + } + + @Test + void 존재하지_않는_대학을_삭제하면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> adminHostUniversityService.deleteHostUniversity(999L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.UNIVERSITY_NOT_FOUND.getMessage()); + } + + @Test + void 참조하는_대학_지원_정보가_있으면_예외_응답을_반환한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + univApplyInfoFixtureBuilder.univApplyInfo() + .termId(1L) + .koreanName("괌 대학 지원 정보") + .university(university) + .create(); + + // when & then + assertThatCode(() -> adminHostUniversityService.deleteHostUniversity(university.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_HAS_REFERENCES.getMessage()); + } + } +} diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminMentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminMentorApplicationServiceTest.java index 0c133165a..a65718443 100644 --- a/src/test/java/com/example/solidconnection/admin/service/AdminMentorApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/admin/service/AdminMentorApplicationServiceTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.admin.service; +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_CONFIRMED; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_OTHER_STATUS; @@ -16,15 +17,20 @@ import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition; import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse; import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.mentor.domain.MentorApplication; import com.example.solidconnection.mentor.domain.MentorApplicationStatus; 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.repository.MentorApplicationRepository; +import com.example.solidconnection.mentor.repository.MentorRepository; +import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.fixture.UniversityFixture; import java.time.LocalDate; import java.util.List; @@ -50,12 +56,21 @@ class AdminMentorApplicationServiceTest { @Autowired private MentorApplicationFixture mentorApplicationFixture; + @Autowired + private MentorFixture mentorFixture; + @Autowired private UniversityFixture universityFixture; @Autowired private MentorApplicationRepository mentorApplicationRepository; + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private MentorRepository mentorRepository; + private MentorApplication mentorApplication1; private MentorApplication mentorApplication2; private MentorApplication mentorApplication3; @@ -66,7 +81,7 @@ class AdminMentorApplicationServiceTest { private MentorApplication mentorApplication8; private SiteUser user; - private University university; + private HostUniversity university; @BeforeEach void setUp() { @@ -78,9 +93,9 @@ void setUp() { SiteUser user6 = siteUserFixture.사용자(6, "test6"); SiteUser user7 = siteUserFixture.사용자(7, "test7"); SiteUser user8 = siteUserFixture.사용자(8, "test8"); - University university1 = universityFixture.메이지_대학(); - University university2 = universityFixture.괌_대학(); - University university3 = universityFixture.그라츠_대학(); + HostUniversity university1 = universityFixture.메이지_대학(); + HostUniversity university2 = universityFixture.괌_대학(); + HostUniversity university3 = universityFixture.그라츠_대학(); mentorApplication1 = mentorApplicationFixture.승인된_멘토신청(user1.getId(), UniversitySelectType.CATALOG, university1.getId()); mentorApplication2 = mentorApplicationFixture.대기중_멘토신청(user2.getId(), UniversitySelectType.CATALOG, university2.getId()); mentorApplication3 = mentorApplicationFixture.거절된_멘토신청(user3.getId(), UniversitySelectType.CATALOG, university3.getId()); @@ -91,7 +106,7 @@ void setUp() { mentorApplication8 = mentorApplicationFixture.거절된_멘토신청(user8.getId(), UniversitySelectType.OTHER, null); user = siteUserFixture.사용자(9, "test9"); - university = universityFixture.메이지_대학(); + university = universityFixture.네바다주립_대학_라스베이거스(); } @Nested @@ -100,7 +115,7 @@ class 멘토_승격_지원서_목록_조회 { @Test void 멘토_승격_상태를_조건으로_페이징하여_조회한다() { // given - MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(MentorApplicationStatus.PENDING,null, null, null); + MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(MentorApplicationStatus.PENDING, null, null, null); Pageable pageable = PageRequest.of(0, 10); List expectedMentorApplications = List.of(mentorApplication2, mentorApplication5, mentorApplication7); @@ -122,7 +137,7 @@ class 멘토_승격_지원서_목록_조회 { } @Test - void 닉네임_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다(){ + void 닉네임_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다() { // given String nickname = "test1"; MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(null, nickname, null, null); @@ -147,7 +162,7 @@ class 멘토_승격_지원서_목록_조회 { } @Test - void 대학명_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다(){ + void 대학명_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다() { // given String universityKoreanName = "메이지 대학"; MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(null, universityKoreanName, null, null); @@ -172,7 +187,7 @@ class 멘토_승격_지원서_목록_조회 { } @Test - void 지역명_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다(){ + void 지역명_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다() { // given String regionKoreanName = "유럽"; MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(null, regionKoreanName, null, null); @@ -197,10 +212,10 @@ class 멘토_승격_지원서_목록_조회 { } @Test - void 나라명_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다(){ + void 나라명_keyword_에_맞는_멘토_지원서를_페이징하여_조회한다() { // given String countryKoreanName = "오스트리아"; - MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(null, countryKoreanName, null,null); + MentorApplicationSearchCondition condition = new MentorApplicationSearchCondition(null, countryKoreanName, null, null); Pageable pageable = PageRequest.of(0, 10); List expectedMentorApplications = List.of(mentorApplication3, mentorApplication4); @@ -302,7 +317,7 @@ class 멘토_승격_지원서_목록_조회 { } @Nested - class 멘토_승격_지원서_승인{ + class 멘토_승격_지원서_승인 { @Test void 대기중인_멘토_지원서를_승인한다() { @@ -314,14 +329,21 @@ class 멘토_승격_지원서_승인{ // then MentorApplication result = mentorApplicationRepository.findById(mentorApplication2.getId()).get(); + SiteUser mentorUser = siteUserRepository.findById(result.getSiteUserId()).get(); + Mentor mentor = mentorRepository.findBySiteUserId(result.getSiteUserId()).get(); assertAll( () -> assertThat(result.getMentorApplicationStatus()).isEqualTo(MentorApplicationStatus.APPROVED), - () -> assertThat(result.getApprovedAt()).isNotNull() + () -> assertThat(result.getApprovedAt()).isNotNull(), + () -> assertThat(mentorUser.getRole()).isEqualTo(Role.MENTOR), + () -> assertThat(mentor).isNotNull(), + () -> assertThat(mentor.getSiteUserId()).isEqualTo(result.getSiteUserId()), + () -> assertThat(mentor.getUniversityId()).isEqualTo(result.getUniversityId()), + () -> assertThat(mentor.getTermId()).isEqualTo(result.getTermId()) ); } @Test - void 대학이_선택되지_않은_멘토_지원서를_승인하면_예외가_발생한다(){ + void 대학이_선택되지_않은_멘토_지원서를_승인하면_예외가_발생한다() { // given SiteUser user = siteUserFixture.사용자(); MentorApplication noUniversityIdMentorApplication = mentorApplicationFixture.대기중_멘토신청(user.getId(), UniversitySelectType.OTHER, null); @@ -364,10 +386,24 @@ class 멘토_승격_지원서_승인{ .isInstanceOf(CustomException.class) .hasMessage(MENTOR_APPLICATION_NOT_FOUND.getMessage()); } + + @Test + void 이미_멘토인_사용자의_지원서를_승인하면_예외가_발생한다() { + // given + SiteUser user = siteUserFixture.사용자(); + HostUniversity university = universityFixture.버지니아_공과_대학(); + MentorApplication pendingApplication = mentorApplicationFixture.대기중_멘토신청(user.getId(), UniversitySelectType.CATALOG, university.getId()); + mentorFixture.멘토(user.getId(), university.getId()); + + // when & then + assertThatCode(() -> adminMentorApplicationService.approveMentorApplication(pendingApplication.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(MENTOR_ALREADY_EXISTS.getMessage()); + } } @Nested - class 멘토_승격_지원서_거절{ + class 멘토_승격_지원서_거절 { @Test void 대기중인_멘토_지원서를_거절한다() { @@ -468,7 +504,7 @@ class 멘토_지원서에_대학_매핑 { void OTHER_타입의_멘토_지원서에_대학을_매핑하면_대학이_할당되고_타입이_CATALOG로_변경된다() { // given long otherTypeMentorApplicationId = mentorApplication7.getId(); - University university = universityFixture.메이지_대학(); + HostUniversity university = universityFixture.아칸소_주립_대학(); // when adminMentorApplicationService.assignUniversity(otherTypeMentorApplicationId, university.getId()); @@ -485,7 +521,7 @@ class 멘토_지원서에_대학_매핑 { void 존재하지_않는_멘토_지원서에_대학을_매핑하면_예외_응답을_반환한다() { // given long nonExistentId = 99999L; - University university = universityFixture.메이지_대학(); + HostUniversity university = universityFixture.메모리얼_대학_세인트존스(); // when & then assertThatCode(() -> adminMentorApplicationService.assignUniversity(nonExistentId, university.getId())) @@ -497,7 +533,7 @@ class 멘토_지원서에_대학_매핑 { void CATALOG_타입의_멘토_지원서에_대학을_매핑하면_예외_응답을_반환한다() { // given long catalogTypeMentorApplicationId = mentorApplication2.getId(); - University university = universityFixture.메이지_대학(); + HostUniversity university = universityFixture.서던덴마크_대학(); // when & then assertThatCode(() -> adminMentorApplicationService.assignUniversity(catalogTypeMentorApplicationId, university.getId())) @@ -517,7 +553,7 @@ class 멘토_지원서에_대학_매핑 { .hasMessage(UNIVERSITY_NOT_FOUND.getMessage()); } } - + @Nested class 멘토_지원서_이력_조회 { @@ -539,7 +575,7 @@ class 멘토_지원서_이력_조회 { .containsExactly(app3.getId(), app2.getId(), app1.getId()), () -> assertThat(response) .extracting(MentorApplicationHistoryResponse::applicationOrder) - .containsExactly(3,2,1) + .containsExactly(3, 2, 1) ); } @@ -565,7 +601,7 @@ class 멘토_지원서_이력_조회 { .containsExactly(app7.getId(), app6.getId(), app5.getId(), app4.getId(), app3.getId()), () -> assertThat(response) .extracting(MentorApplicationHistoryResponse::applicationOrder) - .containsExactly(7,6,5,4,3) + .containsExactly(7, 6, 5, 4, 3) ); } diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java index 9025ce6f8..42b4d1524 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java @@ -72,7 +72,7 @@ class ApplicationQueryServiceTest { private LanguageTestScore languageTestScore3; private UnivApplyInfo 괌대학_A_지원_정보; - private UnivApplyInfo 괌대학_B_지원_정보; + private UnivApplyInfo 버지니아공과대학_지원_정보; private UnivApplyInfo 서던덴마크대학교_지원_정보; private Term term; @@ -94,7 +94,7 @@ void setUp() { languageTestScore3 = languageTestScoreFixture.어학_점수(VerifyStatus.APPROVED, user3); 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); + 버지니아공과대학_지원_정보 = univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()); 서던덴마크대학교_지원_정보 = univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId()); } @@ -120,7 +120,7 @@ class 지원자_목록_조회_테스트 { term.getId(), gpaScore2.getGpa(), languageTestScore2.getLanguageTest(), - 괌대학_B_지원_정보.getId(), + 버지니아공과대학_지원_정보.getId(), null, null ); @@ -146,7 +146,7 @@ class 지원자_목록_조회_테스트 { assertThat(response.firstChoice()).containsAll(List.of( ApplicantsResponse.of(괌대학_A_지원_정보, List.of(application1), user1), - ApplicantsResponse.of(괌대학_B_지원_정보, + ApplicantsResponse.of(버지니아공과대학_지원_정보, List.of(application2), user1), ApplicantsResponse.of(서던덴마크대학교_지원_정보, List.of(application3), user1) @@ -172,7 +172,7 @@ class 지원자_목록_조회_테스트 { term.getId(), gpaScore2.getGpa(), languageTestScore2.getLanguageTest(), - 괌대학_B_지원_정보.getId(), + 버지니아공과대학_지원_정보.getId(), null, null ); @@ -198,7 +198,7 @@ class 지원자_목록_조회_테스트 { assertThat(response.firstChoice()).containsExactlyInAnyOrder( ApplicantsResponse.of(괌대학_A_지원_정보, List.of(application1), user1), - ApplicantsResponse.of(괌대학_B_지원_정보, + ApplicantsResponse.of(버지니아공과대학_지원_정보, List.of(application2), user1) ); } @@ -222,7 +222,7 @@ class 지원자_목록_조회_테스트 { term.getId(), gpaScore2.getGpa(), languageTestScore2.getLanguageTest(), - 괌대학_B_지원_정보.getId(), + 버지니아공과대학_지원_정보.getId(), null, null ); @@ -241,14 +241,14 @@ class 지원자_목록_조회_테스트 { ApplicationsResponse response = applicationQueryService.getApplicants( user1.getId(), null, - "괌" + "미국" ); // then assertThat(response.firstChoice()).containsExactlyInAnyOrder( ApplicantsResponse.of(괌대학_A_지원_정보, List.of(application1), user1), - ApplicantsResponse.of(괌대학_B_지원_정보, + ApplicantsResponse.of(버지니아공과대학_지원_정보, List.of(application2), user1) ); } @@ -289,7 +289,7 @@ class 지원자_목록_조회_테스트 { // then assertThat(response.firstChoice()).containsExactlyInAnyOrder( ApplicantsResponse.of(괌대학_A_지원_정보, List.of(currentApplication), user1), - ApplicantsResponse.of(괌대학_B_지원_정보, List.of(), user1), + ApplicantsResponse.of(버지니아공과대학_지원_정보, List.of(), user1), ApplicantsResponse.of(서던덴마크대학교_지원_정보, List.of(), user1) ); } @@ -315,7 +315,7 @@ class 지원자_목록_조회_테스트 { term.getId(), gpaScore1.getGpa(), languageTestScore1.getLanguageTest(), - 괌대학_B_지원_정보.getId(), + 버지니아공과대학_지원_정보.getId(), null, null ); @@ -401,7 +401,7 @@ class 경쟁자_목록_조회_테스트 { gpaScore2.getGpa(), languageTestScore2.getLanguageTest(), 괌대학_A_지원_정보.getId(), - 괌대학_B_지원_정보.getId(), + 버지니아공과대학_지원_정보.getId(), 서던덴마크대학교_지원_정보.getId() ); Application application3 = applicationFixture.지원서( diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 192b4f601..91877d9d9 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -58,7 +58,7 @@ class ApplicationSubmissionServiceTest { private SiteUser user; private UnivApplyInfo 괌대학_A_지원_정보; - private UnivApplyInfo 괌대학_B_지원_정보; + private UnivApplyInfo 버지니아공과대학_지원_정보; private UnivApplyInfo 서던덴마크대학교_지원_정보; private Term term; @@ -69,7 +69,7 @@ void setUp() { user = siteUserFixture.사용자(); 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); + 버지니아공과대학_지원_정보 = univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()); 서던덴마크대학교_지원_정보 = univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId()); } @@ -80,7 +80,7 @@ void setUp() { LanguageTestScore languageTestScore = languageTestScoreFixture.어학_점수(VerifyStatus.APPROVED, user); UnivApplyInfoChoiceRequest univApplyInfoChoiceRequest = new UnivApplyInfoChoiceRequest( 괌대학_A_지원_정보.getId(), - 괌대학_B_지원_정보.getId(), + 버지니아공과대학_지원_정보.getId(), 서던덴마크대학교_지원_정보.getId() ); ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), univApplyInfoChoiceRequest); @@ -98,7 +98,7 @@ void setUp() { () -> assertThat(response.appliedUniversities().firstChoiceUnivApplyInfo()) .isEqualTo(괌대학_A_지원_정보.getKoreanName()), () -> assertThat(response.appliedUniversities().secondChoiceUnivApplyInfo()) - .isEqualTo(괌대학_B_지원_정보.getKoreanName()), + .isEqualTo(버지니아공과대학_지원_정보.getKoreanName()), () -> assertThat(response.appliedUniversities().thirdChoiceUnivApplyInfo()) .isEqualTo(서던덴마크대학교_지원_정보.getKoreanName()), () -> assertThat(savedApplication.getVerifyStatus()) @@ -108,7 +108,7 @@ void setUp() { () -> assertThat(savedApplication.getFirstChoiceUnivApplyInfoId()) .isEqualTo(괌대학_A_지원_정보.getId()), () -> assertThat(savedApplication.getSecondChoiceUnivApplyInfoId()) - .isEqualTo(괌대학_B_지원_정보.getId()), + .isEqualTo(버지니아공과대학_지원_정보.getId()), () -> assertThat(savedApplication.getThirdChoiceUnivApplyInfoId()) .isEqualTo(서던덴마크대학교_지원_정보.getId()) ); diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 98454da0f..b716b44a4 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -9,8 +9,8 @@ import com.example.solidconnection.auth.domain.RefreshToken; import com.example.solidconnection.auth.domain.Subject; import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.exception.AuthException; import com.example.solidconnection.auth.token.TokenBlackListService; -import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -109,7 +109,7 @@ class 토큰을_재발급한다 { // when, then assertThatCode(() -> authService.reissue(invalidRefreshToken)) - .isInstanceOf(CustomException.class) + .isInstanceOf(AuthException.class) .hasMessage(REFRESH_TOKEN_EXPIRED.getMessage()); } } diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index f5ec202bb..7cfb3dec0 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -117,9 +117,9 @@ class 채팅방_목록을_조회한다 { // then assertAll( () -> assertThat(response.chatRooms()).hasSize(2), - () -> assertThat(response.chatRooms().get(0).partner().partnerId()).isEqualTo(mentor2.getId()), + () -> assertThat(response.chatRooms().get(0).partner().siteUserId()).isEqualTo(mentor2.getId()), () -> assertThat(response.chatRooms().get(0).lastChatMessage()).isEqualTo(newMessage.getContent()), - () -> assertThat(response.chatRooms().get(1).partner().partnerId()).isEqualTo(mentor1.getId()), + () -> assertThat(response.chatRooms().get(1).partner().siteUserId()).isEqualTo(mentor1.getId()), () -> assertThat(response.chatRooms().get(1).lastChatMessage()).isEqualTo(oldMessage.getContent()) ); } @@ -244,9 +244,9 @@ void setUp() { assertAll( () -> assertThat(response.content()).hasSize(2), () -> assertThat(response.content().get(0).content()).isEqualTo(message2.getContent()), - () -> assertThat(response.content().get(0).senderId()).isEqualTo(user.getId()), + () -> assertThat(response.content().get(0).siteUserId()).isEqualTo(user.getId()), () -> assertThat(response.content().get(1).content()).isEqualTo(message1.getContent()), - () -> assertThat(response.content().get(1).senderId()).isEqualTo(mentor1.getId()) + () -> assertThat(response.content().get(1).siteUserId()).isEqualTo(mentor1.getId()) ); } @@ -328,7 +328,7 @@ class 채팅방_파트너_정보를_조회한다 { // then assertAll( - () -> assertThat(response.partnerId()).isEqualTo(mentor1.getId()), + () -> assertThat(response.siteUserId()).isEqualTo(mentor1.getId()), () -> assertThat(response.nickname()).isEqualTo(mentor1.getNickname()), () -> assertThat(response.profileUrl()).isEqualTo(mentor1.getProfileImageUrl()) ); @@ -431,7 +431,7 @@ void setUp() { assertAll( () -> assertThat(destinationCaptor.getValue()).isEqualTo("/topic/chat/" + chatRoom.getId()), () -> assertThat(payloadCaptor.getValue().content()).isEqualTo(content), - () -> assertThat(payloadCaptor.getValue().senderId()).isEqualTo(senderParticipant.getId()) + () -> assertThat(payloadCaptor.getValue().siteUserId()).isEqualTo(sender.getId()) ); } @@ -454,8 +454,8 @@ class 채팅_이미지를_전송한다 { private SiteUser sender; private ChatParticipant senderParticipant; private ChatRoom chatRoom; - private static final String TEST_IMAGE_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example.jpg"; - private static final String TEST_IMAGE_URL2 = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example2.jpg"; + private static final String TEST_IMAGE_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/files/example.jpg"; + private static final String TEST_IMAGE_URL2 = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/files/example2.jpg"; private static final String EXPECTED_THUMBNAIL_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/thumbnails/example_thumb.jpg"; @BeforeEach @@ -491,7 +491,7 @@ void setUp() { () -> assertThat(response.attachments().get(0).url()).isEqualTo(imageUrls.get(0)), () -> assertThat(response.attachments().get(1).url()).isEqualTo(imageUrls.get(1)), () -> assertThat(response.messageType()).isEqualTo(MessageType.IMAGE), - () -> assertThat(response.senderId()).isEqualTo(senderParticipant.getId()), + () -> assertThat(response.siteUserId()).isEqualTo(sender.getId()), () -> assertThat(response.content()).isEmpty() ); } diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java index 36211c341..c752c11e4 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java @@ -25,13 +25,13 @@ import com.example.solidconnection.community.post.fixture.PostFixture; import com.example.solidconnection.community.post.fixture.PostImageFixture; import com.example.solidconnection.community.post.repository.PostRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.redis.RedisService; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.util.RedisUtils; import jakarta.transaction.Transactional; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -57,7 +57,7 @@ class PostCommandServiceTest { private RedisService redisService; @Autowired - private RedisUtils redisUtils; + private PostRedisManager postRedisManager; @Autowired private PostRepository postRepository; @@ -109,7 +109,7 @@ class 게시글_생성_테스트 { PostCreateRequest request = createPostCreateRequest(PostCategory.자유.name()); List imageFiles = List.of(createImageFile()); String expectedImageUrl = "test-image-url"; - given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + given(s3Service.uploadFiles(any(), eq(UploadPath.COMMUNITY))) .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); // when @@ -179,7 +179,7 @@ class 게시글_수정_테스트 { PostUpdateRequest request = createPostUpdateRequest(); List imageFiles = List.of(createImageFile()); - given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + given(s3Service.uploadFiles(any(), eq(UploadPath.COMMUNITY))) .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); // when @@ -266,7 +266,7 @@ class 게시글_삭제_테스트 { // given String originImageUrl = "origin-image-url"; postImageFixture.게시글_이미지(originImageUrl, post); - String viewCountKey = redisUtils.getPostViewCountRedisKey(post.getId()); + String viewCountKey = postRedisManager.getPostViewCountRedisKey(post.getId()); redisService.increaseViewCount(viewCountKey); // when diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java index f3eaf41a8..66803d688 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java @@ -17,11 +17,11 @@ import com.example.solidconnection.community.post.dto.PostListResponse; import com.example.solidconnection.community.post.fixture.PostFixture; import com.example.solidconnection.community.post.fixture.PostImageFixture; +import com.example.solidconnection.redis.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.siteuser.fixture.UserBlockFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.util.RedisUtils; import java.time.ZonedDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -40,7 +40,7 @@ class PostQueryServiceTest { private RedisService redisService; @Autowired - private RedisUtils redisUtils; + private PostRedisManager postRedisManager; @Autowired private SiteUserFixture siteUserFixture; @@ -176,8 +176,8 @@ void setUp() { Comment comment2 = commentFixture.부모_댓글("댓글2", post, user); List comments = List.of(comment1, comment2); - String validateKey = redisUtils.getValidatePostViewCountRedisKey(user.getId(), post.getId()); - String viewCountKey = redisUtils.getPostViewCountRedisKey(post.getId()); + String validateKey = postRedisManager.getValidatePostViewCountRedisKey(user.getId(), post.getId()); + String viewCountKey = postRedisManager.getPostViewCountRedisKey(post.getId()); // when PostFindResponse response = postQueryService.findPostById(user.getId(), post.getId()); diff --git a/src/test/java/com/example/solidconnection/concurrency/PostCreateConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostCreateConcurrencyTest.java new file mode 100644 index 000000000..72b132fbb --- /dev/null +++ b/src/test/java/com/example/solidconnection/concurrency/PostCreateConcurrencyTest.java @@ -0,0 +1,149 @@ +package com.example.solidconnection.concurrency; + +import static com.example.solidconnection.redis.RedisConstants.VALIDATE_POST_CREATE_TTL; +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.solidconnection.community.post.service.PostRedisManager; +import com.example.solidconnection.redis.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@TestContainerSpringBootTest +@DisplayName("게시글 생성 동시성 테스트") +class PostCreateConcurrencyTest { + + @Autowired + private RedisService redisService; + + @Autowired + private PostRedisManager postRedisManager; + + @Autowired + private SiteUserFixture siteUserFixture; + + private SiteUser user; + + @BeforeEach + void setUp() { + user = siteUserFixture.사용자(); + redisService.deleteKey(postRedisManager.getPostCreateRedisKey(user.getId())); + } + + @Test + void 동시에_여러_요청이_들어오면_첫_번째_요청만_허용된다() throws InterruptedException { + // given + ExecutorService executorService = Executors.newFixedThreadPool(5); + CountDownLatch readyLatch = new CountDownLatch(5); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(5); + + AtomicInteger allowedCount = new AtomicInteger(0); + AtomicInteger deniedCount = new AtomicInteger(0); + + // when + for (int i = 0; i < 5; i++) { + executorService.submit(() -> { + try { + readyLatch.countDown(); + startLatch.await(); + + boolean isAllowed = postRedisManager.isPostCreationAllowed(user.getId()); + if (isAllowed) { + allowedCount.incrementAndGet(); + } else { + deniedCount.incrementAndGet(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + readyLatch.await(5, TimeUnit.SECONDS); //모든 스레드가 준비 상태가 될 때까지 대기 + startLatch.countDown(); //동시 실행 + doneLatch.await(5, TimeUnit.SECONDS); //모든 스레드의 작업이 끝날 때까지 대기 + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + + // then + assertThat(allowedCount.get()).isEqualTo(1); + assertThat(deniedCount.get()).isEqualTo(4); + } + + @Test + void TTL이_지나면_다시_게시글_생성이_허용된다() throws InterruptedException { + // given + boolean firstAttempt = postRedisManager.isPostCreationAllowed(user.getId()); + boolean secondAttemptBeforeTtl = postRedisManager.isPostCreationAllowed(user.getId()); + + // when + long ttlSeconds = Long.parseLong(VALIDATE_POST_CREATE_TTL.getValue()); + Thread.sleep((ttlSeconds + 1) * 1000); + + boolean attemptAfterTtl = postRedisManager.isPostCreationAllowed(user.getId()); + + // then + assertThat(firstAttempt).isTrue(); + assertThat(secondAttemptBeforeTtl).isFalse(); + assertThat(attemptAfterTtl).isTrue(); + } + + @Test + void 서로_다른_사용자는_동시에_게시글을_생성할_수_있다() throws InterruptedException { + // given + SiteUser user1 = siteUserFixture.사용자(1, "사용자1"); + SiteUser user2 = siteUserFixture.사용자(2, "사용자2"); + SiteUser user3 = siteUserFixture.사용자(3, "사용자3"); + + redisService.deleteKey(postRedisManager.getPostCreateRedisKey(user1.getId())); + redisService.deleteKey(postRedisManager.getPostCreateRedisKey(user2.getId())); + redisService.deleteKey(postRedisManager.getPostCreateRedisKey(user3.getId())); + + ExecutorService executorService = Executors.newFixedThreadPool(3); + CountDownLatch readyLatch = new CountDownLatch(3); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(3); + + AtomicInteger allowedCount = new AtomicInteger(0); + + // when + for (SiteUser currentUser : new SiteUser[]{user1, user2, user3}) { + executorService.submit(() -> { + try { + readyLatch.countDown(); + startLatch.await(); + + boolean isAllowed = postRedisManager.isPostCreationAllowed(currentUser.getId()); + if (isAllowed) { + allowedCount.incrementAndGet(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + doneLatch.countDown(); + } + }); + } + + readyLatch.await(5, TimeUnit.SECONDS); //모든 스레드가 준비 상태가 될 때까지 대기 + startLatch.countDown(); //동시 실행 + doneLatch.await(5, TimeUnit.SECONDS); //모든 스레드의 작업이 끝날 때까지 대기 + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + + // then + assertThat(allowedCount.get()).isEqualTo(3); + } +} diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index 4396e697b..a3529a0ab 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -1,6 +1,6 @@ package com.example.solidconnection.concurrency; -import static com.example.solidconnection.community.post.service.RedisConstants.VALIDATE_VIEW_COUNT_TTL; +import static com.example.solidconnection.redis.RedisConstants.VALIDATE_VIEW_COUNT_TTL; import static org.junit.jupiter.api.Assertions.assertEquals; import com.example.solidconnection.community.board.domain.Board; @@ -8,11 +8,11 @@ import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.community.post.domain.PostCategory; import com.example.solidconnection.community.post.repository.PostRepository; -import com.example.solidconnection.community.post.service.RedisService; +import com.example.solidconnection.community.post.service.PostRedisManager; +import com.example.solidconnection.redis.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.util.RedisUtils; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -37,7 +37,7 @@ class PostViewCountConcurrencyTest { private BoardRepository boardRepository; @Autowired - private RedisUtils redisUtils; + private PostRedisManager postRedisManager; @Autowired private SiteUserFixture siteUserFixture; @@ -84,7 +84,7 @@ private Post createPost(Board board, SiteUser siteUser) { @Test void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { - redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(user.getId(), post.getId())); + redisService.deleteKey(postRedisManager.getValidatePostViewCountRedisKey(user.getId(), post.getId())); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); @@ -92,7 +92,7 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(postRedisManager.getPostViewCountRedisKey(post.getId())); } finally { doneSignal.countDown(); } @@ -114,7 +114,7 @@ private Post createPost(Board board, SiteUser siteUser) { @Test void 게시글을_조회할_때_조회수_조작_문제를_해결한다() throws InterruptedException { - redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(user.getId(), post.getId())); + redisService.deleteKey(postRedisManager.getValidatePostViewCountRedisKey(user.getId(), post.getId())); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); @@ -122,9 +122,9 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(user.getId(), post.getId())); + boolean isFirstTime = redisService.isPresent(postRedisManager.getValidatePostViewCountRedisKey(user.getId(), post.getId()), VALIDATE_VIEW_COUNT_TTL.getValue()); if (isFirstTime) { - redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(postRedisManager.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); @@ -135,9 +135,9 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(user.getId(), post.getId())); + boolean isFirstTime = redisService.isPresent(postRedisManager.getValidatePostViewCountRedisKey(user.getId(), post.getId()), VALIDATE_VIEW_COUNT_TTL.getValue()); if (isFirstTime) { - redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); + redisService.increaseViewCount(postRedisManager.getPostViewCountRedisKey(post.getId())); } } finally { doneSignal.countDown(); diff --git a/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java b/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java index 347bb684f..4719849f4 100644 --- a/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/mentor/repository/MentorBatchQueryRepositoryTest.java @@ -9,7 +9,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.fixture.UniversityFixture; import java.util.List; import java.util.Map; @@ -37,7 +37,7 @@ class MentorBatchQueryRepositoryTest { @Autowired private UniversityFixture universityFixture; - private University university1, university2; + private HostUniversity university1, university2; private Mentor mentor1, mentor2; private SiteUser mentorUser1, mentorUser2, currentUser; @@ -73,7 +73,7 @@ void setUp() { List mentors = List.of(mentor1, mentor2); // when - Map mentorIdToUniversity = mentorBatchQueryRepository.getMentorIdToUniversityMap(mentors); + Map mentorIdToUniversity = mentorBatchQueryRepository.getMentorIdToUniversityMap(mentors); // then assertAll( diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java index daa429fc3..8987cbe5d 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java @@ -13,7 +13,7 @@ import com.example.solidconnection.mentor.dto.MentorApplicationRequest; import com.example.solidconnection.mentor.fixture.MentorApplicationFixture; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.ExchangeStatus; @@ -69,7 +69,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadPath.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when @@ -87,7 +87,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadPath.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when @@ -105,7 +105,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadPath.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when & then @@ -122,7 +122,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadPath.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when & then @@ -173,7 +173,7 @@ void setUp() { MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; - given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + given(s3Service.uploadFile(file, UploadPath.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); // when diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java index cecae13be..dd3d853e7 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorMyPageServiceTest.java @@ -1,24 +1,15 @@ package com.example.solidconnection.mentor.service; -import static com.example.solidconnection.common.exception.ErrorCode.CHANNEL_REGISTRATION_LIMIT_EXCEEDED; -import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_ALREADY_EXISTS; -import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND; import static com.example.solidconnection.mentor.domain.ChannelType.BLOG; -import static com.example.solidconnection.mentor.domain.ChannelType.BRUNCH; import static com.example.solidconnection.mentor.domain.ChannelType.INSTAGRAM; -import static com.example.solidconnection.mentor.domain.ChannelType.YOUTUBE; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.Assertions.assertAll; -import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Channel; import com.example.solidconnection.mentor.domain.Mentor; -import com.example.solidconnection.mentor.domain.UniversitySelectType; import com.example.solidconnection.mentor.dto.ChannelRequest; import com.example.solidconnection.mentor.dto.ChannelResponse; -import com.example.solidconnection.mentor.dto.MentorMyPageCreateRequest; import com.example.solidconnection.mentor.dto.MentorMyPageResponse; import com.example.solidconnection.mentor.dto.MentorMyPageUpdateRequest; import com.example.solidconnection.mentor.fixture.ChannelFixture; @@ -31,7 +22,7 @@ import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.fixture.TermFixture; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.fixture.UniversityFixture; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -73,7 +64,7 @@ class MentorMyPageServiceTest { private SiteUser mentorUser; private Mentor mentor; - private University university; + private HostUniversity university; private SiteUser siteUser; private Term term; @@ -172,111 +163,4 @@ class 멘토의_마이_페이지를_수정한다 { ); } } - - @Nested - class 멘토의_마이페이지를_생성한다 { - - @Test - void 멘토_정보를_생성한다() { - // given - String introduction = "멘토 자기소개"; - String passTip = "멘토의 합격 팁"; - List channels = List.of( - new ChannelRequest(BLOG, "https://blog.com"), - new ChannelRequest(INSTAGRAM, "https://instagram.com"), - new ChannelRequest(YOUTUBE, "https://youtubr.com"), - new ChannelRequest(BRUNCH, "https://brunch.com") - ); - MentorMyPageCreateRequest request = new MentorMyPageCreateRequest(introduction, passTip, channels); - mentorApplicationFixture.승인된_멘토신청(siteUser.getId(), UniversitySelectType.CATALOG, university.getId()); - - // when - mentorMyPageService.createMentorMyPage(siteUser.getId(), request); - - // then - Mentor createMentor = mentorRepository.findBySiteUserId(siteUser.getId()).get(); - List createChannels = channelRepositoryForTest.findAllByMentorId(createMentor.getId()); - assertAll( - () -> assertThat(createMentor.getIntroduction()).isEqualTo(introduction), - () -> assertThat(createMentor.getPassTip()).isEqualTo(passTip), - () -> assertThat(createMentor.getTermId()).isEqualTo(term.getId()), - () -> assertThat(createMentor.getUniversityId()).isEqualTo(university.getId()), - () -> assertThat(createMentor.getSiteUserId()).isEqualTo(siteUser.getId()), - () -> assertThat(createMentor.getMenteeCount()).isEqualTo(0), - () -> assertThat(createMentor.isHasBadge()).isFalse(), - () -> assertThat(createChannels).extracting(Channel::getSequence, Channel::getType, Channel::getUrl) - .containsExactlyInAnyOrder( - tuple(1, BLOG, "https://blog.com"), - tuple(2, INSTAGRAM, "https://instagram.com"), - tuple(3, YOUTUBE, "https://youtubr.com"), - tuple(4, BRUNCH, "https://brunch.com") - ) - ); - } - - @Test - void 이미_멘토_정보가_존재하는데_생성_요청_시_예외가_발생한다() { - // given - MentorMyPageCreateRequest request = new MentorMyPageCreateRequest("introduction", "passTip", List.of()); - mentorFixture.멘토(siteUser.getId(), university.getId()); - - // when & then - assertThatCode(() -> mentorMyPageService.createMentorMyPage(siteUser.getId(), request)) - .isInstanceOf(CustomException.class) - .hasMessage(MENTOR_ALREADY_EXISTS.getMessage()); - } - - @Test - void 채널을_제한_이상_생성하면_예외가_발생한다() { - // given - List newChannels = List.of( - new ChannelRequest(BLOG, "https://blog.com"), - new ChannelRequest(INSTAGRAM, "https://instagram.com"), - new ChannelRequest(YOUTUBE, "https://youtubr.com"), - new ChannelRequest(BRUNCH, "https://brunch.com"), - new ChannelRequest(BLOG, "https://blog.com") - ); - MentorMyPageCreateRequest request = new MentorMyPageCreateRequest("introduction", "passTip", newChannels); - - // when & then - assertThatCode(() -> mentorMyPageService.createMentorMyPage(siteUser.getId(), request)) - .isInstanceOf(CustomException.class) - .hasMessage(CHANNEL_REGISTRATION_LIMIT_EXCEEDED.getMessage()); - } - - @Test - void 멘토_승격_요청_없이_멘토_정보_생성_시_예외가_발생한다() { - // given - MentorMyPageCreateRequest request = new MentorMyPageCreateRequest("introduction", "passTip", List.of()); - - // when & then - assertThatCode(() -> mentorMyPageService.createMentorMyPage(siteUser.getId(), request)) - .isInstanceOf(CustomException.class) - .hasMessage(MENTOR_APPLICATION_NOT_FOUND.getMessage()); - } - - @Test - void 멘토_승격_요청_상태가_REJECTED_면_예외가_발생한다() { - // given - MentorMyPageCreateRequest request = new MentorMyPageCreateRequest("introduction", "passTip", List.of()); - mentorApplicationFixture.거절된_멘토신청(siteUser.getId(), UniversitySelectType.CATALOG, university.getId()); - - // when & then - assertThatCode(() -> mentorMyPageService.createMentorMyPage(siteUser.getId(), request)) - .isInstanceOf(CustomException.class) - .hasMessage(MENTOR_APPLICATION_NOT_FOUND.getMessage()); - } - - @Test - void 멘토_승격_요청_상태가_PENDING_면_예외가_발생한다() { - // given - MentorMyPageCreateRequest request = new MentorMyPageCreateRequest("introduction", "passTip", List.of()); - mentorApplicationFixture.대기중_멘토신청(siteUser.getId(), UniversitySelectType.CATALOG, university.getId()); - - // when & then - assertThatCode(() -> mentorMyPageService.createMentorMyPage(siteUser.getId(), request)) - .isInstanceOf(CustomException.class) - .hasMessage(MENTOR_APPLICATION_NOT_FOUND.getMessage()); - } - } } 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 8a4088845..08603d556 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorQueryServiceTest.java @@ -19,7 +19,7 @@ import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.term.fixture.TermFixture; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.fixture.UniversityFixture; import java.util.Map; import java.util.function.Function; @@ -56,7 +56,7 @@ class MentorQueryServiceTest { @Autowired private TermFixture termFixture; - private University university; + private HostUniversity university; @BeforeEach void setUp() { @@ -77,11 +77,11 @@ class 멘토_단일_조회_성공 { Channel channel2 = channelFixture.채널(2, mentor); // when - MentorDetailResponse response = mentorQueryService.getMentorDetails(mentor.getId(), siteUser.getId()); + MentorDetailResponse response = mentorQueryService.getMentorDetails(mentor.getSiteUserId(), siteUser.getId()); // then assertAll( - () -> assertThat(response.id()).isEqualTo(mentor.getId()), + () -> assertThat(response.id()).isEqualTo(mentor.getSiteUserId()), () -> assertThat(response.nickname()).isEqualTo(mentorUser.getNickname()), () -> assertThat(response.universityName()).isEqualTo(university.getKoreanName()), () -> assertThat(response.country()).isEqualTo(university.getCountry().getKoreanName()), @@ -101,8 +101,8 @@ class 멘토_단일_조회_성공 { mentoringFixture.대기중_멘토링(mentor.getId(), appliedUser.getId()); // when - MentorDetailResponse notAppliedResponse = mentorQueryService.getMentorDetails(mentor.getId(), notAppliedUser.getId()); - MentorDetailResponse appliedResponse = mentorQueryService.getMentorDetails(mentor.getId(), appliedUser.getId()); + MentorDetailResponse notAppliedResponse = mentorQueryService.getMentorDetails(mentor.getSiteUserId(), notAppliedUser.getId()); + MentorDetailResponse appliedResponse = mentorQueryService.getMentorDetails(mentor.getSiteUserId(), appliedUser.getId()); // then assertAll( @@ -134,7 +134,7 @@ class 멘토_미리보기_목록_정보_조회 { private Mentor mentor1, mentor2; private SiteUser mentorUser1, mentorUser2, currentUser; - private University university1, university2; + private HostUniversity university1, university2; @BeforeEach void setUp() { @@ -159,8 +159,8 @@ void setUp() { // then Map mentorPreviewMap = response.content().stream() .collect(Collectors.toMap(MentorPreviewResponse::id, Function.identity())); - MentorPreviewResponse mentor1Response = mentorPreviewMap.get(mentor1.getId()); - MentorPreviewResponse mentor2Response = mentorPreviewMap.get(mentor2.getId()); + MentorPreviewResponse mentor1Response = mentorPreviewMap.get(mentor1.getSiteUserId()); + MentorPreviewResponse mentor2Response = mentorPreviewMap.get(mentor2.getSiteUserId()); assertAll( () -> assertThat(mentor1Response.nickname()).isEqualTo(mentorUser1.getNickname()), () -> assertThat(mentor1Response.universityName()).isEqualTo(university1.getKoreanName()), @@ -200,7 +200,7 @@ class 멘토_미리보기_목록_필터링 { private Mentor asiaMentor, europeMentor; private SiteUser currentUser; - private University asiaUniversity, europeUniversity; + private HostUniversity asiaUniversity, europeUniversity; @BeforeEach void setUp() { @@ -208,7 +208,7 @@ void setUp() { SiteUser mentorUser1 = siteUserFixture.사용자(2, "멘토1"); SiteUser mentorUser2 = siteUserFixture.사용자(3, "멘토2"); asiaUniversity = universityFixture.메이지_대학(); - europeUniversity = universityFixture.린츠_카톨릭_대학(); + europeUniversity = universityFixture.그라츠공과_대학(); asiaMentor = mentorFixture.멘토(mentorUser1.getId(), asiaUniversity.getId()); europeMentor = mentorFixture.멘토(mentorUser2.getId(), europeUniversity.getId()); } @@ -225,10 +225,10 @@ void setUp() { assertAll( () -> assertThat(asiaFilteredResponse.content()).hasSize(1) .extracting(MentorPreviewResponse::id) - .containsExactly(asiaMentor.getId()), + .containsExactly(asiaMentor.getSiteUserId()), () -> assertThat(europeFilteredResponse.content()).hasSize(1) .extracting(MentorPreviewResponse::id) - .containsExactly(europeMentor.getId()) + .containsExactly(europeMentor.getSiteUserId()) ); } @@ -240,7 +240,7 @@ void setUp() { // then assertThat(response.content()).hasSize(2) .extracting(MentorPreviewResponse::id) - .containsExactlyInAnyOrder(asiaMentor.getId(), europeMentor.getId()); + .containsExactlyInAnyOrder(asiaMentor.getSiteUserId(), europeMentor.getSiteUserId()); } } } diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java index 002dfa6a5..db2a4a5ea 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java @@ -83,7 +83,7 @@ class 멘토링_신청_테스트 { @Test void 멘토링을_성공적으로_신청한다() { // given - MentoringApplyRequest request = new MentoringApplyRequest(mentor1.getId()); + MentoringApplyRequest request = new MentoringApplyRequest(mentor1.getSiteUserId()); // when MentoringApplyResponse response = mentoringCommandService.applyMentoring(menteeUser.getId(), request); @@ -102,7 +102,7 @@ class 멘토링_신청_테스트 { void 동일_멘티_멘토끼리는_재신청되지않는다() { // given mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); - MentoringApplyRequest request = new MentoringApplyRequest(mentor1.getId()); + MentoringApplyRequest request = new MentoringApplyRequest(mentor1.getSiteUserId()); // when & then assertThatThrownBy(() -> mentoringCommandService.applyMentoring(menteeUser.getId(), request)) diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java index 959d8e491..61c9503eb 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringQueryServiceTest.java @@ -25,7 +25,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.fixture.UniversityFixture; import java.util.Map; import java.util.function.Function; @@ -69,7 +69,7 @@ class MentoringQueryServiceTest { private SiteUser mentorUser1, mentorUser2; private SiteUser menteeUser1, menteeUser2, menteeUser3; private Mentor mentor1, mentor2, mentor3; - private University university; + private HostUniversity university; private Pageable pageable; @BeforeEach @@ -290,8 +290,8 @@ void setUp() { // then Map matchMentorMap = response.content().stream() .collect(Collectors.toMap(MatchedMentorResponse::id, Function.identity())); - MatchedMentorResponse mentor1Response = matchMentorMap.get(mentor1.getId()); - MatchedMentorResponse mentor2Response = matchMentorMap.get(mentor2.getId()); + MatchedMentorResponse mentor1Response = matchMentorMap.get(mentor1.getSiteUserId()); + MatchedMentorResponse mentor2Response = matchMentorMap.get(mentor2.getSiteUserId()); assertAll( () -> assertThat(mentor1Response.roomId()).isEqualTo(chatRoom1.getId()), () -> assertThat(mentor1Response.nickname()).isEqualTo(mentorUser1.getNickname()), diff --git a/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java b/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java index f82a3bd84..7911da88e 100644 --- a/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/news/service/NewsCommandServiceTest.java @@ -18,7 +18,7 @@ import com.example.solidconnection.news.dto.NewsUpdateRequest; import com.example.solidconnection.news.fixture.NewsFixture; import com.example.solidconnection.news.repository.NewsRepository; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; @@ -71,7 +71,7 @@ class 소식지_생성_테스트 { NewsCreateRequest request = createNewsCreateRequest(); MultipartFile imageFile = createImageFile(); String expectedImageUrl = "news/5a02ba2f-38f5-4ae9-9a24-53d624a18233"; - given(s3Service.uploadFile(any(), eq(ImgType.NEWS))) + given(s3Service.uploadFile(any(), eq(UploadPath.NEWS))) .willReturn(new UploadedFileUrlResponse(expectedImageUrl)); // when @@ -110,7 +110,7 @@ void setUp() { String expectedUrl = "https://youtu.be/test-edit"; MultipartFile expectedFile = createImageFile(); String expectedNewImageUrl = "news/5a02ba2f-38f5-4ae9-9a24-53d624a18233-edit"; - given(s3Service.uploadFile(any(), eq(ImgType.NEWS))) + given(s3Service.uploadFile(any(), eq(UploadPath.NEWS))) .willReturn(new UploadedFileUrlResponse(expectedNewImageUrl)); NewsUpdateRequest request = createNewsUpdateRequest( expectedTitle, @@ -185,7 +185,7 @@ void setUp() { assertAll( () -> assertThat(savedNews.getThumbnailUrl()).isEqualTo(newsProperties.defaultThumbnailUrl()), () -> then(s3Service).should().deletePostImage(CUSTOM_IMAGE_URL), - () -> then(s3Service).should(never()).uploadFile(null, ImgType.NEWS) + () -> then(s3Service).should(never()).uploadFile(null, UploadPath.NEWS) ); } @@ -194,7 +194,7 @@ void setUp() { // given MultipartFile newImageFile = createImageFile(); String newImageUrl = "news/new-image-url"; - given(s3Service.uploadFile(newImageFile, ImgType.NEWS)) + given(s3Service.uploadFile(newImageFile, UploadPath.NEWS)) .willReturn(new UploadedFileUrlResponse(newImageUrl)); NewsUpdateRequest request = createNewsUpdateRequest( null, @@ -248,7 +248,7 @@ void setUp() { assertAll( () -> assertThat(savedNews.getThumbnailUrl()).isEqualTo(newsProperties.defaultThumbnailUrl()), () -> then(s3Service).should(never()).deletePostImage(newsProperties.defaultThumbnailUrl()), - () -> then(s3Service).should(never()).uploadFile(null, ImgType.NEWS) + () -> then(s3Service).should(never()).uploadFile(null, UploadPath.NEWS) ); } @@ -257,7 +257,7 @@ void setUp() { // given MultipartFile newImageFile = createImageFile(); String newImageUrl = "news/new-image-url"; - given(s3Service.uploadFile(newImageFile, ImgType.NEWS)) + given(s3Service.uploadFile(newImageFile, UploadPath.NEWS)) .willReturn(new UploadedFileUrlResponse(newImageUrl)); NewsUpdateRequest request = createNewsUpdateRequest(null, null, null, null); diff --git a/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java b/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java new file mode 100644 index 000000000..51d2aead5 --- /dev/null +++ b/src/test/java/com/example/solidconnection/s3/service/S3ServiceTest.java @@ -0,0 +1,114 @@ +package com.example.solidconnection.s3.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.s3.domain.UploadPath; +import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; + +@DisplayName("S3 서비스 테스트") +@ExtendWith(MockitoExtension.class) +public class S3ServiceTest { + + @InjectMocks + private S3Service s3Service; + + @Mock private FileUploadService fileUploadService; + private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5; + + private MockMultipartFile createMockFile(String originalName, long size) { + return new MockMultipartFile("file", originalName, "image/jpeg", new byte[(int) size]); + } + + @Nested + class 파일_업로드_경로_및_리사이징_로직 { + + @Test + void O5MB_미만의_이미지는_원본_확장자를_유지하며_업로드된다() { + // given + MockMultipartFile file = createMockFile("test.png", MAX_FILE_SIZE_MB - 100); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile(file, UploadPath.PROFILE); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("profile/"), + () -> assertThat(response.fileUrl()).endsWith(".png"), + () -> assertThat(response.fileUrl()).doesNotContain("original/", "resize/") + ); + } + + @Test + void O5MB_이상의_이미지는_original_경로로_업로드되고_resize_webp_경로를_반환한다() { + // given + MockMultipartFile file = createMockFile("test.jpg", MAX_FILE_SIZE_MB + 100); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile(file, UploadPath.PROFILE); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("resize/profile/"), + () -> assertThat(response.fileUrl()).endsWith(".webp") + ); + } + + @Test + void 채팅_파일은_5MB가_넘어도_리사이징_경로를_적용하지_않고_원본_경로를_반환한다() { + // given + MockMultipartFile file = createMockFile("chat.jpg", MAX_FILE_SIZE_MB + 100); + + // when + UploadedFileUrlResponse response = s3Service.uploadFile(file, UploadPath.CHAT); + + // then + assertAll( + () -> assertThat(response.fileUrl()).startsWith("chat/files/"), + () -> assertThat(response.fileUrl()).endsWith(".jpg"), + () -> assertThat(response.fileUrl()).doesNotContain("resize/") + ); + } + } + + @Nested + class 파일_검증 { + + @Test + void 허용되지_않은_확장자의_파일은_예외를_던진다() { + // given + MockMultipartFile invalidFile = createMockFile("virus.exe", 100); + + // when & then + assertThatThrownBy(() -> s3Service.uploadFile(invalidFile, UploadPath.PROFILE)) + .isInstanceOf(CustomException.class) + .hasMessageContaining("허용된 형식"); + } + + @Test + void 채팅_업로드시_이미지_외의_허용된_문서_확장자들도_성공적으로_검증을_통과한다() { + // given + MockMultipartFile pdfFile = createMockFile("test.pdf", 100); + MockMultipartFile wordFile = createMockFile("test.docx", 100); + + // when & then + assertAll( + () -> assertThatCode(() -> s3Service.uploadFile(pdfFile, UploadPath.CHAT)) + .doesNotThrowAnyException(), + () -> assertThatCode(() -> s3Service.uploadFile(wordFile, UploadPath.CHAT)) + .doesNotThrowAnyException() + ); + } + } +} diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 8760a645b..2522dc962 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -4,7 +4,7 @@ import static org.mockito.BDDMockito.given; import com.example.solidconnection.common.VerifyStatus; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.score.domain.GpaScore; @@ -115,7 +115,7 @@ void setUp() { GpaScoreRequest request = createGpaScoreRequest(); MockMultipartFile file = createFile(); String fileUrl = "/gpa-report.pdf"; - given(s3Service.uploadFile(file, ImgType.GPA)).willReturn(new UploadedFileUrlResponse(fileUrl)); + given(s3Service.uploadFile(file, UploadPath.GPA)).willReturn(new UploadedFileUrlResponse(fileUrl)); // when long scoreId = scoreService.submitGpaScore(user.getId(), request, file); @@ -131,7 +131,7 @@ void setUp() { LanguageTestScoreRequest request = createLanguageTestScoreRequest(); MockMultipartFile file = createFile(); String fileUrl = "/gpa-report.pdf"; - given(s3Service.uploadFile(file, ImgType.LANGUAGE_TEST)).willReturn(new UploadedFileUrlResponse(fileUrl)); + given(s3Service.uploadFile(file, UploadPath.LANGUAGE_TEST)).willReturn(new UploadedFileUrlResponse(fileUrl)); // when long scoreId = scoreService.submitLanguageTestScore(user.getId(), request, file); 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 3a82681f3..360afc640 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/MyPageServiceTest.java @@ -25,7 +25,7 @@ import com.example.solidconnection.location.region.fixture.RegionFixture; import com.example.solidconnection.location.region.repository.InterestedRegionRepository; import com.example.solidconnection.mentor.fixture.MentorFixture; -import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.AuthType; @@ -41,7 +41,8 @@ import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.fixture.TermFixture; import com.example.solidconnection.university.domain.LikedUnivApplyInfo; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.domain.UnivApplyInfo; import com.example.solidconnection.university.fixture.UnivApplyInfoFixture; import com.example.solidconnection.university.repository.LikedUnivApplyInfoRepository; import java.time.LocalDateTime; @@ -105,11 +106,20 @@ class MyPageServiceTest { private SiteUser user; private Term term; + private Long 괌대학_A_지원_정보_ID; + private Long 메이지대학_지원_정보_ID; + private Long 코펜하겐IT대학_지원_정보_ID; + private HostUniversity 괌대학; @BeforeEach void setUp() { user = siteUserFixture.사용자(); term = termFixture.현재_학기("2025-2"); + UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); + 괌대학_A_지원_정보_ID = 괌대학_A_지원_정보.getId(); + 괌대학 = 괌대학_A_지원_정보.getUniversity(); + 메이지대학_지원_정보_ID = univApplyInfoFixture.메이지대학_지원_정보(term.getId()).getId(); + 코펜하겐IT대학_지원_정보_ID = univApplyInfoFixture.코펜하겐IT대학_지원_정보(term.getId()).getId(); } @Test @@ -141,8 +151,7 @@ void setUp() { void 멘토의_마이페이지_정보를_조회한다() { // given SiteUser mentorUser = siteUserFixture.멘토(1, "mentor"); - University university = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()).getUniversity(); - mentorFixture.멘토(mentorUser.getId(), university.getId()); + mentorFixture.멘토(mentorUser.getId(), 괌대학.getId()); int likedUnivApplyInfoCount = createLikedUnivApplyInfos(mentorUser); // when @@ -157,15 +166,15 @@ void setUp() { // () -> assertThat(response.likedPostCount()).isEqualTo(user.getLikedPostList().size()), // todo : 좋아요한 게시물 수 반환 기능 추가와 함께 수정요망 () -> assertThat(response.likedUnivApplyInfoCount()).isEqualTo(likedUnivApplyInfoCount), - () -> assertThat(response.attendedUniversity()).isEqualTo(university.getKoreanName()), + () -> assertThat(response.attendedUniversity()).isEqualTo(괌대학.getKoreanName()), () -> assertThat(response.interestedCountries()).isNull() ); } private int createLikedUnivApplyInfos(SiteUser testUser) { - LikedUnivApplyInfo likedUnivApplyInfo1 = new LikedUnivApplyInfo(null, univApplyInfoFixture.괌대학_A_지원_정보(term.getId()).getId(), testUser.getId()); - LikedUnivApplyInfo likedUnivApplyInfo2 = new LikedUnivApplyInfo(null, univApplyInfoFixture.메이지대학_지원_정보(term.getId()).getId(), testUser.getId()); - LikedUnivApplyInfo likedUnivApplyInfo3 = new LikedUnivApplyInfo(null, univApplyInfoFixture.코펜하겐IT대학_지원_정보(term.getId()).getId(), testUser.getId()); + LikedUnivApplyInfo likedUnivApplyInfo1 = new LikedUnivApplyInfo(null, 괌대학_A_지원_정보_ID, testUser.getId()); + LikedUnivApplyInfo likedUnivApplyInfo2 = new LikedUnivApplyInfo(null, 메이지대학_지원_정보_ID, testUser.getId()); + LikedUnivApplyInfo likedUnivApplyInfo3 = new LikedUnivApplyInfo(null, 코펜하겐IT대학_지원_정보_ID, testUser.getId()); likedUnivApplyInfoRepository.save(likedUnivApplyInfo1); likedUnivApplyInfoRepository.save(likedUnivApplyInfo2); @@ -209,7 +218,7 @@ class 프로필_이미지_수정_테스트 { // given String expectedUrl = "newProfileImageUrl"; MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadPath.PROFILE))) .willReturn(new UploadedFileUrlResponse(expectedUrl)); // when @@ -224,7 +233,7 @@ class 프로필_이미지_수정_테스트 { void 프로필을_처음_수정하는_것이면_이전_이미지를_삭제하지_않는다() { // given MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadPath.PROFILE))) .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when @@ -239,7 +248,7 @@ class 프로필_이미지_수정_테스트 { // given SiteUser 커스텀_프로필_사용자 = createSiteUserWithCustomProfile(); MockMultipartFile imageFile = createValidImageFile(); - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadPath.PROFILE))) .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when @@ -255,7 +264,7 @@ class 닉네임_수정_테스트 { @BeforeEach void setUp() { - given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + given(s3Service.uploadFile(any(), eq(UploadPath.PROFILE))) .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); } diff --git a/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java new file mode 100644 index 000000000..97391a06d --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixture.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.university.fixture; + +import com.example.solidconnection.university.domain.HomeUniversity; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class HomeUniversityFixture { + + private final HomeUniversityFixtureBuilder homeUniversityFixtureBuilder; + + public HomeUniversity 인하대학교() { + return homeUniversityFixtureBuilder.homeUniversity() + .name("인하대학교") + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java new file mode 100644 index 000000000..092b2a0c2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/fixture/HomeUniversityFixtureBuilder.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.university.fixture; + +import com.example.solidconnection.university.domain.HomeUniversity; +import com.example.solidconnection.university.repository.HomeUniversityRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class HomeUniversityFixtureBuilder { + + private final HomeUniversityRepository homeUniversityRepository; + + private String name; + + public HomeUniversityFixtureBuilder homeUniversity() { + return new HomeUniversityFixtureBuilder(homeUniversityRepository); + } + + public HomeUniversityFixtureBuilder name(String name) { + this.name = name; + return this; + } + + public HomeUniversity create() { + return homeUniversityRepository.findByName(name) + .orElseGet(() -> homeUniversityRepository.save(new HomeUniversity(null, name))); + } +} diff --git a/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java b/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java index fdfd11187..2bc2c5843 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java +++ b/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixture.java @@ -10,20 +10,23 @@ public class UnivApplyInfoFixture { private final UnivApplyInfoFixtureBuilder univApplyInfoFixtureBuilder; private final UniversityFixture universityFixture; + private final HomeUniversityFixture homeUniversityFixture; public UnivApplyInfo 괌대학_A_지원_정보(long termId) { return univApplyInfoFixtureBuilder.univApplyInfo() .termId(termId) .koreanName("괌대학(A형)") .university(universityFixture.괌_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } - public UnivApplyInfo 괌대학_B_지원_정보(long termId) { + public UnivApplyInfo 버지니아공과대학_지원_정보(long termId) { return univApplyInfoFixtureBuilder.univApplyInfo() .termId(termId) - .koreanName("괌대학(B형)") - .university(universityFixture.괌_대학()) + .koreanName("버지니아공과대학") + .university(universityFixture.버지니아_공과_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -32,6 +35,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("네바다주립대학 라스베이거스(B형)") .university(universityFixture.네바다주립_대학_라스베이거스()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -40,6 +44,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("아칸소 주립 대학") .university(universityFixture.아칸소_주립_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -48,6 +53,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("메모리얼 대학 세인트존스(A형)") .university(universityFixture.메모리얼_대학_세인트존스()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -56,6 +62,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("서던덴마크대학교") .university(universityFixture.서던덴마크_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -64,6 +71,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("코펜하겐 IT대학") .university(universityFixture.코펜하겐IT_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -72,6 +80,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("그라츠 대학") .university(universityFixture.그라츠_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -80,6 +89,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("그라츠공과대학") .university(universityFixture.그라츠공과_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -88,6 +98,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("린츠 카톨릭 대학교") .university(universityFixture.린츠_카톨릭_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } @@ -96,6 +107,7 @@ public class UnivApplyInfoFixture { .termId(termId) .koreanName("메이지대학") .university(universityFixture.메이지_대학()) + .homeUniversity(homeUniversityFixture.인하대학교()) .create(); } } diff --git a/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixtureBuilder.java b/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixtureBuilder.java index 6f78d8086..2320f2544 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixtureBuilder.java +++ b/src/test/java/com/example/solidconnection/university/fixture/UnivApplyInfoFixtureBuilder.java @@ -3,8 +3,9 @@ import static com.example.solidconnection.university.domain.SemesterAvailableForDispatch.ONE_SEMESTER; import static com.example.solidconnection.university.domain.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; +import com.example.solidconnection.university.domain.HomeUniversity; +import com.example.solidconnection.university.domain.HostUniversity; import com.example.solidconnection.university.domain.UnivApplyInfo; -import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.repository.UnivApplyInfoRepository; import java.util.HashSet; import lombok.RequiredArgsConstructor; @@ -18,7 +19,8 @@ public class UnivApplyInfoFixtureBuilder { private long termId; private String koreanName; - private University university; + private HostUniversity university; + private HomeUniversity homeUniversity; public UnivApplyInfoFixtureBuilder univApplyInfo() { return new UnivApplyInfoFixtureBuilder(univApplyInfoRepository); @@ -34,14 +36,19 @@ public UnivApplyInfoFixtureBuilder koreanName(String koreanName) { return this; } - public UnivApplyInfoFixtureBuilder university(University university) { + public UnivApplyInfoFixtureBuilder university(HostUniversity university) { this.university = university; return this; } + public UnivApplyInfoFixtureBuilder homeUniversity(HomeUniversity homeUniversity) { + this.homeUniversity = homeUniversity; + return this; + } + public UnivApplyInfo create() { UnivApplyInfo univApplyInfo = new UnivApplyInfo( - null, termId, koreanName, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + null, termId, homeUniversity, koreanName, 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", "detailsForAccommodation", "detailsForEnglishCourse", "details", diff --git a/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java b/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java index bbc3fc3b4..bb0015b2f 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java +++ b/src/test/java/com/example/solidconnection/university/fixture/UniversityFixture.java @@ -2,7 +2,7 @@ import com.example.solidconnection.location.country.fixture.CountryFixture; import com.example.solidconnection.location.region.fixture.RegionFixture; -import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.HostUniversity; import lombok.RequiredArgsConstructor; import org.springframework.boot.test.context.TestComponent; @@ -14,7 +14,7 @@ public final class UniversityFixture { private final CountryFixture countryFixture; private final UniversityFixtureBuilder universityFixtureBuilder; - public University 괌_대학() { + public HostUniversity 괌_대학() { return universityFixtureBuilder.university() .koreanName("괌 대학") .englishName("University of Guam") @@ -23,7 +23,7 @@ public final class UniversityFixture { .create(); } - public University 네바다주립_대학_라스베이거스() { + public HostUniversity 네바다주립_대학_라스베이거스() { return universityFixtureBuilder.university() .koreanName("네바다주립 대학 라스베이거스") .englishName("University of Nevada, Las Vegas") @@ -32,7 +32,7 @@ public final class UniversityFixture { .create(); } - public University 아칸소_주립_대학() { + public HostUniversity 아칸소_주립_대학() { return universityFixtureBuilder.university() .koreanName("아칸소 주립 대학") .englishName("Arkansas State University") @@ -41,7 +41,7 @@ public final class UniversityFixture { .create(); } - public University 메모리얼_대학_세인트존스() { + public HostUniversity 메모리얼_대학_세인트존스() { return universityFixtureBuilder.university() .koreanName("메모리얼 대학 세인트존스") .englishName("Memorial University of Newfoundland St. John's") @@ -50,7 +50,7 @@ public final class UniversityFixture { .create(); } - public University 서던덴마크_대학() { + public HostUniversity 서던덴마크_대학() { return universityFixtureBuilder.university() .koreanName("서던덴마크 대학") .englishName("University of Southern Denmark") @@ -59,7 +59,7 @@ public final class UniversityFixture { .create(); } - public University 코펜하겐IT_대학() { + public HostUniversity 코펜하겐IT_대학() { return universityFixtureBuilder.university() .koreanName("코펜하겐IT 대학") .englishName("IT University of Copenhagen") @@ -68,7 +68,7 @@ public final class UniversityFixture { .create(); } - public University 그라츠_대학() { + public HostUniversity 그라츠_대학() { return universityFixtureBuilder.university() .koreanName("그라츠 대학") .englishName("University of Graz") @@ -77,7 +77,7 @@ public final class UniversityFixture { .create(); } - public University 그라츠공과_대학() { + public HostUniversity 그라츠공과_대학() { return universityFixtureBuilder.university() .koreanName("그라츠공과 대학") .englishName("Graz University of Technology") @@ -86,7 +86,7 @@ public final class UniversityFixture { .create(); } - public University 린츠_카톨릭_대학() { + public HostUniversity 린츠_카톨릭_대학() { return universityFixtureBuilder.university() .koreanName("린츠 카톨릭 대학") .englishName("Catholic Private University Linz") @@ -95,7 +95,7 @@ public final class UniversityFixture { .create(); } - public University 메이지_대학() { + public HostUniversity 메이지_대학() { return universityFixtureBuilder.university() .koreanName("메이지 대학") .englishName("Meiji University") @@ -103,4 +103,13 @@ public final class UniversityFixture { .region(regionFixture.아시아()) .create(); } + + public HostUniversity 버지니아_공과_대학() { + return universityFixtureBuilder.university() + .koreanName("버지니아 공과 대학") + .englishName("Virginia Tech") + .country(countryFixture.미국()) + .region(regionFixture.영미권()) + .create(); + } } diff --git a/src/test/java/com/example/solidconnection/university/fixture/UniversityFixtureBuilder.java b/src/test/java/com/example/solidconnection/university/fixture/UniversityFixtureBuilder.java index 4da6cdfd7..9fc0e89d0 100644 --- a/src/test/java/com/example/solidconnection/university/fixture/UniversityFixtureBuilder.java +++ b/src/test/java/com/example/solidconnection/university/fixture/UniversityFixtureBuilder.java @@ -2,8 +2,8 @@ import com.example.solidconnection.location.country.domain.Country; import com.example.solidconnection.location.region.domain.Region; -import com.example.solidconnection.university.domain.University; -import com.example.solidconnection.university.repository.UniversityRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepositoryForTest; import lombok.RequiredArgsConstructor; import org.springframework.boot.test.context.TestComponent; @@ -11,7 +11,7 @@ @RequiredArgsConstructor public class UniversityFixtureBuilder { - private final UniversityRepository universityRepository; + private final HostUniversityRepositoryForTest hostUniversityRepositoryForTest; private String koreanName; private String englishName; @@ -19,7 +19,7 @@ public class UniversityFixtureBuilder { private Region region; public UniversityFixtureBuilder university() { - return new UniversityFixtureBuilder(universityRepository); + return new UniversityFixtureBuilder(hostUniversityRepositoryForTest); } public UniversityFixtureBuilder koreanName(String koreanName) { @@ -42,8 +42,8 @@ public UniversityFixtureBuilder region(Region region) { return this; } - public University create() { - University university = new University( + public HostUniversity create() { + HostUniversity university = new HostUniversity( null, koreanName, englishName, "formatName", "https://homepage-url", @@ -53,6 +53,6 @@ public University create() { "https://background-image-url", null, country, region ); - return universityRepository.save(university); + return hostUniversityRepositoryForTest.save(university); } } diff --git a/src/test/java/com/example/solidconnection/university/repository/HostUniversityRepositoryForTest.java b/src/test/java/com/example/solidconnection/university/repository/HostUniversityRepositoryForTest.java new file mode 100644 index 000000000..c941a5199 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/repository/HostUniversityRepositoryForTest.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.university.repository; + +import com.example.solidconnection.university.domain.HostUniversity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface HostUniversityRepositoryForTest extends JpaRepository { +} diff --git a/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java index ef6a55bcb..7bf901a0a 100644 --- a/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java @@ -35,7 +35,7 @@ void setUp() { term = termFixture.현재_학기("2025-2"); univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); + univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()); univApplyInfoFixture.네바다주립대학_라스베이거스_지원_정보(term.getId()); univApplyInfoFixture.메모리얼대학_세인트존스_A_지원_정보(term.getId()); univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId()); diff --git a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java index cf8a0590d..83d960ca9 100644 --- a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoQueryServiceTest.java @@ -1,11 +1,9 @@ package com.example.solidconnection.university.service; import static com.example.solidconnection.common.exception.ErrorCode.UNIV_APPLY_INFO_NOT_FOUND; -import static com.example.solidconnection.university.domain.LanguageTestType.TOEIC; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; @@ -15,10 +13,8 @@ import com.example.solidconnection.term.fixture.TermFixture; import com.example.solidconnection.university.domain.UnivApplyInfo; import com.example.solidconnection.university.dto.UnivApplyInfoDetailResponse; -import com.example.solidconnection.university.dto.UnivApplyInfoFilterSearchRequest; import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse; import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponses; -import com.example.solidconnection.university.fixture.LanguageRequirementFixture; import com.example.solidconnection.university.fixture.UnivApplyInfoFixture; import com.example.solidconnection.university.repository.UnivApplyInfoRepository; import java.util.List; @@ -42,9 +38,6 @@ class UnivApplyInfoQueryServiceTest { @Autowired private UnivApplyInfoFixture univApplyInfoFixture; - @Autowired - private LanguageRequirementFixture languageRequirementFixture; - @Autowired private TermFixture termFixture; @@ -98,70 +91,6 @@ class 대학_지원_정보_상세_조회 { } } - @Nested - class 대학_지원_정보_필터링_검색 { - - @Test - void 어학_시험_종류로_필터링한다() { - // given - UnivApplyInfoFilterSearchRequest request = new UnivApplyInfoFilterSearchRequest(TOEIC, null, null); - UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - languageRequirementFixture.토익_800(괌대학_A_지원_정보); - UnivApplyInfo 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); - languageRequirementFixture.토플_70(괌대학_B_지원_정보); - - // when - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request); - - // then - assertThat(response.univApplyInfoPreviews()) - .containsExactly(UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName())); - } - - @Test - void 어학_시험_점수가_기준치_이상인_곳을_필터링한다() { - // given - UnivApplyInfoFilterSearchRequest request = new UnivApplyInfoFilterSearchRequest(TOEIC, "800", null); - UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - languageRequirementFixture.토익_800(괌대학_A_지원_정보); - UnivApplyInfo 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); - languageRequirementFixture.토익_900(괌대학_B_지원_정보); - - // when - UnivApplyInfoPreviewResponses response = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request); - - // then - assertThat(response.univApplyInfoPreviews()) - .containsExactly(UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName())); - } - - @Test - void 국가_코드로_필터링한다() { - // given - UnivApplyInfoFilterSearchRequest request1 = new UnivApplyInfoFilterSearchRequest(TOEIC, null, List.of("US")); - UnivApplyInfoFilterSearchRequest request2 = new UnivApplyInfoFilterSearchRequest(TOEIC, null, List.of("US", "CA")); - UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - languageRequirementFixture.토익_800(괌대학_A_지원_정보); - UnivApplyInfo 메모리얼대학_세인트존스_A_지원_정보 = univApplyInfoFixture.메모리얼대학_세인트존스_A_지원_정보(term.getId()); - languageRequirementFixture.토익_800(메모리얼대학_세인트존스_A_지원_정보); - - // when - UnivApplyInfoPreviewResponses response1 = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request1); - UnivApplyInfoPreviewResponses response2 = univApplyInfoQueryService.searchUnivApplyInfoByFilter(request2); - - // then - assertAll( - () -> assertThat(response1.univApplyInfoPreviews()) - .containsExactly(UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName())), - () -> assertThat(response2.univApplyInfoPreviews()) - .containsExactlyInAnyOrder( - UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보, term.getName()) - ) - ); - } - } - @Nested class 대학_지원_정보_텍스트_검색 { @@ -177,8 +106,8 @@ class 대학_지원_정보_텍스트_검색 { // then assertThat(response.univApplyInfoPreviews()) .containsExactly( - UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(메이지대학_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(괌대학_A_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(메이지대학_지원_정보, term.getName()) ); } @@ -199,8 +128,8 @@ class 각각의_검색_대상에_대해_검색한다 { // then assertThat(response.univApplyInfoPreviews()) .containsExactly( - UnivApplyInfoPreviewResponse.from(메이지대학_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(메이지대학_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(메모리얼대학_세인트존스_A_지원_정보, term.getName()) ); } @@ -209,7 +138,7 @@ class 각각의_검색_대상에_대해_검색한다 { // given String text = "미국"; UnivApplyInfo 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - UnivApplyInfo 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); + UnivApplyInfo 버지니아공과대학_지원_정보 = univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()); univApplyInfoFixture.메이지대학_지원_정보(term.getId()); // when @@ -218,8 +147,8 @@ class 각각의_검색_대상에_대해_검색한다 { // then assertThat(response.univApplyInfoPreviews()) .containsExactly( - UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(괌대학_B_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(괌대학_A_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(버지니아공과대학_지원_정보, term.getName()) ); } @@ -237,8 +166,8 @@ class 각각의_검색_대상에_대해_검색한다 { // then assertThat(response.univApplyInfoPreviews()) .containsExactly( - UnivApplyInfoPreviewResponse.from(린츠_카톨릭대학_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(서던덴마크대학교_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(린츠_카톨릭대학_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(서던덴마크대학교_지원_정보, term.getName()) ); } } @@ -257,9 +186,9 @@ class 각각의_검색_대상에_대해_검색한다 { // then assertThat(response.univApplyInfoPreviews()) .containsExactly( - UnivApplyInfoPreviewResponse.from(대학지원정보_아, term.getName()), - UnivApplyInfoPreviewResponse.from(국가_아, term.getName()), - UnivApplyInfoPreviewResponse.from(권역_아, term.getName()) + UnivApplyInfoPreviewResponse.of(대학지원정보_아, term.getName()), + UnivApplyInfoPreviewResponse.of(국가_아, term.getName()), + UnivApplyInfoPreviewResponse.of(권역_아, term.getName()) ); } diff --git a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java index 9eeeb2c2b..f6b425e3f 100644 --- a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java @@ -58,7 +58,7 @@ class UnivApplyInfoRecommendServiceTest { private SiteUser user; private Term term; private UnivApplyInfo 괌대학_A_지원_정보; - private UnivApplyInfo 괌대학_B_지원_정보; + private UnivApplyInfo 버지니아공과대학_지원_정보; private UnivApplyInfo 네바다주립대학_라스베이거스_지원_정보; private UnivApplyInfo 메모리얼대학_세인트존스_A_지원_정보; private UnivApplyInfo 서던덴마크대학교_지원_정보; @@ -69,7 +69,7 @@ void setUp() { term = termFixture.현재_학기("2025-2"); user = siteUserFixture.사용자(); 괌대학_A_지원_정보 = univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - 괌대학_B_지원_정보 = univApplyInfoFixture.괌대학_B_지원_정보(term.getId()); + 버지니아공과대학_지원_정보 = univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()); 네바다주립대학_라스베이거스_지원_정보 = univApplyInfoFixture.네바다주립대학_라스베이거스_지원_정보(term.getId()); 메모리얼대학_세인트존스_A_지원_정보 = univApplyInfoFixture.메모리얼대학_세인트존스_A_지원_정보(term.getId()); 서던덴마크대학교_지원_정보 = univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId()); @@ -93,10 +93,10 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsAll(List.of( - UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(괌대학_B_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(괌대학_A_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(버지니아공과대학_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(메모리얼대학_세인트존스_A_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(네바다주립대학_라스베이거스_지원_정보, term.getName()) )); } @@ -112,8 +112,8 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsAll(List.of( - UnivApplyInfoPreviewResponse.from(서던덴마크대학교_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(코펜하겐IT대학_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(서던덴마크대학교_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(코펜하겐IT대학_지원_정보, term.getName()) )); } @@ -130,12 +130,12 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsExactlyInAnyOrder( - UnivApplyInfoPreviewResponse.from(괌대학_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(괌대학_B_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(서던덴마크대학교_지원_정보, term.getName()), - UnivApplyInfoPreviewResponse.from(코펜하겐IT대학_지원_정보, term.getName()) + UnivApplyInfoPreviewResponse.of(괌대학_A_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(버지니아공과대학_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(메모리얼대학_세인트존스_A_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(네바다주립대학_라스베이거스_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(서던덴마크대학교_지원_정보, term.getName()), + UnivApplyInfoPreviewResponse.of(코펜하겐IT대학_지원_정보, term.getName()) ); } @@ -149,10 +149,7 @@ void setUp() { .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsExactlyInAnyOrderElementsOf( generalUnivApplyInfoRecommendService.getGeneralRecommends().stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.from( - univApplyInfo, - term.getName() - )) + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) .toList() ); } @@ -167,10 +164,7 @@ void setUp() { .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsExactlyInAnyOrderElementsOf( generalUnivApplyInfoRecommendService.getGeneralRecommends().stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.from( - univApplyInfo, - term.getName() - )) + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) .toList() ); }