From 34fb79ef706e87d431cb17c55f804242a8a0a8a2 Mon Sep 17 00:00:00 2001 From: DongkyunKim <134477764+dongkyun0713@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:27:22 +0900 Subject: [PATCH] Main push (#167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat(#29) 필터 기능 구현 * Refactor(#29): GenericFilterBean로 filter 기능 확장 * Refactor(#29): WebConfig 위치 변경 * Refactor(#6): AuthService getIdToken 메서드 추가 구현 * Refactor(#6): validateNotFoundEmail 메서드 예외 메시지 추가 작성 * Refactor(#6): validateNotFoundEmail 메서드 객체지향 생활 체조 9가지 원칙 규칙 4 적용 * Feat(#29): getUserIdFromToken 메서드 구현 * Refactor(#29): getUserEmailFromToken로 수정 * Refactor(AJD-Archive#6): generateAccessToken 메서드 분리 * Refactor(AJD-Archive#6): createMember 메서드 분리 * Refactor(AJD-Archive#6): AuthService 들여쓰기 수정 * Refactor(AJD-Archive#6): AuthService 상수 static 추가 * Test(#18): test yml 파일 추가, 적용 * Test(#20): yml 파일 적용 코드 추가 * Feat(#29): 어노테이션 구현 * Refactor(#29): 클래스 이름 수정 * Test(#18): BlockControllerTest Restdocs 적용 * Refeactor(#6): 커스텀 예외처리 추가 구현 * Feat(#11): Block 상태별 전체 조회, 단건 조회 기능 구현 * Feat(#11): Block 상태별 전체 조회, 단건 조회 기능 구현 * Test(#18): Block 상태별 전체 조회, 단건 조회 테스트 코드 구현 * Refactor(#11): 불필요 코드 제거 * Test(#20): AuthService 테스트코드 작성 * Test(#20): AuthService 테스트코드 작성 * Test(#20): AuthController 테스트코드 작성 * Test(44): LoginCheckFilter 테스트코드 작성 * Test(#44): LoginCheckFilter 테스트코드 작성 * Test(#20): GoogleAuthService 테스트코드 수정 * Test(#44): LoginCheckFilter 테스트코드 주석 수정 * Test(#44): urrentUserEmailArgumentResolver 테스트코드 작성 * Feat(#11): Block 논리 삭제 기능 구현, Block 상태별 전체조회 Query 수정 * Test(#18): Block 논리 삭제 테스트 코드 구현 * Test(#18): Block 논리 삭제 Restdocs 수정 * Test(#18): Block 테스트 코드 구현 * Test(#18): Repository 테스트를 위한 h2 의존성 추가 * Test(#18): BlockRepository 테스트 코드 구현 * Refactor: 중복 클레스 제거 * Test: TokenProvider 목킹 의존성 주입 추가 * Feat(#48): 프로필 정보 조회 기능 구현 * Refactor(#48): MemberController 위치 변경 * Refactor(#48): mypage 반환 값 변경 * Feat(#48): 프로필 정보 수정 구현 * Feat(#52): 프로필 정보 조회 테스트 * Feat(#52): 프로필 정보 변경 테스트 * Refactor(#52): 프로필 조회 관련 url 수정 * Feat(#50): Validation 의존성 추가 * Refactor: RestTemplate 의존성 주입으로 수정 * Feat(#50): Challenge 구현 * Feat(#50): Cycle 구현 * Feat(#50): CycleDetail 구현 * Feat(#48): 회원가입시 랜덤 닉네임 부여 * Refactor(#50): 생성자에 status 추가 * Feat(#50): ChallengeSaveReqDto 구현 * Feat(#50): ChallengeInfoResDto 구현 * Feat(#50):CycleList를 json 배열로 저장하기 위한 CycleDetailsConverter 구현 * Feat(#50): ChallengeRepository 구현 * Feat(#50): customException 구현 * Feat(#50): customException 구현 * Feat(#50): ChallengeService 구현 * Feat(#50): ChallengeController 구현 * Test(#50): ChallengeTest 작성 * Test(#50): CycleDetailsConverterTest 작성 * Test(#50): ChallengeServiceTest 작성 * Refactor(#48): 닉네임 관련 객체들 빈으로 등록 * Refactor(#48): 닉네임 중복 확인 로직 데이터베이스 접근 방법으로 수정 * Refactor(#48): 닉네임 중복 확인 로직 로직 추가 * Refactor(#48): 닉네임 부여 방식 수정 * Refactor(#50): 날짜 타입을 String에서 LocalDate타입으로 변경 * Refactor(#50):생성 요청 dto 날짜 타입을 String에서 LocalDate타입으로 변경 * Feat(#50): ChallengeController.class 추가 * Refactor(#48): 닉네임 타임아웃 예외처리 수정 * Refacor(): Staus 필드 명 변경 * Refactor():챌린지 생성 메서드 명 변경 * Refactor():챌린지 생성 메서드 명 변경 * Feat(): MemberNotFoundException 추가 * Refactor(#50): Status.ACTIVE로 변경 * Refactor(#50): Status.ACTIVE로 변경 * Feat(#50): challengeService 추가 * Feat(#50): DELETED로 변경 * Refactor(#48): 회원가입시 닉네임 이름 동일화 코드 수정 * Refactor: status 수정 * Refactor: status 수정 * Refactor: MockBean 수정 * Refactor(#48): 닉네임 중복 가능하게 로직 수정 * Refactor(#11): RequestParam에 name 속성 추가 * Refactor: 닉네임 반환 테스트코드 수정 * Refactor: 쓰지 않는 timeout 예외처리 삭제 * Refactor: 쓰지 않는 timeout 예외처리 삭제 * Refactor(#48): 닉네임 중복 가능명사, 형용사 주입 오류 수정 * Refactor(#52): 닉네임 생성 테스트코드 * Update README.md * Feat(#48): 회원가입시 랜덤 태그 부여 * Feat(#48): 랜덤 태그 중복 처리 * Feat(#48): 랜덤 태그 중복 처리 * Refactor(#48): return 개행 수정 * Refactor(#52): tag 생성 테스트코드 * Refactor(#52): MemberController 테스트코드 작성 * Refactor(#52): 개행 수정 * Refactor(#11): Block 사용자 선언, dto 정적 팩토리 메소드 수정 * Refactor(#18): ControllerTest Header 추가, mockMvc 설정 * Refactor(#48): MemberCustomRepository 이름 수정 * Feat(#62): Dashboard 도메인 구현, Block에 dashboard 연관 관계 추가 * Feat(#62): PersonalDashboard 저장, 수정 기능 구현 * Test(#62): PersonalDashboardService 저장, 수정 테스트코드 구현 * Refactor(#62): PersonalDashboard 수정 요청값 추가 * Refactor(#62): 개인 대시보드 Save DTO에 Validation 예외 추가 * Refactor(#62): 블록 생성 수정(대시보드 요청 추가) * Test(#62): 블록 생성 수정(대시보드 요청 추가) * Test(#62): 개인 대시보드 생성, 수정 테스트 코드 구현, 블록 컨트롤러 어노테이션 수정, Dto 어노테이션 수정 * Refactor(#48): MemberCustomRepositoryImpl 이름 수정 * Feat(#62): 개인 대시보드 삭제 기능 구현 * Refactor(#48): config 위치 수정 * Test(#62): 개인 대시보드 삭제 테스트 코드 구현 * Refactor(#62): 블록 리스트 요청시 대시보드 아이디 추가 구현 * Test(#62): 블록 리스트 요청시 대시보드 아이디 추가 테스트 구현 * Feat(#62): 개인 대시보드 전체, 상세 조회 기능 구현 * Test(#62): 개인 대시보드 전체, 상세 조회 테스트 코드 구현 * Docs(#62): 개인 대시보드 Restdocs 정적 페이지 구현 * Feat(#62): 개인 대시보드 상세 보기에 블록 진행도 추가 * Test(#62): 개인 대시보드 상세 보기 테스트에 블록 진행도 추가 * Test(#62): 개인 대시보드 Entity, Repository 테스트 코드 구현 * Refactor(#62): 개인 대시보드 queryDSL 코드 수정 * Test(#62): 개인 대시보드 ControllerTest 반환 추가 * Docs(#52): 내 프로필 정보 조회 API 문서 작성 * Docs(#52): 로그인, 토큰 재발급 API 문서 작성 * Refactor(#11): 블록 삭제 기능 메시지 변경 * Refactor(#18): 블록 삭제 테스트 DisplayName 수정 * Refactor(#11): 블록 Type 추가 * Feat(#67 #62): 팀 대시보드 기능 구현, 테스트 코드 구현, 개인 대시보드 삭제 메시지, queryDSL 수정 (#68) * Feat(#67): 팀 대시보드 도메인 구현 * Feat(#67): 팀 대시보드 저장 기능 구현 * Feat(#67): 팀 대시보드 수정 기능 구현 * Refactor(#62): 개인 대시보드 삭제 기능 메시지 변경 * Refactor(#62): 개인 대시보드 삭제 예외 처리 추가 * Refactor(#62): 개인 대시보드 queryDSL 수정 * Test(#62): 개인 대시보드 삭제 테스트 DisplayName 수정 * Feat(#67): 팀 대시보드 삭제 기능 구현 * Feat(#67): 팀 대시보드 전체, 상세 조회 기능 구현 * Refactor(#62): 개인 대시보드 정적 팩토리 메소드명 수정 * Feat(#67): 팀 대시보드 참여, 탈퇴 기능 구현 * Feat(#67): 팀 대시보드 초대 멤버 리스트 불러오는 기능 구현 * Test(#67): 팀 대시보드 컨트롤러 테스트 코드 구현 * Test(#67): 팀 대시보드 테스트 코드 구현 * Test(#67): 팀 대시보드 서비스 테스트 코드 구현 * Test(#67): 팀 대시보드 레포지토리 테스트 코드 구현 * Test(#62): 개인 대시보드 서비스 테스트 코드 수정 * Test(#67): 팀 대시보드 서비스 테스트 코드 수정 * Docs(#67): 팀 대시보드 Restdocs 정적 페이지 추가 * Refactor(#11 #18): 블록 반환 값 추가, 반환 값 추가로 인한 테스트 코드 수정 (#71) * Refactor(#11): 블록 반환 값 추가 * Refactor(#18): 블록 반환 값 추가로 인한 테스트 코드 수정 * Feat(#50): 챌린지 전체 조회, 상세 조회, 삭제, 검색 기능 추가 (#64) * Feat(#50): 수정 메서드, 상태 변경 메서드 추가 * Feat(#50): 멤버-챌린지 연관관계 설정 * Feat(#50): 챌린지 커스텀 레포지토리 인터페이스 추가 * Feat(#50): 챌린지 커스텀 레포지토리 구현체 추가 * Feat(#50): 챌린지 커스텀 레포지토리 상속받도록 추가 * Refactor(#50): dto단에서 예외처리하도록 변경 * Refactor(#50): 정적 팩토리 메서드 네이밍, 매개변수 변경 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 기능 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 api 추가 * Feat(#50): ChallengeAccessDeniedException 추가 * Feat(#50): ChallengeNotFoundException 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 커스텀 예외에서 메시지 처리하도록 변경 * Test(#50): 수정, 상태 변경 로직 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 메서드 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 api 테스트 및 레스트독스 적용 * Docs(#50): challenge.adoc 추가 * Docs(#50): index.adoc 추가 * Test(#50): ControllerTest ChallengeController.class 추가 * Feat(#50): Valid 어노테이션 추가 * Style(#50): 코즈 포맷팅 수정 * Refactor(#50): startDate, endDate @NotNull 적용 * Style(#50): 경로변수 카멜케이스로 변경 * Style(#50): 메서드 명 변경 * Style(#50): 매개변수명 변경 * Refactor(#50): 상세조회에 readOnly옵션 설정 * Feat(#50): cycle 필드 저장하도록 변경 * Feat(#50): 블록 엔티티에 챌린지 매핑 및 상태 수정 메서드 추가 * Feat(#50): CycleDetail value 추가 * Feat(#50): status 계산을 위한 ChallengeBlockStatusUtil 추가 * Feat(#50): 매일 자정에 챌린지 블록 상태를 업데이트 하기 위한 스케줄러 설정 * Feat(#50): 챌린지 참여 메서드 추가 * Feat(#50): 챌린지 참여 메서드 추가 * Test(#50): ChallengeBlockStatusUtilTest 추가 * Test(#50): 챌린지 참여 메서드 테스트 추가 * Feat(#50): findByType 추가 * Docs(#50): 챌린지 참여 문서화 * Style(#50): 불필요한 공백 라인 제거 * Test(#50): 블록 도메인에 타입 필드 추가에 따른 챌린지 컨트롤러 테스트 코드 수정 (#72) * Feat(#50): 수정 메서드, 상태 변경 메서드 추가 * Feat(#50): 멤버-챌린지 연관관계 설정 * Feat(#50): 챌린지 커스텀 레포지토리 인터페이스 추가 * Feat(#50): 챌린지 커스텀 레포지토리 구현체 추가 * Feat(#50): 챌린지 커스텀 레포지토리 상속받도록 추가 * Refactor(#50): dto단에서 예외처리하도록 변경 * Refactor(#50): 정적 팩토리 메서드 네이밍, 매개변수 변경 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 기능 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 api 추가 * Feat(#50): ChallengeAccessDeniedException 추가 * Feat(#50): ChallengeNotFoundException 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 커스텀 예외에서 메시지 처리하도록 변경 * Test(#50): 수정, 상태 변경 로직 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 메서드 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 api 테스트 및 레스트독스 적용 * Docs(#50): challenge.adoc 추가 * Docs(#50): index.adoc 추가 * Test(#50): ControllerTest ChallengeController.class 추가 * Feat(#50): Valid 어노테이션 추가 * Style(#50): 코즈 포맷팅 수정 * Refactor(#50): startDate, endDate @NotNull 적용 * Style(#50): 경로변수 카멜케이스로 변경 * Style(#50): 메서드 명 변경 * Style(#50): 매개변수명 변경 * Refactor(#50): 상세조회에 readOnly옵션 설정 * Feat(#50): cycle 필드 저장하도록 변경 * Feat(#50): 블록 엔티티에 챌린지 매핑 및 상태 수정 메서드 추가 * Feat(#50): CycleDetail value 추가 * Feat(#50): status 계산을 위한 ChallengeBlockStatusUtil 추가 * Feat(#50): 매일 자정에 챌린지 블록 상태를 업데이트 하기 위한 스케줄러 설정 * Feat(#50): 챌린지 참여 메서드 추가 * Feat(#50): 챌린지 참여 메서드 추가 * Test(#50): ChallengeBlockStatusUtilTest 추가 * Test(#50): 챌린지 참여 메서드 테스트 추가 * Feat(#50): findByType 추가 * Docs(#50): 챌린지 참여 문서화 * Style(#50): 불필요한 공백 라인 제거 * feat(#50): 블록 도메인에 타입 추가에 따른 테스트 코드 수정 * Refactor(#11 #18): 블록 시작 시간 속성 추가 (#75) * Refactor(#11): 블록 반환 값 추가 * Refactor(#18): 블록 반환 값 추가로 인한 테스트 코드 수정 * Refactor(#11): 블록 시작 시간 속성 추가 * Test(#18): 블록 시작 시간 속성이 추가됨으로써 테스트 코드 수정 * Feat(#67 #62): 팀 대시보드 기능 구현, 테스트 코드 구현, 개인 대시보드 삭제 메시지, queryDSL 수정 (#68) * Feat(#67): 팀 대시보드 도메인 구현 * Feat(#67): 팀 대시보드 저장 기능 구현 * Feat(#67): 팀 대시보드 수정 기능 구현 * Refactor(#62): 개인 대시보드 삭제 기능 메시지 변경 * Refactor(#62): 개인 대시보드 삭제 예외 처리 추가 * Refactor(#62): 개인 대시보드 queryDSL 수정 * Test(#62): 개인 대시보드 삭제 테스트 DisplayName 수정 * Feat(#67): 팀 대시보드 삭제 기능 구현 * Feat(#67): 팀 대시보드 전체, 상세 조회 기능 구현 * Refactor(#62): 개인 대시보드 정적 팩토리 메소드명 수정 * Feat(#67): 팀 대시보드 참여, 탈퇴 기능 구현 * Feat(#67): 팀 대시보드 초대 멤버 리스트 불러오는 기능 구현 * Test(#67): 팀 대시보드 컨트롤러 테스트 코드 구현 * Test(#67): 팀 대시보드 테스트 코드 구현 * Test(#67): 팀 대시보드 서비스 테스트 코드 구현 * Test(#67): 팀 대시보드 레포지토리 테스트 코드 구현 * Test(#62): 개인 대시보드 서비스 테스트 코드 수정 * Test(#67): 팀 대시보드 서비스 테스트 코드 수정 * Docs(#67): 팀 대시보드 Restdocs 정적 페이지 추가 * Refactor(#11 #18): 블록 반환 값 추가, 반환 값 추가로 인한 테스트 코드 수정 (#71) * Refactor(#11): 블록 반환 값 추가 * Refactor(#18): 블록 반환 값 추가로 인한 테스트 코드 수정 * Feat(#50): 챌린지 전체 조회, 상세 조회, 삭제, 검색 기능 추가 (#64) * Feat(#50): 수정 메서드, 상태 변경 메서드 추가 * Feat(#50): 멤버-챌린지 연관관계 설정 * Feat(#50): 챌린지 커스텀 레포지토리 인터페이스 추가 * Feat(#50): 챌린지 커스텀 레포지토리 구현체 추가 * Feat(#50): 챌린지 커스텀 레포지토리 상속받도록 추가 * Refactor(#50): dto단에서 예외처리하도록 변경 * Refactor(#50): 정적 팩토리 메서드 네이밍, 매개변수 변경 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 기능 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 api 추가 * Feat(#50): ChallengeAccessDeniedException 추가 * Feat(#50): ChallengeNotFoundException 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 커스텀 예외에서 메시지 처리하도록 변경 * Test(#50): 수정, 상태 변경 로직 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 메서드 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 api 테스트 및 레스트독스 적용 * Docs(#50): challenge.adoc 추가 * Docs(#50): index.adoc 추가 * Test(#50): ControllerTest ChallengeController.class 추가 * Feat(#50): Valid 어노테이션 추가 * Style(#50): 코즈 포맷팅 수정 * Refactor(#50): startDate, endDate @NotNull 적용 * Style(#50): 경로변수 카멜케이스로 변경 * Style(#50): 메서드 명 변경 * Style(#50): 매개변수명 변경 * Refactor(#50): 상세조회에 readOnly옵션 설정 * Feat(#50): cycle 필드 저장하도록 변경 * Feat(#50): 블록 엔티티에 챌린지 매핑 및 상태 수정 메서드 추가 * Feat(#50): CycleDetail value 추가 * Feat(#50): status 계산을 위한 ChallengeBlockStatusUtil 추가 * Feat(#50): 매일 자정에 챌린지 블록 상태를 업데이트 하기 위한 스케줄러 설정 * Feat(#50): 챌린지 참여 메서드 추가 * Feat(#50): 챌린지 참여 메서드 추가 * Test(#50): ChallengeBlockStatusUtilTest 추가 * Test(#50): 챌린지 참여 메서드 테스트 추가 * Feat(#50): findByType 추가 * Docs(#50): 챌린지 참여 문서화 * Style(#50): 불필요한 공백 라인 제거 * Test(#50): 블록 도메인에 타입 필드 추가에 따른 챌린지 컨트롤러 테스트 코드 수정 (#72) * Feat(#50): 수정 메서드, 상태 변경 메서드 추가 * Feat(#50): 멤버-챌린지 연관관계 설정 * Feat(#50): 챌린지 커스텀 레포지토리 인터페이스 추가 * Feat(#50): 챌린지 커스텀 레포지토리 구현체 추가 * Feat(#50): 챌린지 커스텀 레포지토리 상속받도록 추가 * Refactor(#50): dto단에서 예외처리하도록 변경 * Refactor(#50): 정적 팩토리 메서드 네이밍, 매개변수 변경 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): ChallengeListResDto 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 기능 추가 * Feat(#50): 조회, 상세조회, 수정, 삭제, 검색 api 추가 * Feat(#50): ChallengeAccessDeniedException 추가 * Feat(#50): ChallengeNotFoundException 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 메서드 추가 * Refactor(#50): 커스텀 예외에서 메시지 처리하도록 변경 * Test(#50): 수정, 상태 변경 로직 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 메서드 테스트 추가 * Test(#50): ChallengeServiceTest 조회, 수정, 삭제, 검색 api 테스트 및 레스트독스 적용 * Docs(#50): challenge.adoc 추가 * Docs(#50): index.adoc 추가 * Test(#50): ControllerTest ChallengeController.class 추가 * Feat(#50): Valid 어노테이션 추가 * Style(#50): 코즈 포맷팅 수정 * Refactor(#50): startDate, endDate @NotNull 적용 * Style(#50): 경로변수 카멜케이스로 변경 * Style(#50): 메서드 명 변경 * Style(#50): 매개변수명 변경 * Refactor(#50): 상세조회에 readOnly옵션 설정 * Feat(#50): cycle 필드 저장하도록 변경 * Feat(#50): 블록 엔티티에 챌린지 매핑 및 상태 수정 메서드 추가 * Feat(#50): CycleDetail value 추가 * Feat(#50): status 계산을 위한 ChallengeBlockStatusUtil 추가 * Feat(#50): 매일 자정에 챌린지 블록 상태를 업데이트 하기 위한 스케줄러 설정 * Feat(#50): 챌린지 참여 메서드 추가 * Feat(#50): 챌린지 참여 메서드 추가 * Test(#50): ChallengeBlockStatusUtilTest 추가 * Test(#50): 챌린지 참여 메서드 테스트 추가 * Feat(#50): findByType 추가 * Docs(#50): 챌린지 참여 문서화 * Style(#50): 불필요한 공백 라인 제거 * feat(#50): 블록 도메인에 타입 추가에 따른 테스트 코드 수정 * Refactor(#11): 블록 시작 시간 속성 추가 * Fix: 컨플릭 수정 * Fix: 컨플릭 수정 --------- Co-authored-by: DongkyunKim <134477764+dongkyun0713@users.noreply.github.com> * Test (#76) * Feat(#67): 팀 대시보드 도메인 구현 * Feat(#67): 팀 대시보드 저장 기능 구현 * Feat(#67): 팀 대시보드 수정 기능 구현 * Refactor(#62): 개인 대시보드 삭제 기능 메시지 변경 * Refactor(#62): 개인 대시보드 삭제 예외 처리 추가 * Refactor(#62): 개인 대시보드 queryDSL 수정 * Test(#62): 개인 대시보드 삭제 테스트 DisplayName 수정 * Feat(#67): 팀 대시보드 삭제 기능 구현 * Feat(#67): 팀 대시보드 전체, 상세 조회 기능 구현 * Refactor(#62): 개인 대시보드 정적 팩토리 메소드명 수정 * Feat(#67): 팀 대시보드 참여, 탈퇴 기능 구현 * Feat(#67): 팀 대시보드 초대 멤버 리스트 불러오는 기능 구현 * Test(#67): 팀 대시보드 컨트롤러 테스트 코드 구현 * Test(#67): 팀 대시보드 테스트 코드 구현 * Test(#67): 팀 대시보드 서비스 테스트 코드 구현 * Test(#67): 팀 대시보드 레포지토리 테스트 코드 구현 * Test(#62): 개인 대시보드 서비스 테스트 코드 수정 * Test(#67): 팀 대시보드 서비스 테스트 코드 수정 * Docs(#67): 팀 대시보드 Restdocs 정적 페이지 추가 * Feat(#70): 팀 문서, 팀 파일 CRUD 구현 (#73) * Feat(#70): 팀 문서, 팀 파일 CRUD 구현 초안 * Test(#70): 팀 파일 repositoryTest 구현 * Test(#70): 팀 파일 Controller, Service, domain 구현 * Test(#70): 레스트독스 적용 * Feat(#70): 팀 문서 CRUD 완성 * Test(#70): DocumentRepository Test 구현 * Test(#70): DocumentRepositoryTest 구현 * Refactor(#70): Document 주석 삭제 * Test(#70): DocumentTest 구현 * Test(#70): DocumentServiceTest 구현 * Test(#70): DocumentControllerTest 구현 * Refactor(#70): document update 전용 dto 생성 * Refactor(#70): 컨밴션 수정 * Refactor(#70): 띄어쓰기 컨밴션 수정 * Refactor(#70): 띄어쓰기 컨밴션 수정 * Feature(#74): 알림 기능 구현 (#77) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Feat(#81): 챌린지 카테고리 추가 (#83) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Refactor: 컨벤션에 맞게 수정 (#80) * Feat(#70): 팀 문서, 팀 파일 CRUD 구현 초안 * Test(#70): 팀 파일 repositoryTest 구현 * Test(#70): 팀 파일 Controller, Service, domain 구현 * Test(#70): 레스트독스 적용 * Feat(#70): 팀 문서 CRUD 완성 * Test(#70): DocumentRepository Test 구현 * Test(#70): DocumentRepositoryTest 구현 * Refactor(#70): Document 주석 삭제 * Test(#70): DocumentTest 구현 * Test(#70): DocumentServiceTest 구현 * Test(#70): DocumentControllerTest 구현 * Refactor(#70): document update 전용 dto 생성 * Refactor(#70): 컨밴션 수정 * Refactor(#70): 띄어쓰기 컨밴션 수정 * Refactor(#70): 띄어쓰기 컨밴션 수정 * Refactor: 컨벤션에 맞게 수정 * Refactor: 컨벤션에 맞게 수정 * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 (#82) * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 어노테이션 순서 수정 * Feat(#48): 챌린지 이메일로 조회하는 로직 추가, 마이페이지에서 조회하기 로직 추가 * Refactor(#48): 줄바꿈 수정 * Test(#48): 마이페이지 팀 대시보드, 챌린지 조회 테스트코드, 문서 작업 * Feat(#62): 사용자의 개인 대시보드 카테고리 조회 기능 구현 (#84) * Feat(#62): 사용자의 개인 대시보드 카테고리 조회 기능 구현 * Test(#62): 사용자의 개인 대시보드 카테고리 조회 테스트 코드 구현 * Feat(#86): 블록 순번 속성 추가, 순번 지정, 순번 변경 api 기능 구현, 테스트 코드 구현 (#87) * Feat(#86): 블록 순번 속성 추가, 순번 지정, 순번 변경 api 기능 구현 * Test(#86): 블록 순번 속성 추가와 지정함으로써 테스트 코드 구현 * Docs(#86): restdocs 문서 추가 * Docs(#86): restdocs 문서 수정을 위한 테스트 코드 수정 * Refactor(#86): 블록의 대시보드 반환을 위한 dType 속성, 반환 값 추가 * Style: 끝 줄 추가 * Refactor(#88): 개인, 팀 대시보드 전체 조회 페이지네이션 제거 (#89) * Feat(#62): 사용자의 개인 대시보드 카테고리 조회 기능 구현 * Test(#62): 사용자의 개인 대시보드 카테고리 조회 테스트 코드 구현 * Feature(#74): 알림 기능 구현 (#77) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Feat(#81): 챌린지 카테고리 추가 (#83) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Refactor: 컨벤션에 맞게 수정 (#80) * Feat(#70): 팀 문서, 팀 파일 CRUD 구현 초안 * Test(#70): 팀 파일 repositoryTest 구현 * Test(#70): 팀 파일 Controller, Service, domain 구현 * Test(#70): 레스트독스 적용 * Feat(#70): 팀 문서 CRUD 완성 * Test(#70): DocumentRepository Test 구현 * Test(#70): DocumentRepositoryTest 구현 * Refactor(#70): Document 주석 삭제 * Test(#70): DocumentTest 구현 * Test(#70): DocumentServiceTest 구현 * Test(#70): DocumentControllerTest 구현 * Refactor(#70): document update 전용 dto 생성 * Refactor(#70): 컨밴션 수정 * Refactor(#70): 띄어쓰기 컨밴션 수정 * Refactor(#70): 띄어쓰기 컨밴션 수정 * Refactor: 컨벤션에 맞게 수정 * Refactor: 컨벤션에 맞게 수정 * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 (#82) * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 어노테이션 순서 수정 * Feat(#48): 챌린지 이메일로 조회하는 로직 추가, 마이페이지에서 조회하기 로직 추가 * Refactor(#48): 줄바꿈 수정 * Test(#48): 마이페이지 팀 대시보드, 챌린지 조회 테스트코드, 문서 작업 * Refactor(#88): 개인, 팀 대시보드 전체 조회 페이지네이션 제거 --------- Co-authored-by: DongkyunKim <134477764+dongkyun0713@users.noreply.github.com> Co-authored-by: choi inho <129029251+inhooo00@users.noreply.github.com> * Fix(#92): Build시에 Q클래스 생성되지 않는 버그 (#93) * Revert "Refactor(#88): 개인, 팀 대시보드 전체 조회 페이지네이션 제거 (#89)" (#94) This reverts commit dc3dd78f1e0b9ba8ef4c1efccb8417b2fe512cfc. * Refactor(#88): 개인, 팀 대시보드 전체 조회 페이지네이션 제거 (#95) * Revert "Refactor(#88): 개인, 팀 대시보드 전체 조회 페이지네이션 제거 (#89)" This reverts commit dc3dd78f1e0b9ba8ef4c1efccb8417b2fe512cfc. * Refactor(#88): 개인, 팀 대시보드 전체 조회 페이지네이션 제거 * Docs(#90): api 명세서에 enum 항목 설명 추가 (#91) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#18): 테스트 코드 페이지, 사이즈 파라미터 추가 (#98) * Refactor(#74): 팀 대시보드 초대 시 알림 발송 API 추가 (#99) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 및 수정 시 팀 초대 알림 가도록 변경 (#102) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Feat(#100): 삭제된 블록 조회, 영구 삭제 기능, 테스트 코드 구현 (#101) * Refactor(#18): 테스트 코드 페이지, 사이즈 파라미터 추가 * Feat(#100): 삭제된 블록 조회, 영구 삭제 기능 구현 * Test(#100): 삭제된 블록 조회, 영구 삭제 테스트 코드 구현 * Test(#100): 영구 삭제 테스트 코드 수정 * Docs(#100): 삭제된 블록, 영구 삭제 문서화 * Refactor(#104): Response에 챌린지 id 추가 (#105) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor: authServiceFactory로 id토큰 반환하는 로직 수정, JsonNode 반환 방법 DTO 사용 (#103) * Refactor: authServiceFactory로 id토큰 반환하는 로직 수정, JsonNode 반환 방법 DTO 사용 방법으로 변경 * Refactor: authServices로 매개변수 이름 수정 * Docs: 내 정보 수정 api 문서 작업 (#108) * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 어노테이션 순서 수정 * Feat(#48): 챌린지 이메일로 조회하는 로직 추가, 마이페이지에서 조회하기 로직 추가 * Refactor(#48): 줄바꿈 수정 * Test(#48): 마이페이지 팀 대시보드, 챌린지 조회 테스트코드, 문서 작업 * Test: mypage 수정 테스트 코드 작성(아직 미완성) * Docs: 내 정보 수정 api 문서 작업 * Feat(#106): 팀 문서 전체적인 작업 (#107) * Feat(#106): 팀 문서 전체적인 작업 * Feat(#106): 팀 문서 상세 조회 로직 구현 * Test(#106): 팀 문서 컨트롤러 작성 * Test(#106): 팀 문서 컨트롤러 수정 중.. * Refactor(#106): DTO 정적 팩토링 메서드 이름 변경 * Docs(#106): 팀 문서 관전 api docs 파일로 구현 * Refactor(#106): 카테고리 dto 풀어서 전달 * Refactor(#112): 사용자 사진 반환 값 추가 (#113) * Refactor(#112): 사용자 사진 반환 값 추가 * Refactor(#112): 사용자 사진 반환 값 추가로 인한 테스트 코드 수정 * Refactor(#110): 대시보드 접근 권한 수정, 카테고리 중복 수정 (#111) * Refactor(#110): 대시보드 접근 권한 수정, 카테고리 중복 수정 * Test(#110): 카테고리 중복 수정으로 인한 테스트 코드 수정 * Refactor(#106): 카테고리로 팀문서 조회하는 로직 수정 (#109) * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 어노테이션 순서 수정 * Feat(#48): 챌린지 이메일로 조회하는 로직 추가, 마이페이지에서 조회하기 로직 추가 * Refactor(#48): 줄바꿈 수정 * Test(#48): 마이페이지 팀 대시보드, 챌린지 조회 테스트코드, 문서 작업 * Test: mypage 수정 테스트 코드 작성(아직 미완성) * Docs: 내 정보 수정 api 문서 작업 * Test(#106): 팀 문서 post 테스트코드 수정 * Refactor(#106): 카테고리로 팀문서 조회하는 로직 수정 * Docs: teamDocument api 문서 수정 * Test(#106): 팀 문서 post 테스트코드 수정 (#114) * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 어노테이션 순서 수정 * Feat(#48): 챌린지 이메일로 조회하는 로직 추가, 마이페이지에서 조회하기 로직 추가 * Refactor(#48): 줄바꿈 수정 * Test(#48): 마이페이지 팀 대시보드, 챌린지 조회 테스트코드, 문서 작업 * Test: mypage 수정 테스트 코드 작성(아직 미완성) * Docs: 내 정보 수정 api 문서 작업 * Test(#106): 팀 문서 post 테스트코드 수정 * Refactor(#106): 카테고리로 팀문서 조회하는 로직 수정 * Docs: teamDocument api 문서 수정 * Test(#106): 팀 문서 post 테스트코드 수정 * Refactor: mypage 챌랜지, 팀 대시보드 조회 페이지네이션 수정 (#115) * Feat(#48): 마이페이지 챌린지, 팀 대시보드 조회 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 컨벤션 리팩토링 * Refactor(#48): 어노테이션 순서 수정 * Feat(#48): 챌린지 이메일로 조회하는 로직 추가, 마이페이지에서 조회하기 로직 추가 * Refactor(#48): 줄바꿈 수정 * Test(#48): 마이페이지 팀 대시보드, 챌린지 조회 테스트코드, 문서 작업 * Test: mypage 수정 테스트 코드 작성(아직 미완성) * Docs: 내 정보 수정 api 문서 작업 * Test(#106): 팀 문서 post 테스트코드 수정 * Refactor(#106): 카테고리로 팀문서 조회하는 로직 수정 * Docs: teamDocument api 문서 수정 * Test(#106): 팀 문서 post 테스트코드 수정 * Refactor: mypage 챌랜지, 팀 대시보드 조회 페이지네이션 수정 * Docs: 팀 대시보드와 챌린지 정보 조회 API 쿼리 정보 추가 * Fix(#110): 404에러 발생 수정 (#118) * Refactor(#110): 대시보드 접근 권한 수정, 카테고리 중복 수정 * Test(#110): 카테고리 중복 수정으로 인한 테스트 코드 수정 * Fix(#110): 404에러 발생 수정 * Fix(#110): 404에러 발생 수정 (#119) * Refactor(#110): 대시보드 접근 권한 수정, 카테고리 중복 수정 * Test(#110): 카테고리 중복 수정으로 인한 테스트 코드 수정 * Fix(#110): 404에러 발생 수정 * Fix(#110): 404에러 발생 수정 * Refactor: 문서 삭제 * Refactor(#10): 팀 대시보드 팀원에 대시보드 생성자 추가 (#125) * Refactor(#110): 대시보드 접근 권한 수정, 카테고리 중복 수정 * Test(#110): 카테고리 중복 수정으로 인한 테스트 코드 수정 * Fix(#110): 404에러 발생 수정 * Fix(#110): 404에러 발생 수정 * Refactor(#10): 팀 대시보드 팀원에 대시보드 생성자 추가 * Test(#10): 팀 대시보드 팀원에 대시보드 생성자 추가로 인한 테스트 코드 수정 (#126) * Refactor(#110): 대시보드 접근 권한 수정, 카테고리 중복 수정 * Test(#110): 카테고리 중복 수정으로 인한 테스트 코드 수정 * Fix(#110): 404에러 발생 수정 * Fix(#110): 404에러 발생 수정 * Refactor(#10): 팀 대시보드 팀원에 대시보드 생성자 추가 * Test(#10): 팀 대시보드 팀원에 대시보드 생성자 추가로 인한 테스트 코드 수정 * Refacotr(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 (#128) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 (#129) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refactor: 알림 이름 안보내도록 수정 (#130) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor(#131): 알림 전체 조회 시 읽은 여부 true로 변경 (#132) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor(#131): 알림 전체 조회 시 읽은 여부 true로 변경 * Test(#131): 알림 로직 변경에 따른 테스트 코드 수정 (#133) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor(#131): 알림 전체 조회 시 읽은 여부 true로 변경 * Test(#131): 로직 변경에 따른 테스트 코드 수정 * Refactor(#131): 코드 리펙토링 * Refactor(#97): 챌린지 코드 리펙토링 (#135) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Docs(#139): 카테고리 별 검색 문서에 추가 및 쿼리 파라미터 추가 (#140) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feature(#141): 이미지 업로드 기능 추가 (#142) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refcator(#141): url 고유하도록 변경 (#143) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#144): 알림 전체조회 페이지네이션 삭제 (#145) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor(#131): 알림 전체 조회 시 읽은 여부 true로 변경 * Test(#131): 로직 변경에 따른 테스트 코드 수정 * Refactor(#131): 코드 리펙토링 * Refactor(#144): 알림 전체조회 페이지네이션 삭제 * Feat(#146): 모든 알림 읽음으로 표시하는 api 추가 (#147) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor(#131): 알림 전체 조회 시 읽은 여부 true로 변경 * Test(#131): 로직 변경에 따른 테스트 코드 수정 * Refactor(#131): 코드 리펙토링 * Refactor(#144): 알림 전체조회 페이지네이션 삭제 * Feat(#146): 모든 알림 읽음으로 표시하는 api 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 (#149) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#150): 팀 초대 메시지에 대시보드 아이디 추가 (#151) * Feat(#74): 알림 엔티티 구현 * Feat(#74): 알림 레포지토리 구현 * Feat(#74): NotificationInfoResDto 구현 * Feat(#74): NotificationListResDto 구현 * Feat(#74): NotificationNotFoundException 구현 * Feat(#74): service 로직 구현 * Feat(#74): controller 로직 구현 * Feat(#74): spring-boot-starter-web-services 의존성 추가 * Feat(#74): 알림-멤버 매핑 * Feat(#74): 챌린지 참여 할 때 알림가도록 추가 * Feat(#74): 챌린지 생성될때마다 알림가도록 변경 * Style(#74): RequiredArgsConstructor 어노테이션 사용하도록 변경 * Feat(#74):notificationService 추가 * Style(#74):notification 객체 생성 빌더 패턴 방식으로 변경 * Refactor(#74): emitter를 SseEmitterManager 에서 관리하도록 변경 * Style(#74): 불필요한 공백 라인 제거 * Style(#74): 줄바꿈 추가 * Style(#74): 마지막 공백 라인 추가 * Style(#74): SseEmitterManager 추가 * Fix(#74): 챌린지를_개인_대시보드에_추가할_수_있다 테스트 안되는 오류 해결 * Test(#74): NotificationServiceTest 구현 * Feat(#74): 파라미터 name 설정 * Feat(#74): JsonIgnore 어노테이션 삽입 * Feat(#74): isRead 필드 업데이트 메서드 로직 수정 * Test(#74): NotificationController 추가 * Feat(#74): JsonIgnore 어노테이션 삭제 * Fix(#74): Member 필드 삭제 * Test(#74): NotificationController 테스트 구현 * Test(#74): 문서 생성 경로 변경 * Test(#74): 알림 조회 문서 헤더 필드 생성 * Docs(#74): 알림 문서화 * Refactor(#74): 어노테이션 변경 * Refactor(#74): 팀 대시보드 초대 시 알림 메시지 전송 구현 * Refactor(#74): 매직 넘버 상수화 * Test(#74): notificationService 필드 추가 * Feat(#74): 팀원 초대 api 추가 * Refactor(#74): 쿼리스트링으로 입력받도록 변경 및 예외처리 추가 * Test(#74): 팀원 초대 테스트 * Refactor(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Test(#74): 팀 대시보드 생성 시 초대 알림 가도록 변경 * Fix(#74): 충돌 수정 * Refactor(#74): 메서드 분리 * Feat(): 매직 넘버 상수화 * Refactor(#127): 팀원이 팀 초대 수락하면 팀장한테 알람가도록 구현 및 팀 초대 api 로직 수정 * Refacor: 알림 연결 성공시 더미데이터 반환 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor: 알림 이름 안보내도록 수정 * Refacor(#131): 알림 전체 조회 시 읽은 여부 true로 변경 * Test(#131): 로직 변경에 따른 테스트 코드 수정 * Refactor(#131): 코드 리펙토링 * Refactor(#144): 알림 전체조회 페이지네이션 삭제 * Feat(#146): 모든 알림 읽음으로 표시하는 api 추가 * Refactor(#150): 팀 초대 메시지에 대시보드 아이디 추가 * Refactor: 마이페이지 내에서 개인 대시보드 조회 기능 추가 & 상대방 프로필은 isPublic이 true인 것만 보이도록 수정 (#152) * Refactor: 마이페이지 내에서 개인 대시보드 조회 기능 추가 & 상대방 프로필은 isPublic이 true인 것만 보이도록 수정 * Docs: mypage 대시보드 & 챌린지 목록 조회 문서 수정 * Test: mypage 개인 대시보드 확인 로직 추가 수정 * Refactor(#153): 챌린지 상세조회 반환값 수정 (#154) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#153): 챌린지 상세조회 반환값 수정 * Refactor(#155): 팀 대시보드 중복 참여, 참여하지 않았지만 탈퇴하려는 사용자 예외 처리, 참가자 팀 대시보드 리스트 수정 (#156) * Refactor(#153): 챌린지 실시간 완료자 반환하도록 수정 (#157) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#153): 챌린지 상세조회 반환값 수정 * Refactor(#153): 챌린지 실시간 완료자 반환하도록 수정 * Fix(#158): 이전 팀 문서 삭제 (#159) * Refactor(#153): 챌린지 주기마다 블록이 새로 생성되도록 변경 (#160) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#153): 챌린지 상세조회 반환값 수정 * Refactor(#153): 챌린지 실시간 완료자 반환하도록 수정 * Refactor(#153): 챌린지 주기마다 블록이 새로 생성되도록 변경 * Refactor(#153): 필요없는 주석 제거 * Fix(#161): 이미지 수정 시 null 반환하는 오류 해결 (#162) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#153): 챌린지 상세조회 반환값 수정 * Refactor(#153): 챌린지 실시간 완료자 반환하도록 수정 * Refactor(#153): 챌린지 주기마다 블록이 새로 생성되도록 변경 * Refactor(#153): 필요없는 주석 제거 * Refacotr(#161): 이미지 수정 시 null 반환하는 오류 해결 * Fix(#163): 매개변수 순서 수정 (#164) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#153): 챌린지 상세조회 반환값 수정 * Refactor(#153): 챌린지 실시간 완료자 반환하도록 수정 * Refactor(#153): 챌린지 주기마다 블록이 새로 생성되도록 변경 * Refactor(#153): 필요없는 주석 제거 * Refacotr(#161): 이미지 수정 시 null 반환하는 오류 해결 * Fix(#163): 매개변수 수정 * Feat(#165): 챌린지 탈퇴 api 추가" (#166) * Feat(#74): 카테고리 검색 메서드 추가 * Test(#81): * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 클래스 추가 * Feat(#81): 챌린지 엔티티에 카테고리 필드 추가 * Feat(#81): 카테고리 필드 추가 * Feat(#81): 카테고리 검색 메서드 추가 * Feat(#81): 카테고리 필드 추가 * Test(#81): 챌린지 카테고리 검색 테스트 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Fix(#81): 카테고리 타입 변경 * Docs(#90): enum 항목 설명 추가 * Refactor(#97): 매직 넘버 상수로 변환 * Refactor(#104): responseDto에 challengeId 추가 * Refactor(#97): 메서드 분리 * Refacor(#97): 싱글턴 패턴으로 변경 * Test(#97): 싱글턴 패턴으로 변경 * Refacor(#97): cycle 검증을 도메인 계층에서 하도록 변경 * Test(#97): 테스트 코드 오류 수정 * Refactor(#138): 챌린지 생성 시 생성 날짜 자동 주입 및 블록 이름 추가 * Test(#139): 쿼리 파라미터 추가 * Docs(#139): 쿼리 파라미터 추가 및 카테거리 검색 API 추가 * Feat(#141): 이미지 업로드 기능 추가 * Feat(#141): 의존성 추가 * Test(#141): 이미지 업로드 테스트 * Docs(#141): 문서 업데이트 * Test(#141): 이미지 업로드 테스트 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): url 고유하도록 변경 * Refactor(#141): 불필요한 주석 제거 * Feat: 카테고리와 키워드를 통해 챌린지 검색하는 기능 추가 * Refactor(#148): 챌린지 카테고리 및 키워드 검색 api 하나로 통합 * Refactor(#153): 챌린지 상세조회 반환값 수정 * Refactor(#153): 챌린지 실시간 완료자 반환하도록 수정 * Refactor(#153): 챌린지 주기마다 블록이 새로 생성되도록 변경 * Refactor(#153): 필요없는 주석 제거 * Refacotr(#161): 이미지 수정 시 null 반환하는 오류 해결 * Fix(#163): 매개변수 수정 * Feat(#165): 챌린지 탈퇴 api 추가 * Fix: 충돌 해결 * Fix: 충돌 해결 * Fix: 충돌 해결 * Fix: 충돌 해결 --------- Co-authored-by: inhooo00 Co-authored-by: giwoong Co-authored-by: choi inho <129029251+inhooo00@users.noreply.github.com> --- build.gradle | 5 +- .../block/application/BlockService.java | 12 +- .../challenge/api/ChallengeController.java | 74 ++-- .../api/dto/reqeust/ChallengeSaveReqDto.java | 36 +- .../dto/reqeust/ChallengeSearchReqDto.java | 6 +- .../ChallengeCompletedMemberInfoResDto.java | 21 + .../api/dto/response/ChallengeInfoResDto.java | 36 +- .../application/ChallengeService.java | 143 +++--- .../ChallengeBlockStatusUpdateScheduler.java | 67 ++- .../util/ChallengeBlockStatusUtil.java | 10 +- .../challenge/domain/Challenge.java | 95 +++- .../domain/ChallengeMemberMapping.java | 58 +++ .../repository/ChallengeCustomRepository.java | 4 +- .../ChallengeCustomRepositoryImpl.java | 47 +- ...hallengeMemberMappingCustomRepository.java | 8 + ...engeMemberMappingCustomRepositoryImpl.java | 27 ++ .../ChallengeMemberMappingRepository.java | 8 + .../repository/DashboardCustomRepository.java | 4 + .../DashboardCustomRepositoryImpl.java | 53 ++- .../PersonalDashboardPageListResDto.java | 27 ++ .../application/PersonalDashboardService.java | 35 +- .../personal/domain/PersonalDashboard.java | 8 +- .../application/TeamDashboardService.java | 24 +- .../dashboard/team/domain/TeamDashboard.java | 12 +- .../exception/AlreadyJoinedTeamException.java | 13 + .../exception/NotMemberOfTeamException.java | 13 + .../teamdoc/domain/TeamDocument.java | 6 +- .../TeamDocumentCustomRepositoryImpl.java | 6 +- .../teamdocument/api/DocumentController.java | 52 --- .../teamdocument/api/FileController.java | 54 --- .../api/dto/request/DocumentInfoReqDto.java | 19 - .../api/dto/request/DocumentUpdateReqDto.java | 6 - .../api/dto/request/FileInfoReqDto.java | 20 - .../api/dto/response/DocumentInfoResDto.java | 17 - .../api/dto/response/DocumentListResDto.java | 19 - .../api/dto/response/FileInfoResDto.java | 21 - .../api/dto/response/FileListResDto.java | 19 - .../application/DocumentService.java | 70 --- .../teamdocument/application/FileService.java | 76 ---- .../teamdocument/domain/Document.java | 52 --- .../dashboard/teamdocument/domain/File.java | 53 --- .../repository/DocumentCustomRepository.java | 9 - .../DocumentCustomRepositoryImpl.java | 46 -- .../domain/repository/DocumentRepository.java | 7 - .../repository/FileCustomRepository.java | 10 - .../repository/FileCustomRepositoryImpl.java | 46 -- .../domain/repository/FileRepository.java | 7 - .../exception/DocumentNotFoundException.java | 13 - .../exception/FileNotFoundException.java | 13 - .../TeamDashboardNotFoundException.java | 13 - .../kkeujeokbackend/global/aws/S3Service.java | 40 ++ .../InvalidImageUploadException.java | 14 + .../global/config/S3Config.java | 27 ++ .../member/api/MemberController.java | 8 +- .../kkeujeokbackend/member/domain/Member.java | 4 + .../TeamDashboardsAndChallengesResDto.java | 7 +- .../mypage/application/MyPageService.java | 24 +- .../api/NotificationController.java | 15 +- .../dto/response/NotificationListResDto.java | 8 +- .../application/NotificationService.java | 21 +- .../NotificationCustomRepository.java | 6 - .../NotificationCustomRepositoryImpl.java | 41 -- .../repository/NotificationRepository.java | 2 +- .../api/ChallengeControllerTest.java | 407 ++++++++++-------- .../application/ChallengeServiceTest.java | 109 ++--- .../util/ChallengeBlockStatusUtilTest.java | 12 +- .../challenge/domain/ChallengeTest.java | 27 +- .../common/annotation/ControllerTest.java | 12 +- .../application/TeamDashboardServiceTest.java | 29 ++ .../api/DocumentControllerTest.java | 212 --------- .../teamdocument/api/FileControllerTest.java | 268 ------------ .../application/DocumentServiceTest.java | 180 -------- .../application/FileServiceTest.java | 210 --------- .../teamdocument/domain/DocumentTest.java | 60 --- .../teamdocument/domain/FileTest.java | 65 --- .../repository/DocumentRepositoryTest.java | 92 ---- .../domain/repository/FileRepositoryTest.java | 96 ----- .../member/api/MemberControllerTest.java | 26 +- .../mypage/application/MyPageServiceTest.java | 10 +- .../api/NotificationControllerTest.java | 43 +- .../application/NotificationServiceTest.java | 27 +- 81 files changed, 1201 insertions(+), 2401 deletions(-) create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeCompletedMemberInfoResDto.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeMemberMapping.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepository.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepositoryImpl.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingRepository.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/api/dto/response/PersonalDashboardPageListResDto.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/AlreadyJoinedTeamException.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/NotMemberOfTeamException.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentController.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileController.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentInfoReqDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentUpdateReqDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/FileInfoReqDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentInfoResDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentListResDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileInfoResDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileListResDto.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentService.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileService.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/Document.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/File.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepository.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepositoryImpl.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepository.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepository.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepositoryImpl.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepository.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/DocumentNotFoundException.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/FileNotFoundException.java delete mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/TeamDashboardNotFoundException.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/S3Service.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/exception/InvalidImageUploadException.java create mode 100644 src/main/java/shop/kkeujeok/kkeujeokbackend/global/config/S3Config.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentControllerTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileControllerTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentServiceTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileServiceTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/DocumentTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/FileTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepositoryTest.java delete mode 100644 src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepositoryTest.java diff --git a/build.gradle b/build.gradle index fd7bb910..9e53693a 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,9 @@ dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:3.0.1' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:3.0.1' + // s3 + implementation 'software.amazon.awssdk:s3:2.20.140' + } ext { snippetsDir = file('build/generated-snippets') @@ -84,4 +87,4 @@ bootJar { tasks.named('test') { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/block/application/BlockService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/block/application/BlockService.java index c500b0a5..96a9c6bb 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/block/application/BlockService.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/block/application/BlockService.java @@ -13,9 +13,11 @@ import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockListResDto; import shop.kkeujeok.kkeujeokbackend.block.domain.Block; import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; +import shop.kkeujeok.kkeujeokbackend.block.domain.Type; import shop.kkeujeok.kkeujeokbackend.block.domain.repository.BlockRepository; import shop.kkeujeok.kkeujeokbackend.block.exception.BlockNotFoundException; import shop.kkeujeok.kkeujeokbackend.block.exception.InvalidProgressException; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.Challenge; import shop.kkeujeok.kkeujeokbackend.dashboard.domain.Dashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.domain.repository.DashboardRepository; import shop.kkeujeok.kkeujeokbackend.dashboard.exception.DashboardNotFoundException; @@ -74,9 +76,18 @@ public BlockInfoResDto progressUpdate(String email, Long blockId, String progres block.progressUpdate(progress); + updateChallengeCompletedMemberByProgress(block, member, progress); + return BlockInfoResDto.from(block); } + private void updateChallengeCompletedMemberByProgress(Block block, Member member, Progress progress) { + if (block.getType() == Type.CHALLENGE) { + Challenge challenge = block.getChallenge(); + challenge.updateCompletedMember(member, progress); + } + } + // 블록 리스트 public BlockListResDto findForBlockByProgress(String email, Long dashboardId, String progress, Pageable pageable) { Member member = memberRepository.findByEmail(email).orElseThrow(MemberNotFoundException::new); @@ -158,5 +169,4 @@ private Progress parseProgress(String progressString) { throw new InvalidProgressException(); } } - } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeController.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeController.java index 62448dbc..594acb34 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeController.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeController.java @@ -9,11 +9,11 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockInfoResDto; +import org.springframework.web.multipart.MultipartFile; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.reqeust.ChallengeSaveReqDto; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.reqeust.ChallengeSearchReqDto; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeInfoResDto; @@ -31,16 +31,19 @@ public class ChallengeController { @PostMapping public RspTemplate save(@CurrentUserEmail String email, - @Valid @RequestBody ChallengeSaveReqDto challengeSaveReqDto) { - return new RspTemplate<>(HttpStatus.CREATED, "챌린지 생성 성공", challengeService.save(email, challengeSaveReqDto)); + @Valid @RequestPart ChallengeSaveReqDto challengeSaveReqDto, + @RequestPart(value = "representImage", required = false) MultipartFile representImage) { + return new RspTemplate<>(HttpStatus.CREATED, "챌린지 생성 성공", + challengeService.save(email, challengeSaveReqDto, representImage)); } @PatchMapping("/{challengeId}") public RspTemplate update(@CurrentUserEmail String email, @PathVariable(name = "challengeId") Long challengeId, - @Valid @RequestBody ChallengeSaveReqDto challengeSaveReqDto) { + @Valid @RequestPart ChallengeSaveReqDto challengeSaveReqDto, + @RequestPart(value = "representImage", required = false) MultipartFile representImage) { return new RspTemplate<>(HttpStatus.OK, "챌린지 수정 성공", - challengeService.update(email, challengeId, challengeSaveReqDto)); + challengeService.update(email, challengeId, challengeSaveReqDto, representImage)); } @GetMapping @@ -51,43 +54,48 @@ public RspTemplate findAllChallenges(@RequestParam(defaultV challengeService.findAllChallenges(PageRequest.of(page, size))); } - @GetMapping("/search") - public RspTemplate findChallengesByKeyWord(@RequestParam(name = "keyword") String keyWord, - @RequestParam(defaultValue = "0", name = "page") int page, - @RequestParam(defaultValue = "10", name = "size") int size) { - ChallengeSearchReqDto searchReqDto = ChallengeSearchReqDto.from(keyWord); - return new RspTemplate<>(HttpStatus.OK, - "챌린지 검색 성공", - challengeService.findChallengesByKeyWord(searchReqDto, PageRequest.of(page, size))); - } - - @GetMapping("/find") - public RspTemplate findByCategory(@RequestParam(name = "category") String category, - @RequestParam(defaultValue = "0", name = "page") int page, - @RequestParam(defaultValue = "10", name = "size") int size) { - return new RspTemplate<>(HttpStatus.OK, - "챌린지 카테고리 검색 성공", - challengeService.findByCategory(category, PageRequest.of(page, size))); - } - @GetMapping("/{challengeId}") - public RspTemplate findById(@PathVariable(name = "challengeId") Long challengeId) { - return new RspTemplate<>(HttpStatus.OK, "챌린지 상세보기", challengeService.findById(challengeId)); + public RspTemplate findById(@CurrentUserEmail String email, + @PathVariable(name = "challengeId") Long challengeId) { + return new RspTemplate<>(HttpStatus.OK, "챌린지 상세보기", challengeService.findById(email, challengeId)); } @DeleteMapping("/{challengeId}") public RspTemplate delete(@CurrentUserEmail String email, @PathVariable(name = "challengeId") Long challengeId) { challengeService.delete(email, challengeId); + return new RspTemplate<>(HttpStatus.OK, "챌린지 삭제 성공"); } @PostMapping("/{challengeId}/{dashboardId}") - public RspTemplate addChallengeToPersonalDashboard(@CurrentUserEmail String email, - @PathVariable(name = "challengeId") Long challengeId, - @PathVariable(name = "dashboardId") Long personalDashboardId) { - return new RspTemplate<>(HttpStatus.OK, - "챌린지 참여 성공", - challengeService.addChallengeToPersonalDashboard(email, challengeId, personalDashboardId)); + public RspTemplate addChallengeToPersonalDashboard(@CurrentUserEmail String email, + @PathVariable(name = "challengeId") Long challengeId, + @PathVariable(name = "dashboardId") Long personalDashboardId) { + challengeService.addChallengeToPersonalDashboard(email, challengeId, personalDashboardId); + + return new RspTemplate<>(HttpStatus.OK, "챌린지 참여 성공"); } + + @GetMapping("/search") + public RspTemplate findChallengesByCategoryAndKeyword( + @RequestParam(required = false) String category, + @RequestParam(required = false) String keyword, + @RequestParam(defaultValue = "0", name = "page") int page, + @RequestParam(defaultValue = "10", name = "size") int size) { + + ChallengeSearchReqDto challengeSearchReqDto = ChallengeSearchReqDto.from(keyword, category); + + return new RspTemplate<>(HttpStatus.OK, "챌린지 검색 성공", + challengeService.findChallengesByCategoryAndKeyword(challengeSearchReqDto, + PageRequest.of(page, size))); + } + + @DeleteMapping("/{challengeId}/withdraw") + public RspTemplate withdrawFromChallenge(@CurrentUserEmail String email, + @PathVariable(name = "challengeId") Long challengeId) { + challengeService.withdrawFromChallenge(email, challengeId); + return new RspTemplate<>(HttpStatus.OK, "챌린지 탈퇴 성공"); + } + } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSaveReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSaveReqDto.java index d2c5630b..b55a42cf 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSaveReqDto.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSaveReqDto.java @@ -2,14 +2,11 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -import java.util.HashSet; import java.util.List; -import java.util.Set; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Category; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Challenge; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Cycle; import shop.kkeujeok.kkeujeokbackend.challenge.domain.CycleDetail; -import shop.kkeujeok.kkeujeokbackend.challenge.exception.InvalidCycleException; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -29,16 +26,13 @@ public record ChallengeSaveReqDto( @NotNull(message = "주기 상세정보는 필수 입력값입니다.") List cycleDetails, - @NotNull(message = "시작 날짜는 필수 입력값입니다.") - LocalDate startDate, - @NotNull(message = "종료 날짜는 필수 입력값입니다.") LocalDate endDate, - String representImage + @NotNull(message = "블록 이름은 필수 입력값입니다.") + String blockName ) { - public Challenge toEntity(Member member) { - validateCycleDetails(); + public Challenge toEntity(Member member, String representImage) { return Challenge.builder() .status(Status.ACTIVE) .title(title) @@ -46,31 +40,11 @@ public Challenge toEntity(Member member) { .category(category) .cycle(cycle) .cycleDetails(cycleDetails) - .startDate(startDate) + .startDate(LocalDate.now()) .endDate(endDate) .representImage(representImage) .member(member) + .blockName(blockName) .build(); } - - private void validateCycleDetails() { - Set distinctCycleDetails = new HashSet<>(); - - cycleDetails.forEach(cycleDetail -> { - validateCycleDetailUniqueness(distinctCycleDetails, cycleDetail); - validateCycleDetailMatch(cycleDetail); - }); - } - - private void validateCycleDetailUniqueness(Set seenDetails, CycleDetail cycleDetail) { - if (!seenDetails.add(cycleDetail)) { - throw InvalidCycleException.forDuplicateDetail(); - } - } - - private void validateCycleDetailMatch(CycleDetail cycleDetail) { - if (cycleDetail.getCycle() != cycle) { - throw InvalidCycleException.forMismatchDetail(); - } - } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSearchReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSearchReqDto.java index 9c1657a3..88fe2829 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSearchReqDto.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/reqeust/ChallengeSearchReqDto.java @@ -4,11 +4,13 @@ @Builder public record ChallengeSearchReqDto( - String keyWord + String keyWord, + String category ) { - public static ChallengeSearchReqDto from(String keyWord) { + public static ChallengeSearchReqDto from(String keyWord, String category) { return ChallengeSearchReqDto.builder() .keyWord(keyWord) + .category(category) .build(); } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeCompletedMemberInfoResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeCompletedMemberInfoResDto.java new file mode 100644 index 00000000..5fdd753f --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeCompletedMemberInfoResDto.java @@ -0,0 +1,21 @@ +package shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response; + +import lombok.Builder; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; + +@Builder +public record ChallengeCompletedMemberInfoResDto( + Long memberId, + String email, + String picture, + String nickname +) { + public static ChallengeCompletedMemberInfoResDto from(Member member) { + return ChallengeCompletedMemberInfoResDto.builder() + .memberId(member.getId()) + .email(member.getEmail()) + .picture(member.getPicture()) + .nickname(member.getNickname()) + .build(); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeInfoResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeInfoResDto.java index f9e7ed49..92b69308 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeInfoResDto.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/api/dto/response/ChallengeInfoResDto.java @@ -1,7 +1,9 @@ package shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response; import java.time.LocalDate; +import java.util.Collections; import java.util.List; +import java.util.Set; import lombok.Builder; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Category; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Challenge; @@ -20,7 +22,12 @@ public record ChallengeInfoResDto( LocalDate endDate, String representImage, String authorName, - String authorProfileImage + String authorProfileImage, + String blockName, + int participantCount, + boolean isParticipant, + boolean isAuthor, + Set completedMembers ) { public static ChallengeInfoResDto from(Challenge challenge) { return ChallengeInfoResDto.builder() @@ -35,6 +42,33 @@ public static ChallengeInfoResDto from(Challenge challenge) { .representImage(challenge.getRepresentImage()) .authorName(challenge.getMember().getNickname()) .authorProfileImage(challenge.getMember().getPicture()) + .blockName(challenge.getBlockName()) + .participantCount(challenge.getParticipantsCount()) + .isAuthor(true) + .isParticipant(false) + .completedMembers(Collections.emptySet()) + .build(); + } + + public static ChallengeInfoResDto of(Challenge challenge, boolean isParticipant, boolean isAuthor, + Set completedMembers) { + return ChallengeInfoResDto.builder() + .challengeId(challenge.getId()) + .title(challenge.getTitle()) + .contents(challenge.getContents()) + .category(challenge.getCategory()) + .cycle(challenge.getCycle()) + .cycleDetails(challenge.getCycleDetails()) + .startDate(challenge.getStartDate()) + .endDate(challenge.getEndDate()) + .representImage(challenge.getRepresentImage()) + .authorName(challenge.getMember().getNickname()) + .authorProfileImage(challenge.getMember().getPicture()) + .blockName(challenge.getBlockName()) + .participantCount(challenge.getParticipantsCount()) + .isParticipant(isParticipant) + .isAuthor(isAuthor) + .completedMembers(completedMembers) .build(); } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeService.java index 928eea11..38e8fd1f 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeService.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeService.java @@ -3,31 +3,34 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; 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; -import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockInfoResDto; +import org.springframework.web.multipart.MultipartFile; import shop.kkeujeok.kkeujeokbackend.block.domain.Block; import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; import shop.kkeujeok.kkeujeokbackend.block.domain.Type; import shop.kkeujeok.kkeujeokbackend.block.domain.repository.BlockRepository; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.reqeust.ChallengeSaveReqDto; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.reqeust.ChallengeSearchReqDto; +import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeCompletedMemberInfoResDto; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeInfoResDto; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeListResDto; -import shop.kkeujeok.kkeujeokbackend.challenge.application.util.ChallengeBlockStatusUtil; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Challenge; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; import shop.kkeujeok.kkeujeokbackend.challenge.domain.repository.ChallengeRepository; import shop.kkeujeok.kkeujeokbackend.challenge.exception.ChallengeAccessDeniedException; import shop.kkeujeok.kkeujeokbackend.challenge.exception.ChallengeNotFoundException; import shop.kkeujeok.kkeujeokbackend.dashboard.domain.Dashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.exception.DashboardNotFoundException; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.repository.PersonalDashboardRepository; +import shop.kkeujeok.kkeujeokbackend.global.aws.S3Service; import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; import shop.kkeujeok.kkeujeokbackend.member.exception.MemberNotFoundException; @@ -38,36 +41,49 @@ public class ChallengeService { private static final String CHALLENGE_JOIN_MESSAGE = "%s님이 챌린지에 참여했습니다"; - + private static final String DEADLINE_DATE_FORMAT = "yyyy.MM.dd 23:59"; private final ChallengeRepository challengeRepository; private final MemberRepository memberRepository; private final PersonalDashboardRepository personalDashboardRepository; private final BlockRepository blockRepository; private final NotificationService notificationService; + private final S3Service s3Service; @Transactional - public ChallengeInfoResDto save(String email, ChallengeSaveReqDto challengeSaveReqDto) { + public ChallengeInfoResDto save(String email, ChallengeSaveReqDto challengeSaveReqDto, + MultipartFile representImage) { Member member = findMemberByEmail(email); - Challenge challenge = challengeSaveReqDto.toEntity(member); + String imageUrl = null; + if (representImage != null && !representImage.isEmpty()) { + imageUrl = s3Service.uploadChallengeImage(representImage); + } + + Challenge challenge = challengeSaveReqDto.toEntity(member, imageUrl); challengeRepository.save(challenge); return ChallengeInfoResDto.from(challenge); } @Transactional - public ChallengeInfoResDto update(String email, Long challengeId, ChallengeSaveReqDto challengeSaveReqDto) { + public ChallengeInfoResDto update(String email, Long challengeId, ChallengeSaveReqDto challengeSaveReqDto, + MultipartFile representImage) { Member member = findMemberByEmail(email); Challenge challenge = findChallengeById(challengeId); verifyMemberIsAuthor(challenge, member); + String imageUrl = challenge.getRepresentImage(); + if (representImage != null && !representImage.isEmpty()) { + imageUrl = s3Service.uploadChallengeImage(representImage); + } + challenge.update(challengeSaveReqDto.title(), challengeSaveReqDto.contents(), challengeSaveReqDto.cycleDetails(), - challengeSaveReqDto.startDate(), challengeSaveReqDto.endDate(), - challengeSaveReqDto.representImage()); + challengeSaveReqDto.blockName(), + imageUrl); return ChallengeInfoResDto.from(challenge); } @@ -84,33 +100,28 @@ public ChallengeListResDto findAllChallenges(Pageable pageable) { } @Transactional(readOnly = true) - public ChallengeListResDto findChallengesByKeyWord(ChallengeSearchReqDto challengeSearchReqDto, - Pageable pageable) { - Page challenges = challengeRepository.findChallengesByKeyWord(challengeSearchReqDto, pageable); - - List challengeInfoResDtoList = challenges.stream() - .map(ChallengeInfoResDto::from) - .toList(); + public ChallengeInfoResDto findById(String email, Long challengeId) { + Member member = findMemberByEmail(email); + Challenge challenge = findChallengeById(challengeId); - return ChallengeListResDto.of(challengeInfoResDtoList, PageInfoResDto.from(challenges)); - } + boolean isParticipant = checkIfParticipant(challenge, member); + boolean isAuthor = member.equals(challenge.getMember()); - @Transactional(readOnly = true) - public ChallengeListResDto findByCategory(String category, Pageable pageable) { - Page challenges = challengeRepository.findChallengesByCategory(category, pageable); + Set completedMembers = getCompletedMembers(challenge); - List challengeInfoResDtoList = challenges.stream() - .map(ChallengeInfoResDto::from) - .toList(); - - return ChallengeListResDto.of(challengeInfoResDtoList, PageInfoResDto.from(challenges)); + return ChallengeInfoResDto.of(challenge, isParticipant, isAuthor, completedMembers); } - @Transactional(readOnly = true) - public ChallengeInfoResDto findById(Long challengeId) { - Challenge challenge = findChallengeById(challengeId); + private boolean checkIfParticipant(Challenge challenge, Member member) { + return challenge.getParticipants().stream() + .anyMatch(mapping -> mapping.getMember().equals(member)); + } - return ChallengeInfoResDto.from(challenge); + private Set getCompletedMembers(Challenge challenge) { + return challenge.getParticipants().stream() + .filter(ChallengeMemberMapping::isCompleted) + .map(mapping -> ChallengeCompletedMemberInfoResDto.from(mapping.getMember())) + .collect(Collectors.toSet()); } @Transactional @@ -123,21 +134,40 @@ public void delete(String email, Long challengeId) { } @Transactional - public BlockInfoResDto addChallengeToPersonalDashboard(String email, Long personalDashboardId, Long challengeId) { + public void addChallengeToPersonalDashboard(String email, Long challengeId, Long personalDashboardId) { Member member = findMemberByEmail(email); Challenge challenge = findChallengeById(challengeId); - Dashboard personalDashboard = personalDashboardRepository.findById(personalDashboardId) + PersonalDashboard personalDashboard = personalDashboardRepository.findById(personalDashboardId) .orElseThrow(DashboardNotFoundException::new); - Block block = createBlock(challenge, member, personalDashboard); - updateBlockStatusIfNotActive(block, challenge); + challenge.addParticipant(member, personalDashboard); + createBlockIfActiveToday(challenge, member, personalDashboard); - blockRepository.save(block); + challengeRepository.save(challenge); String message = String.format(CHALLENGE_JOIN_MESSAGE, member.getName()); notificationService.sendNotification(challenge.getMember(), message); + } - return BlockInfoResDto.from(block); + private void createBlockIfActiveToday(Challenge challenge, Member member, Dashboard personalDashboard) { + if (challenge.isActiveToday()) { + Block block = createBlock(challenge, member, personalDashboard); + blockRepository.save(block); + } + } + + private Block createBlock(Challenge challenge, Member member, Dashboard personalDashboard) { + return Block.builder() + .title(challenge.getTitle()) + .contents(challenge.getContents()) + .progress(Progress.NOT_STARTED) + .type(Type.CHALLENGE) + .deadLine(LocalDate.now() + .format(DateTimeFormatter.ofPattern(DEADLINE_DATE_FORMAT))) + .member(member) + .dashboard(personalDashboard) + .challenge(challenge) + .build(); } @Transactional(readOnly = true) @@ -153,26 +183,34 @@ public ChallengeListResDto findChallengeForMemberId(String email, Pageable pagea return ChallengeListResDto.of(challengeInfoResDtoList, PageInfoResDto.from(challenges)); } - private Block createBlock(Challenge challenge, Member member, Dashboard personalDashboard) { - return Block.builder() - .title(challenge.getTitle()) - .contents(challenge.getContents()) - .progress(Progress.NOT_STARTED) - .type(Type.CHALLENGE) - .deadLine(LocalDate.now() - .format(DateTimeFormatter.ofPattern("yyyy.MM.dd 23:59"))) - .member(member) - .dashboard(personalDashboard) - .challenge(challenge) - .build(); + @Transactional(readOnly = true) + public ChallengeListResDto findChallengesByCategoryAndKeyword(ChallengeSearchReqDto challengeSearchReqDto, + Pageable pageable) { + Page challenges = challengeRepository.findChallengesByCategoryAndKeyword(challengeSearchReqDto, + pageable); + + List challengeInfoResDtoList = challenges.stream() + .map(ChallengeInfoResDto::from) + .toList(); + + return ChallengeListResDto.of(challengeInfoResDtoList, PageInfoResDto.from(challenges)); } - private void updateBlockStatusIfNotActive(Block block, Challenge challenge) { - if (!ChallengeBlockStatusUtil.isChallengeBlockActiveToday(challenge.getCycle(), challenge.getCycleDetails())) { - block.updateChallengeStatus(Status.UN_ACTIVE); - } + @Transactional + public void withdrawFromChallenge(String email, Long challengeId) { + Member member = findMemberByEmail(email); + Challenge challenge = findChallengeById(challengeId); + + ChallengeMemberMapping mapping = challenge.getParticipants().stream() + .filter(participant -> participant.getMember().equals(member)) + .findFirst() + .orElseThrow(() -> new ChallengeAccessDeniedException("이 챌린지에 참여하지 않았습니다.")); + + challenge.removeParticipant(mapping); + challengeRepository.save(challenge); } + private Member findMemberByEmail(String email) { return memberRepository.findByEmail(email) .orElseThrow(MemberNotFoundException::new); @@ -183,7 +221,6 @@ private Challenge findChallengeById(Long challengeId) { .orElseThrow(ChallengeNotFoundException::new); } - private void verifyMemberIsAuthor(Challenge challenge, Member member) { if (!challenge.getMember().equals(member)) { throw new ChallengeAccessDeniedException(); diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUpdateScheduler.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUpdateScheduler.java index 4a5ff792..8c46b992 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUpdateScheduler.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUpdateScheduler.java @@ -1,14 +1,20 @@ package shop.kkeujeok.kkeujeokbackend.challenge.application.util; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import shop.kkeujeok.kkeujeokbackend.block.domain.Block; +import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; import shop.kkeujeok.kkeujeokbackend.block.domain.Type; import shop.kkeujeok.kkeujeokbackend.block.domain.repository.BlockRepository; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.Challenge; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.repository.challengeMemberMapping.ChallengeMemberMappingRepository; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.notification.application.NotificationService; @@ -17,35 +23,52 @@ public class ChallengeBlockStatusUpdateScheduler { private static final String DAILY_CRON_EXPRESSION = "0 0 0 * * ?"; - private static final String CHALLENGE_CREATED_MESSAGE_TEMPLATE = "%s 챌린지가 생성되었습니다"; + private static final String CHALLENGE_CREATED_MESSAGE_TEMPLATE = "%s 챌린지 블록이 생성되었습니다."; + private static final String DEADLINE_DATE_FORMAT = "yyyy.MM.dd 23:59"; + private static final int DEADLINE_EXTENSION_DAYS = 1; + private final ChallengeMemberMappingRepository challengeMemberMappingRepository; private final BlockRepository blockRepository; private final NotificationService notificationService; - @Scheduled(cron = DAILY_CRON_EXPRESSION) @Transactional - public void updateStatuses() { - List blocks = blockRepository.findByType(Type.CHALLENGE); - - blocks.forEach(block -> { - Status previousStatus = block.getStatus(); - Status newStatus; - - if (!ChallengeBlockStatusUtil.isChallengeBlockActiveToday(block.getChallenge().getCycle(), - block.getChallenge().getCycleDetails())) { - newStatus = Status.UN_ACTIVE; - } else { - newStatus = Status.ACTIVE; + @Scheduled(cron = DAILY_CRON_EXPRESSION) + public void createNewChallengeBlocks() { + List activeMappings = challengeMemberMappingRepository.findActiveMappings(); + + for (ChallengeMemberMapping mapping : activeMappings) { + if (isNewBlockNeeded(mapping)) { + createNewBlock(mapping); } + } + } + + private boolean isNewBlockNeeded(ChallengeMemberMapping mapping) { + return mapping.getChallenge().isActiveToday(); + } - if (newStatus == Status.ACTIVE && previousStatus != Status.ACTIVE) { - block.updateChallengeStatus(newStatus); + private void createNewBlock(ChallengeMemberMapping mapping) { + PersonalDashboard dashboard = mapping.getPersonalDashboard(); - Member member = block.getMember(); - String message = String.format(CHALLENGE_CREATED_MESSAGE_TEMPLATE, block.getChallenge().getTitle()); + Block newBlock = Block.builder() + .title(mapping.getChallenge().getTitle()) + .contents(mapping.getChallenge().getContents()) + .progress(Progress.NOT_STARTED) + .type(Type.CHALLENGE) + .deadLine(LocalDate.now().plusDays(DEADLINE_EXTENSION_DAYS) + .format(DateTimeFormatter.ofPattern(DEADLINE_DATE_FORMAT))) + .member(mapping.getMember()) + .dashboard(dashboard) + .challenge(mapping.getChallenge()) + .build(); - notificationService.sendNotification(member, message); - } - }); + blockRepository.save(newBlock); + + sendChallengeCreatedNotification(mapping.getMember(), mapping.getChallenge()); + } + + private void sendChallengeCreatedNotification(Member member, Challenge challenge) { + String message = String.format(CHALLENGE_CREATED_MESSAGE_TEMPLATE, challenge.getTitle()); + notificationService.sendNotification(member, message); } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtil.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtil.java index 8f6b8374..c5d912b2 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtil.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtil.java @@ -2,11 +2,19 @@ import java.time.LocalDate; import java.util.List; +import lombok.Getter; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Cycle; import shop.kkeujeok.kkeujeokbackend.challenge.domain.CycleDetail; public class ChallengeBlockStatusUtil { - public static Boolean isChallengeBlockActiveToday(Cycle cycle, List cycleDetails) { + + @Getter + private static final ChallengeBlockStatusUtil instance = new ChallengeBlockStatusUtil(); + + private ChallengeBlockStatusUtil() { + } + + public Boolean isChallengeBlockActiveToday(Cycle cycle, List cycleDetails) { LocalDate today = LocalDate.now(); int dayOfWeekNumber = today.getDayOfWeek().getValue(); diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/Challenge.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/Challenge.java index cad6b03f..306d866d 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/Challenge.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/Challenge.java @@ -1,5 +1,6 @@ package shop.kkeujeok.kkeujeokbackend.challenge.domain; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; @@ -7,16 +8,24 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import java.time.LocalDate; +import java.util.HashSet; import java.util.List; +import java.util.Set; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; +import shop.kkeujeok.kkeujeokbackend.challenge.application.util.ChallengeBlockStatusUtil; import shop.kkeujeok.kkeujeokbackend.challenge.converter.CycleDetailsConverter; +import shop.kkeujeok.kkeujeokbackend.challenge.exception.InvalidCycleException; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.global.entity.BaseEntity; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; +import shop.kkeujeok.kkeujeokbackend.member.exception.MemberNotFoundException; @Entity @Getter @@ -36,8 +45,8 @@ public class Challenge extends BaseEntity { private Cycle cycle; - @Convert(converter = CycleDetailsConverter.class) @Column(name = "cycle_details") + @Convert(converter = CycleDetailsConverter.class) private List cycleDetails; @Column(name = "start_date") @@ -46,13 +55,18 @@ public class Challenge extends BaseEntity { @Column(name = "end_date") private LocalDate endDate; - @Column(name = "represent_image") + @Column(name = "represent_image", columnDefinition = "TEXT") private String representImage; @ManyToOne @JoinColumn(name = "member_id") private Member member; + private String blockName; + + @OneToMany(mappedBy = "challenge", cascade = CascadeType.ALL, orphanRemoval = true) + private Set participants = new HashSet<>(); + @Builder private Challenge(Status status, String title, @@ -63,7 +77,10 @@ private Challenge(Status status, LocalDate startDate, LocalDate endDate, String representImage, - Member member) { + Member member, + String blockName) { + validateCycleDetails(cycle, cycleDetails); + this.status = status; this.title = title; this.contents = contents; @@ -74,32 +91,88 @@ private Challenge(Status status, this.endDate = endDate; this.representImage = representImage; this.member = member; + this.blockName = blockName; } public void update(String updateTitle, String updateContents, List updateCycleDetails, - LocalDate updateStartDate, LocalDate updateEndDate, String updateRepresentImage) { - if (hasChanges(updateTitle, updateContents, updateCycleDetails, updateStartDate, updateEndDate, - updateRepresentImage)) { + LocalDate updateEndDate, String updateBlockName, String updateRepresentImage) { + validateCycleDetails(cycle, cycleDetails); + + if (hasChanges(updateTitle, updateContents, updateCycleDetails, updateEndDate, + updateBlockName)) { this.title = updateTitle; this.contents = updateContents; this.cycleDetails = updateCycleDetails; - this.startDate = updateStartDate; this.endDate = updateEndDate; - this.representImage = updateRepresentImage; + this.blockName = updateBlockName; + } + this.representImage = updateRepresentImage; + } + + private void validateCycleDetails(Cycle cycle, List cycleDetails) { + Set distinctCycleDetails = new HashSet<>(); + + cycleDetails.forEach(cycleDetail -> { + validateCycleDetailUniqueness(distinctCycleDetails, cycleDetail); + validateCycleDetailMatch(cycle, cycleDetail); + }); + } + + private void validateCycleDetailUniqueness(Set seenDetails, CycleDetail cycleDetail) { + if (!seenDetails.add(cycleDetail)) { + throw InvalidCycleException.forDuplicateDetail(); + } + } + + private void validateCycleDetailMatch(Cycle cycle, CycleDetail cycleDetail) { + if (cycleDetail.getCycle() != cycle) { + throw InvalidCycleException.forMismatchDetail(); } } private boolean hasChanges(String updateTitle, String updateContents, List updateCycleDetails, - LocalDate updateStartDate, LocalDate updateEndDate, String updateRepresentImage) { + LocalDate updateEndDate, String blockName) { return !this.title.equals(updateTitle) || !this.contents.equals(updateContents) || !this.cycleDetails.equals(updateCycleDetails) || - !this.startDate.equals(updateStartDate) || !this.endDate.equals(updateEndDate) || - !this.representImage.equals(updateRepresentImage); + !this.blockName.equals(blockName); } public void updateStatus() { this.status = (this.status == Status.ACTIVE) ? Status.DELETED : Status.ACTIVE; } + + public int getParticipantsCount() { + return participants.size(); + } + + public void addParticipant(Member member, PersonalDashboard dashboard) { + ChallengeMemberMapping mapping = ChallengeMemberMapping.of(this, member, dashboard); + this.participants.add(mapping); + } + + public void updateCompletedMember(Member member, Progress progress) { + ChallengeMemberMapping mapping = findParticipantByMember(member); + + mapping.updateIsCompleted(progress == Progress.COMPLETED); + + } + + private ChallengeMemberMapping findParticipantByMember(Member member) { + return this.participants.stream() + .filter(p -> p.getMember().equals(member)) + .findFirst() + .orElseThrow(MemberNotFoundException::new); + } + + public boolean isActiveToday() { + return ChallengeBlockStatusUtil.getInstance() + .isChallengeBlockActiveToday(this.getCycle(), this.getCycleDetails()); + } + + public void removeParticipant(ChallengeMemberMapping mapping) { + participants.remove(mapping); + } + } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeMemberMapping.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeMemberMapping.java new file mode 100644 index 00000000..4485e947 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeMemberMapping.java @@ -0,0 +1,58 @@ +package shop.kkeujeok.kkeujeokbackend.challenge.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; +import shop.kkeujeok.kkeujeokbackend.global.entity.BaseEntity; +import shop.kkeujeok.kkeujeokbackend.member.domain.Member; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ChallengeMemberMapping extends BaseEntity { + + @ManyToOne + @JoinColumn(name = "challenge_id") + private Challenge challenge; + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne + @JoinColumn(name = "personal_dashboard_id") + private PersonalDashboard personalDashboard; + + @JoinColumn(name = "is_completed") + private boolean isCompleted; + + @Builder + public ChallengeMemberMapping(Challenge challenge, Member member, PersonalDashboard personalDashboard, + boolean isCompleted) { + this.challenge = challenge; + this.member = member; + this.personalDashboard = personalDashboard; + this.isCompleted = isCompleted; + } + + public static ChallengeMemberMapping of(Challenge challenge, Member member, PersonalDashboard dashboard) { + return ChallengeMemberMapping.builder() + .challenge(challenge) + .member(member) + .personalDashboard(dashboard) + .isCompleted(false) + .build(); + } + + public void updateIsCompleted(boolean completed) { + if (this.isCompleted == completed) { + return; + } + this.isCompleted = completed; + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepository.java index 3fe3e11e..7d93adca 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepository.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepository.java @@ -9,9 +9,7 @@ public interface ChallengeCustomRepository { Page findAllChallenges(Pageable pageable); - Page findChallengesByKeyWord(ChallengeSearchReqDto challengeSearchReqDto, Pageable pageable); - Page findChallengesByEmail(Member member, Pageable pageable); - Page findChallengesByCategory(String category, Pageable pageable); + Page findChallengesByCategoryAndKeyword(ChallengeSearchReqDto challengeSearchReqDto, Pageable pageable); } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepositoryImpl.java index ff6281fd..7b31e8f4 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepositoryImpl.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/ChallengeCustomRepositoryImpl.java @@ -2,6 +2,7 @@ import static shop.kkeujeok.kkeujeokbackend.challenge.domain.QChallenge.challenge; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import java.util.Optional; @@ -44,28 +45,6 @@ public Page findAllChallenges(Pageable pageable) { return new PageImpl<>(challenges, pageable, total); } - @Override - public Page findChallengesByKeyWord(ChallengeSearchReqDto challengeSearchReqDto, Pageable pageable) { - long total = Optional.ofNullable( - queryFactory - .select(challenge.count()) - .from(challenge) - .where(challenge.status.eq(Status.ACTIVE), - challenge.title.containsIgnoreCase(challengeSearchReqDto.keyWord())) - .fetchOne() - ).orElse(0L); - - List challenges = queryFactory - .selectFrom(challenge) - .where(challenge.status.eq(Status.ACTIVE), - challenge.title.containsIgnoreCase(challengeSearchReqDto.keyWord())) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - return new PageImpl<>(challenges, pageable, total); - } - @Override public Page findChallengesByEmail(Member member, Pageable pageable) { long total = queryFactory @@ -85,23 +64,37 @@ public Page findChallengesByEmail(Member member, Pageable pageable) { return new PageImpl<>(challenges, pageable, total); } - public Page findChallengesByCategory(String category, Pageable pageable) { + @Override + public Page findChallengesByCategoryAndKeyword(ChallengeSearchReqDto challengeSearchReqDto, + Pageable pageable) { + String keyword = challengeSearchReqDto.keyWord(); + String category = challengeSearchReqDto.category(); + + BooleanExpression predicate = challenge.status.eq(Status.ACTIVE); + + if (keyword != null && !keyword.isEmpty()) { + predicate = predicate.and(challenge.title.containsIgnoreCase(keyword)); + } + + if (category != null && !category.isEmpty()) { + predicate = predicate.and(challenge.category.eq(Category.valueOf(category))); + } + long total = Optional.ofNullable( queryFactory .select(challenge.count()) .from(challenge) - .where(challenge.status.eq(Status.ACTIVE), - challenge.category.eq(Category.valueOf(category))) + .where(predicate) .fetchOne() ).orElse(0L); List challenges = queryFactory .selectFrom(challenge) - .where(challenge.status.eq(Status.ACTIVE), - challenge.category.eq(Category.valueOf(category))) + .where(predicate) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); + return new PageImpl<>(challenges, pageable, total); } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepository.java new file mode 100644 index 00000000..55151819 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepository.java @@ -0,0 +1,8 @@ +package shop.kkeujeok.kkeujeokbackend.challenge.domain.repository.challengeMemberMapping; + +import java.util.List; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; + +public interface ChallengeMemberMappingCustomRepository { + List findActiveMappings(); +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepositoryImpl.java new file mode 100644 index 00000000..6fde7489 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingCustomRepositoryImpl.java @@ -0,0 +1,27 @@ +package shop.kkeujeok.kkeujeokbackend.challenge.domain.repository.challengeMemberMapping; + +import static shop.kkeujeok.kkeujeokbackend.challenge.domain.QChallengeMemberMapping.challengeMemberMapping; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; +import shop.kkeujeok.kkeujeokbackend.global.entity.Status; + +@Repository +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ChallengeMemberMappingCustomRepositoryImpl implements ChallengeMemberMappingCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findActiveMappings() { + return queryFactory + .selectFrom(challengeMemberMapping) + .where(challengeMemberMapping.challenge.status.eq(Status.ACTIVE)) + .fetch(); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingRepository.java new file mode 100644 index 00000000..f1cda116 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/repository/challengeMemberMapping/ChallengeMemberMappingRepository.java @@ -0,0 +1,8 @@ +package shop.kkeujeok.kkeujeokbackend.challenge.domain.repository.challengeMemberMapping; + +import org.springframework.data.jpa.repository.JpaRepository; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; + +public interface ChallengeMemberMappingRepository extends JpaRepository, + ChallengeMemberMappingCustomRepository { +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepository.java index 4329afe8..5f3574f2 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepository.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepository.java @@ -6,12 +6,15 @@ import org.springframework.data.domain.Pageable; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboardMemberMapping; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; public interface DashboardCustomRepository { List findForPersonalDashboard(Member member); + Page findForPersonalDashboard(Member member, Pageable pageable); + Set findCategoriesForDashboard(Member member); Page findForTeamDashboard(Member member, Pageable pageable); @@ -22,4 +25,5 @@ public interface DashboardCustomRepository { double calculateCompletionPercentage(Long dashboardId); + Page findPublicPersonalDashboard(Member member, Pageable pageable); } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepositoryImpl.java index 83a9f615..b3b5d693 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepositoryImpl.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/domain/repository/DashboardCustomRepositoryImpl.java @@ -3,6 +3,7 @@ import static shop.kkeujeok.kkeujeokbackend.block.domain.QBlock.block; import static shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.QPersonalDashboard.personalDashboard; import static shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.QTeamDashboard.teamDashboard; +import static shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.QTeamDashboardMemberMapping.teamDashboardMemberMapping; import static shop.kkeujeok.kkeujeokbackend.member.domain.QMember.member; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -18,6 +19,7 @@ import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboardMemberMapping; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -40,6 +42,25 @@ public List findForPersonalDashboard(Member member) { .fetch(); } + @Override + public Page findForPersonalDashboard(Member member, Pageable pageable) { + long total = queryFactory + .selectFrom(personalDashboard) + .where(personalDashboard._super.member.eq(member) + .and(personalDashboard._super.status.eq(Status.ACTIVE))) + .fetchCount(); + + List dashboards = queryFactory + .selectFrom(personalDashboard) + .where(personalDashboard._super.member.eq(member) + .and(personalDashboard._super.status.eq(Status.ACTIVE))) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(dashboards, pageable, total); + } + @Override public Set findCategoriesForDashboard(Member member) { return queryFactory @@ -73,9 +94,14 @@ public Page findForTeamDashboard(Member member, Pageable pageable @Override public List findForTeamDashboard(Member member) { return queryFactory - .selectFrom(teamDashboard) - .where(teamDashboard._super.member.eq(member) - .and(teamDashboard._super.status.eq(Status.ACTIVE))) + .select(teamDashboard) + .from(teamDashboard) + .leftJoin(teamDashboard.teamDashboardMemberMappings, teamDashboardMemberMapping) + .where( + teamDashboardMemberMapping.member.eq(member) + .or(teamDashboard._super.member.eq(member)) + .and(teamDashboard._super.status.eq(Status.ACTIVE)) + ) .fetch(); } @@ -118,4 +144,25 @@ public double calculateCompletionPercentage(Long dashboardId) { return (double) completedBlocks / totalBlocks * 100; } + + @Override + public Page findPublicPersonalDashboard(Member member, Pageable pageable) { + long total = queryFactory + .selectFrom(personalDashboard) + .where(personalDashboard._super.member.eq(member) + .and(personalDashboard._super.status.eq(Status.ACTIVE)) + .and(personalDashboard.isPublic.eq(true))) // isPublic이 true인 경우 + .fetchCount(); + + List dashboards = queryFactory + .selectFrom(personalDashboard) + .where(personalDashboard._super.member.eq(member) + .and(personalDashboard._super.status.eq(Status.ACTIVE)) + .and(personalDashboard.isPublic.eq(true))) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(dashboards, pageable, total); + } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/api/dto/response/PersonalDashboardPageListResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/api/dto/response/PersonalDashboardPageListResDto.java new file mode 100644 index 00000000..3288b7d6 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/api/dto/response/PersonalDashboardPageListResDto.java @@ -0,0 +1,27 @@ +package shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response; + +import lombok.Builder; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardInfoResDto; +import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; + +import java.util.List; + +@Builder +public record PersonalDashboardPageListResDto( + List personalDashboardInfoResDto, + PageInfoResDto pageInfoResDto +) { + public static PersonalDashboardPageListResDto of(List personalDashboards, + PageInfoResDto pageInfoResDto) { + return PersonalDashboardPageListResDto.builder() + .personalDashboardInfoResDto(personalDashboards) + .pageInfoResDto(pageInfoResDto) + .build(); + } + + public static PersonalDashboardPageListResDto from(List personalDashboards) { + return PersonalDashboardPageListResDto.builder() + .personalDashboardInfoResDto(personalDashboards) + .build(); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/application/PersonalDashboardService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/application/PersonalDashboardService.java index a3e643ae..dd463b75 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/application/PersonalDashboardService.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/application/PersonalDashboardService.java @@ -2,7 +2,10 @@ import java.util.List; import java.util.Set; + 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; import shop.kkeujeok.kkeujeokbackend.dashboard.exception.DashboardNotFoundException; @@ -12,9 +15,11 @@ import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardCategoriesResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardInfoResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardListResDto; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardPageListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.repository.PersonalDashboardRepository; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.exception.DashboardAccessDeniedException; +import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; import shop.kkeujeok.kkeujeokbackend.member.exception.MemberNotFoundException; @@ -115,4 +120,32 @@ private void verifyMemberIsAuthor(PersonalDashboard dashboard, Member member) { } } -} + // isPublic이 true인 대시보드 조회 메서드 + public PersonalDashboardPageListResDto findPublicPersonalDashboards(String email, Pageable pageable) { + Member member = memberRepository.findByEmail(email).orElseThrow(MemberNotFoundException::new); + + Page publicDashboards = personalDashboardRepository.findPublicPersonalDashboard(member, pageable); + + List publicDashboardInfoResDtoList = publicDashboards.stream() + .map(dashboard -> PersonalDashboardInfoResDto.of(member, dashboard)) + .toList(); + + return PersonalDashboardPageListResDto.of(publicDashboardInfoResDtoList, PageInfoResDto.from(publicDashboards)); + } + + // 개인 대시보드 전체 조회(페이지네이션 적용) + public PersonalDashboardPageListResDto findForPersonalDashboard(String email, Pageable pageable) { + Member member = memberRepository.findByEmail(email).orElseThrow(MemberNotFoundException::new); + + // Repository를 통해 페이징된 개인 대시보드 조회 + Page personalDashboards = personalDashboardRepository.findForPersonalDashboard(member, pageable); + + // DTO 변환 + List personalDashboardInfoResDtoList = personalDashboards.stream() + .map(p -> PersonalDashboardInfoResDto.of(member, p)) + .toList(); + + // 페이징 정보를 포함한 응답 반환 + return PersonalDashboardPageListResDto.of(personalDashboardInfoResDtoList, PageInfoResDto.from(personalDashboards)); + } +} \ No newline at end of file diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/domain/PersonalDashboard.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/domain/PersonalDashboard.java index a86fcf9b..57a85d3d 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/domain/PersonalDashboard.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/personal/domain/PersonalDashboard.java @@ -1,10 +1,14 @@ package shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain; import jakarta.persistence.Entity; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; import shop.kkeujeok.kkeujeokbackend.dashboard.domain.Dashboard; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -18,6 +22,9 @@ public class PersonalDashboard extends Dashboard { private String category; + @OneToMany(mappedBy = "personalDashboard") + private List challengeMemberMappings = new ArrayList<>(); + @Builder private PersonalDashboard(String title, String description, String dType, Member member, boolean isPublic, String category) { @@ -35,5 +42,4 @@ public void update(String updateTitle, String updateDescription, boolean updateI public void statusUpdate() { super.statusUpdate((super.getStatus() == Status.ACTIVE) ? Status.DELETED : Status.ACTIVE); } - } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardService.java index 3f6dd30b..2c0d27d8 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardService.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardService.java @@ -17,6 +17,8 @@ import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.repository.TeamDashboardRepository; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.exception.AlreadyJoinedTeamException; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.exception.NotMemberOfTeamException; import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; @@ -28,10 +30,9 @@ @Transactional(readOnly = true) public class TeamDashboardService { - private static final String TEAM_DASHBOARD_JOIN_MESSAGE = "%s님이 %s 대시보드에 초대하였습니다."; + private static final String TEAM_DASHBOARD_JOIN_MESSAGE = "%s님이 %s 대시보드에 초대하였습니다.%d"; private static final String TEAM_JOIN_ACCEPT_MESSAGE = "%s님이 초대를 수락하였습니다."; - private final TeamDashboardRepository teamDashboardRepository; private final MemberRepository memberRepository; private final NotificationService notificationService; @@ -132,21 +133,37 @@ public void joinTeam(String email, Long dashboardId) { TeamDashboard dashboard = teamDashboardRepository.findById(dashboardId) .orElseThrow(DashboardNotFoundException::new); + validateMemberNotAlreadyJoined(dashboard, member); + dashboard.addMember(member); String message = String.format(TEAM_JOIN_ACCEPT_MESSAGE, member.getEmail()); notificationService.sendNotification(dashboard.getMember(), message); } + private void validateMemberNotAlreadyJoined(TeamDashboard dashboard, Member member) { + if (dashboard.hasMember(member)) { + throw new AlreadyJoinedTeamException(); + } + } + @Transactional public void leaveTeam(String email, Long dashboardId) { Member member = memberRepository.findByEmail(email).orElseThrow(MemberNotFoundException::new); TeamDashboard dashboard = teamDashboardRepository.findById(dashboardId) .orElseThrow(DashboardNotFoundException::new); + validateMemberInTeam(dashboard, member); + dashboard.removeMember(member); } + private void validateMemberInTeam(TeamDashboard dashboard, Member member) { + if (!dashboard.hasMember(member)) { + throw new NotMemberOfTeamException(); + } + } + public SearchMemberListResDto searchMembers(String query) { List searchMembers = teamDashboardRepository.findForMembersByQuery(query); @@ -162,14 +179,13 @@ private void inviteMember(Member member, TeamDashboard teamDashboard, List teamDashboardMemberMappings = new ArrayList<>(); - @OneToMany(mappedBy = "teamDashboard", cascade = CascadeType.ALL) - private List documents = new ArrayList<>(); + @OneToMany(mappedBy = "teamDashboard", cascade = CascadeType.ALL, orphanRemoval = true) + private List documents = new ArrayList<>(); @Builder private TeamDashboard(String title, String description, String dType, Member member) { @@ -50,4 +49,9 @@ public void removeMember(Member member) { teamDashboardMemberMappings.removeIf(mapping -> mapping.getMember().equals(member)); } + public boolean hasMember(Member member) { + return teamDashboardMemberMappings.stream() + .anyMatch(mapping -> mapping.getMember().equals(member)); + } + } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/AlreadyJoinedTeamException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/AlreadyJoinedTeamException.java new file mode 100644 index 00000000..fe2baf13 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/AlreadyJoinedTeamException.java @@ -0,0 +1,13 @@ +package shop.kkeujeok.kkeujeokbackend.dashboard.team.exception; + +import shop.kkeujeok.kkeujeokbackend.global.error.exception.InvalidGroupException; + +public class AlreadyJoinedTeamException extends InvalidGroupException { + public AlreadyJoinedTeamException(String message) { + super(message); + } + + public AlreadyJoinedTeamException() { + this("이미 해당 팀에 참여한 멤버입니다."); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/NotMemberOfTeamException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/NotMemberOfTeamException.java new file mode 100644 index 00000000..af097012 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/exception/NotMemberOfTeamException.java @@ -0,0 +1,13 @@ +package shop.kkeujeok.kkeujeokbackend.dashboard.team.exception; + +import shop.kkeujeok.kkeujeokbackend.global.error.exception.InvalidGroupException; + +public class NotMemberOfTeamException extends InvalidGroupException { + public NotMemberOfTeamException(String message) { + super(message); + } + + public NotMemberOfTeamException() { + this("해당 멤버는 팀에 속해 있지 않습니다."); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/TeamDocument.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/TeamDocument.java index d7639028..284f5b47 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/TeamDocument.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/TeamDocument.java @@ -28,8 +28,8 @@ public class TeamDocument extends BaseEntity { private String category; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "TeamDashboard_id") - private TeamDashboard TeamDashboard; + @JoinColumn(name = "team_dashboard_id") + private TeamDashboard teamDashboard; @Builder private TeamDocument(String author, @@ -44,7 +44,7 @@ private TeamDocument(String author, this.title = title; this.content = content; this.category = category; - this.TeamDashboard = teamDashboard; + this.teamDashboard = teamDashboard; } public void update(String updateTitle, String updateContent, String updateCategory) { diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/repository/TeamDocumentCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/repository/TeamDocumentCustomRepositoryImpl.java index 5f35b3a4..e537932b 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/repository/TeamDocumentCustomRepositoryImpl.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdoc/domain/repository/TeamDocumentCustomRepositoryImpl.java @@ -32,7 +32,7 @@ public Page findForTeamDocumentByCategory(TeamDashboard teamDashbo List results = queryFactory .selectFrom(teamDocument) .where( - teamDocument.TeamDashboard.eq(teamDashboard), + teamDocument.teamDashboard.eq(teamDashboard), categoryEq(category), teamDocument.status.eq(Status.ACTIVE) ) @@ -43,7 +43,7 @@ public Page findForTeamDocumentByCategory(TeamDashboard teamDashbo long total = queryFactory .selectFrom(teamDocument) .where( - teamDocument.TeamDashboard.eq(teamDashboard), + teamDocument.teamDashboard.eq(teamDashboard), categoryEq(category), teamDocument.status.eq(Status.ACTIVE) ) @@ -61,7 +61,7 @@ public List findTeamDocumentCategory(TeamDashboard teamDashboard) { Set uniqueCategories = queryFactory .select(teamDocument.category) .from(teamDocument) - .where(teamDocument.TeamDashboard.eq(teamDashboard) + .where(teamDocument.teamDashboard.eq(teamDashboard) ,teamDocument.status.eq(Status.ACTIVE)) .stream() .collect(Collectors.toSet()); diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentController.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentController.java deleted file mode 100644 index 5c205ea6..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentController.java +++ /dev/null @@ -1,52 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentUpdateReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application.DocumentService; -import shop.kkeujeok.kkeujeokbackend.global.annotation.CurrentUserEmail; -import shop.kkeujeok.kkeujeokbackend.global.template.RspTemplate; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/documents") -public class DocumentController { - - private final DocumentService documentService; - - @PostMapping("") - public RspTemplate save(@RequestBody DocumentInfoReqDto documentInfoReqDto) { - return new RspTemplate<>(HttpStatus.OK, "팀 문서 등록", documentService.save(documentInfoReqDto)); - } - - @PatchMapping("/{documentId}") - public RspTemplate update(@PathVariable(name = "documentId") Long documentId, - @RequestBody DocumentUpdateReqDto documentUpdateReqDto) { - return new RspTemplate<>(HttpStatus.OK, "팀 문서 수회", documentService.update(documentId, documentUpdateReqDto)); - } - - // 팀 문서 조회 - @GetMapping("") - public RspTemplate findForDocumentByTeamDashboardId( - @RequestParam(name = "teamDashboardId") Long teamDashboardId, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = "10") int size) { - return new RspTemplate<>(HttpStatus.OK, - "블록 상태별 전체 조회", - documentService.findDocumentByTeamDashboardId(teamDashboardId, PageRequest.of(page, size))); - } - - // 팀 문서 삭제 - @DeleteMapping("{documentId}") - public RspTemplate delete(@PathVariable(name = "documentId") Long documentId) { - documentService.delete(documentId); - - return new RspTemplate<>(HttpStatus.OK, "팀 문서 삭제"); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileController.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileController.java deleted file mode 100644 index 928f8970..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileController.java +++ /dev/null @@ -1,54 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; -import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.FileInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application.FileService; -import shop.kkeujeok.kkeujeokbackend.global.annotation.CurrentUserEmail; -import shop.kkeujeok.kkeujeokbackend.global.template.RspTemplate; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/files") -public class FileController { - - private final FileService fileService; - - @PostMapping("") - public RspTemplate save(@CurrentUserEmail String email, - @RequestBody FileInfoReqDto fileInfoReqDto) { - return new RspTemplate<>(HttpStatus.OK, "팀 파일 등록", fileService.save(email, fileInfoReqDto)); - } - - @PatchMapping("/{fileId}") - public RspTemplate update(@PathVariable(name = "fileId") Long fileId, - @RequestBody FileInfoReqDto fileInfoReqDto) { - return new RspTemplate<>(HttpStatus.OK, "팀 파일 수정", fileService.update(fileId, fileInfoReqDto)); - } - - @GetMapping("") - public RspTemplate findForFile(@RequestParam(name = "documentId") Long documentId, - @RequestParam(name = "page", defaultValue = "0") int page, - @RequestParam(name = "size", defaultValue = "10") int size) { - return new RspTemplate<>(HttpStatus.OK, - "팀 문서 파일 조회", - fileService.findForFile(documentId, PageRequest.of(page, size))); - } - - @GetMapping("/{fileId}") - public RspTemplate findById(@PathVariable(name = "fileId") Long fileId) { - return new RspTemplate<>(HttpStatus.OK, "팀 파일 상세보기", fileService.findById(fileId)); - } - - @DeleteMapping("{fileId}") - public RspTemplate delete(@PathVariable(name = "fileId") Long blockId) { - fileService.delete(blockId); - - return new RspTemplate<>(HttpStatus.OK, "팀 파일 삭제"); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentInfoReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentInfoReqDto.java deleted file mode 100644 index ded35d63..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentInfoReqDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request; - -import lombok.Builder; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.member.domain.Member; - -@Builder -public record DocumentInfoReqDto( - Long teamDashboardId, - String title -) { - public Document toEntity(TeamDashboard teamDashboard) { - return Document.builder() - .title(title) - .teamDashboard(teamDashboard) - .build(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentUpdateReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentUpdateReqDto.java deleted file mode 100644 index 52d880b3..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/DocumentUpdateReqDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request; - -public record DocumentUpdateReqDto ( - String title -){ -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/FileInfoReqDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/FileInfoReqDto.java deleted file mode 100644 index 04651cfa..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/request/FileInfoReqDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request; - -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; - -public record FileInfoReqDto( - Long documentId, - String email, - String title, - String content -) { - public File toEntity(String email, Document document) { - return File.builder() - .email(email) - .title(title) - .content(content) - .document(document) - .build(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentInfoResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentInfoResDto.java deleted file mode 100644 index 0d8d3502..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentInfoResDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response; - -import lombok.Builder; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; - -@Builder -public record DocumentInfoResDto( - Long documentId, - String title -) { - public static DocumentInfoResDto from(Document document) { - return DocumentInfoResDto.builder() - .documentId(document.getId()) - .title(document.getTitle()) - .build(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentListResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentListResDto.java deleted file mode 100644 index 74a68ecb..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/DocumentListResDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response; - -import lombok.Builder; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; - -import java.util.List; - -@Builder -public record DocumentListResDto( - List documentInfoResDtos, - PageInfoResDto pageInfoResDto -) { - public static DocumentListResDto of(List documentInfoResDtos, PageInfoResDto pageInfoResDto) { - return DocumentListResDto.builder() - .documentInfoResDtos(documentInfoResDtos) - .pageInfoResDto(pageInfoResDto) - .build(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileInfoResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileInfoResDto.java deleted file mode 100644 index f8972695..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileInfoResDto.java +++ /dev/null @@ -1,21 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response; - -import lombok.Builder; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; - -@Builder -public record FileInfoResDto( - Long fileId, - String email, - String title, - String content -) { - public static FileInfoResDto from(File file) { - return FileInfoResDto.builder() - .fileId(file.getId()) - .email(file.getEmail()) - .title(file.getTitle()) - .content(file.getContent()) - .build(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileListResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileListResDto.java deleted file mode 100644 index 9f14c89f..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/dto/response/FileListResDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response; - -import lombok.Builder; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; - -import java.util.List; - -@Builder -public record FileListResDto( - List fileInfoResDto, - PageInfoResDto pageInfoResDto -) { - public static FileListResDto of(List fileInfoResDto, PageInfoResDto pageInfoResDto) { - return FileListResDto.builder() - .fileInfoResDto(fileInfoResDto) - .pageInfoResDto(pageInfoResDto) - .build(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentService.java deleted file mode 100644 index 3bc45ec1..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentService.java +++ /dev/null @@ -1,70 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application; - -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; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.repository.TeamDashboardRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentUpdateReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.DocumentNotFoundException; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository.DocumentRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.TeamDashboardNotFoundException; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; - -import java.util.List; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class DocumentService { - - private final DocumentRepository documentRepository; - private final TeamDashboardRepository teamDashboardRepository; - - // 팀 문서 생성 - @Transactional - public DocumentInfoResDto save(DocumentInfoReqDto documentInfoReqDto) { - TeamDashboard teamDashboard = teamDashboardRepository.findById(documentInfoReqDto.teamDashboardId()) - .orElseThrow(TeamDashboardNotFoundException::new); - Document document = documentInfoReqDto.toEntity(teamDashboard); - - documentRepository.save(document); - - return DocumentInfoResDto.from(document); - } - - // 팀 문서 수정 - @Transactional - public DocumentInfoResDto update(Long documentId, DocumentUpdateReqDto documentUpdateReqDto) { - Document document = documentRepository.findById(documentId).orElseThrow(DocumentNotFoundException::new); - - document.update(documentUpdateReqDto.title()); - - return DocumentInfoResDto.from(document); - } - - // 팀 문서 조회 - public DocumentListResDto findDocumentByTeamDashboardId(Long teamDashboardId, Pageable pageable) { - Page documents = documentRepository.findByDocumentWithTeamDashboard(teamDashboardId, pageable); - - List documentInfoResDtoList = documents.stream() - .map(DocumentInfoResDto::from) - .toList(); - - return DocumentListResDto.of(documentInfoResDtoList, PageInfoResDto.from(documents)); - } - - // 팀 문서 삭제 - @Transactional - public void delete(Long documentId) { - Document document = documentRepository.findById(documentId).orElseThrow(DocumentNotFoundException::new); - - document.statusUpdate(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileService.java deleted file mode 100644 index a3c1586c..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileService.java +++ /dev/null @@ -1,76 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application; - -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; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.FileInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository.DocumentRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository.FileRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.DocumentNotFoundException; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.FileNotFoundException; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; - -import java.util.List; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class FileService { - - private final DocumentRepository documentRepository; - private final FileRepository fileRepository; - - // 팀 파일 생성 - @Transactional - public FileInfoResDto save(String email, FileInfoReqDto fileInfoReqDto) { - Document document = documentRepository.findById(fileInfoReqDto.documentId()) - .orElseThrow(DocumentNotFoundException::new); - File file = fileInfoReqDto.toEntity(email, document); - - fileRepository.save(file); - - return FileInfoResDto.from(file); - } - - // 팀 파일 수정 - @Transactional - public FileInfoResDto update(Long fileID, FileInfoReqDto fileInfoReqDto) { - File file = fileRepository.findById(fileID).orElseThrow(FileNotFoundException::new); - - file.update(fileInfoReqDto.title(), fileInfoReqDto.content()); - - return FileInfoResDto.from(file); - } - - // 팀 파일 리스트 조회 - public FileListResDto findForFile(Long documentId, Pageable pageable) { - Page files = fileRepository.findByFilesWithDocumentId(documentId, pageable); - - List fileInfoResDtoList = files.stream() - .map(FileInfoResDto::from) - .toList(); - - return FileListResDto.of(fileInfoResDtoList, PageInfoResDto.from(files)); - } - - // 팀 파일 상세보기 - public FileInfoResDto findById(Long fileId) { - File file = fileRepository.findById(fileId).orElseThrow(FileNotFoundException::new); - - return FileInfoResDto.from(file); - } - - // 팀 파일 삭제 - @Transactional - public void delete(Long fileId) { - File file = fileRepository.findById(fileId).orElseThrow(FileNotFoundException::new); - - file.statusUpdate(); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/Document.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/Document.java deleted file mode 100644 index 3fd7fdab..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/Document.java +++ /dev/null @@ -1,52 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; -import shop.kkeujeok.kkeujeokbackend.global.entity.BaseEntity; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; - -import java.util.ArrayList; -import java.util.List; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Document extends BaseEntity { - - @Enumerated(value = EnumType.STRING) - private Status status; - - private String title; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "teamDashboard_id") - private TeamDashboard teamDashboard; - - @OneToMany(mappedBy = "document", cascade = CascadeType.ALL) - private List files = new ArrayList<>(); - - @Builder - private Document(String title, TeamDashboard teamDashboard) { - this.status = Status.ACTIVE; - this.title = title; - this.teamDashboard = teamDashboard; - } - - public void update(String updateTitle) { - if (isUpdateRequired(updateTitle)) { - this.title = updateTitle; - } - } - - private boolean isUpdateRequired(String updateTitle) { - return !this.title.equals(updateTitle); - } - - public void statusUpdate() { - this.status = (this.status == Status.ACTIVE) ? Status.DELETED : Status.ACTIVE; - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/File.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/File.java deleted file mode 100644 index a6acba7f..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/File.java +++ /dev/null @@ -1,53 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import shop.kkeujeok.kkeujeokbackend.global.entity.BaseEntity; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class File extends BaseEntity { - - @Enumerated(value = EnumType.STRING) - private Status status; - - private String email; - - private String title; - - private String content; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "document_id") - private Document document; - - @Builder - private File(String email, String title, String content, Document document) { - this.status = Status.ACTIVE; - this.email = email; - this.title = title; - this.content = content; - this.document = document; - } - - public void update(String updateTitle, String updateContent) { - if (isUpdateRequired(updateTitle, updateContent)) { - this.title = updateTitle; - this.content = updateContent; - } - } - - private boolean isUpdateRequired(String updateTitle, String updateContent) { - return !this.title.equals(updateTitle) || - !this.content.equals(updateContent); - } - - public void statusUpdate() { - this.status = (this.status == Status.ACTIVE) ? Status.DELETED : Status.ACTIVE; - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepository.java deleted file mode 100644 index e86a58a3..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; - -public interface DocumentCustomRepository { - Page findByDocumentWithTeamDashboard(Long documentId, Pageable pageable); -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepositoryImpl.java deleted file mode 100644 index 26ecad85..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentCustomRepositoryImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import static shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.QDocument.document; - -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; - -@Repository -@Transactional(readOnly = true) -public class DocumentCustomRepositoryImpl implements DocumentCustomRepository { - - private final JPAQueryFactory queryFactory; - - public DocumentCustomRepositoryImpl(JPAQueryFactory queryFactory) { - this.queryFactory = queryFactory; - } - - @Override - public Page findByDocumentWithTeamDashboard(Long teamDashboardId, Pageable pageable) { - long total = queryFactory - .selectFrom(document) - .where(document.teamDashboard.id.eq(teamDashboardId) - .and(document.status.eq(Status.ACTIVE))) - .fetchCount(); - - List documents = queryFactory - .selectFrom(document) - .where(document.teamDashboard.id.eq(teamDashboardId) - .and(document.status.eq(Status.ACTIVE))) - .orderBy(document.id.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - return new PageImpl<>(documents, pageable, total); - } - -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepository.java deleted file mode 100644 index d8f3e226..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; - -public interface DocumentRepository extends JpaRepository, DocumentCustomRepository { -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepository.java deleted file mode 100644 index 185ae2bb..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; - -public interface FileCustomRepository { - Page findByFilesWithDocumentId(Long documentId, Pageable pageable); -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepositoryImpl.java deleted file mode 100644 index a3067b6c..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileCustomRepositoryImpl.java +++ /dev/null @@ -1,46 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import static shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.QFile.file; - -import com.querydsl.jpa.impl.JPAQueryFactory; - -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; - -@Repository -@Transactional(readOnly = true) -public class FileCustomRepositoryImpl implements FileCustomRepository { - - private final JPAQueryFactory queryFactory; - - public FileCustomRepositoryImpl(JPAQueryFactory queryFactory) { - this.queryFactory = queryFactory; - } - - @Override - public Page findByFilesWithDocumentId(Long documentId, Pageable pageable) { - long total = queryFactory - .selectFrom(file) - .where(file.document.id.eq(documentId) - .and(file.status.eq(Status.ACTIVE))) - .fetchCount(); - - List files = queryFactory - .selectFrom(file) - .where(file.document.id.eq(documentId) - .and(file.status.eq(Status.ACTIVE))) - .orderBy(file.id.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - return new PageImpl<>(files, pageable, total); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepository.java deleted file mode 100644 index aff17d41..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; - -public interface FileRepository extends JpaRepository, FileCustomRepository { -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/DocumentNotFoundException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/DocumentNotFoundException.java deleted file mode 100644 index 51cc9426..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/DocumentNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception; - -import shop.kkeujeok.kkeujeokbackend.global.error.exception.NotFoundGroupException; - -public class DocumentNotFoundException extends NotFoundGroupException { - public DocumentNotFoundException(String message) { - super(message); - } - - public DocumentNotFoundException() { - this("존재하지 않는 팀 문서 입니다."); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/FileNotFoundException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/FileNotFoundException.java deleted file mode 100644 index da2a1c69..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/FileNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception; - -import shop.kkeujeok.kkeujeokbackend.global.error.exception.NotFoundGroupException; - -public class FileNotFoundException extends NotFoundGroupException { - public FileNotFoundException(String message) { - super(message); - } - - public FileNotFoundException() { - this("존재하지 않는 팀 파일 입니다."); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/TeamDashboardNotFoundException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/TeamDashboardNotFoundException.java deleted file mode 100644 index 84482462..00000000 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/exception/TeamDashboardNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception; - -import shop.kkeujeok.kkeujeokbackend.global.error.exception.NotFoundGroupException; - -public class TeamDashboardNotFoundException extends NotFoundGroupException { - public TeamDashboardNotFoundException(String message) { - super(message); - } - - public TeamDashboardNotFoundException() { - this("존재하지 않는 팀 대시보드 입니다."); - } -} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/S3Service.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/S3Service.java new file mode 100644 index 00000000..2df9ff90 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/S3Service.java @@ -0,0 +1,40 @@ +package shop.kkeujeok.kkeujeokbackend.global.aws; + +import java.io.IOException; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import shop.kkeujeok.kkeujeokbackend.global.aws.exception.InvalidImageUploadException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +@Service +@RequiredArgsConstructor +public class S3Service { + + private final S3Client s3Client; + + private static final String BUCKET_NAME = "kkeujeok-image-bucket"; + private static final String IMAGE_PATH = "challenge-images/"; + private static final String S3_URL_FORMAT = "https://%s.s3.amazonaws.com/%s"; + private static final String IMAGE_EXTENSION = ".jpg"; + + + public String uploadChallengeImage(MultipartFile file) { + String key = IMAGE_PATH + UUID.randomUUID() + IMAGE_EXTENSION; + try { + s3Client.putObject( + PutObjectRequest.builder() + .bucket(BUCKET_NAME) + .key(key) + .build(), + RequestBody.fromInputStream(file.getInputStream(), file.getSize()) + ); + return String.format(S3_URL_FORMAT, BUCKET_NAME, key); + } catch (IOException e) { + throw new InvalidImageUploadException(); + } + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/exception/InvalidImageUploadException.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/exception/InvalidImageUploadException.java new file mode 100644 index 00000000..c1e96605 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/aws/exception/InvalidImageUploadException.java @@ -0,0 +1,14 @@ +package shop.kkeujeok.kkeujeokbackend.global.aws.exception; + +import shop.kkeujeok.kkeujeokbackend.global.error.exception.InvalidGroupException; + +public class InvalidImageUploadException extends InvalidGroupException { + + public InvalidImageUploadException(String message) { + super(message); + } + + public InvalidImageUploadException() { + this("이미지 업로드 중 오류가 발생했습니다."); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/global/config/S3Config.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/config/S3Config.java new file mode 100644 index 00000000..da9d7cd6 --- /dev/null +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/global/config/S3Config.java @@ -0,0 +1,27 @@ +package shop.kkeujeok.kkeujeokbackend.global.config; + +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 S3Config { + + @Value("${aws.s3.accessKey}") + private String accessKey; + + @Value("${aws.s3.secretKey}") + private String secretKey; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey))) + .build(); + } +} diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberController.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberController.java index 51ac9a04..ed3380dd 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberController.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberController.java @@ -2,7 +2,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import shop.kkeujeok.kkeujeokbackend.global.annotation.CurrentUserEmail; @@ -12,8 +11,6 @@ import shop.kkeujeok.kkeujeokbackend.member.mypage.api.dto.response.TeamDashboardsAndChallengesResDto; import shop.kkeujeok.kkeujeokbackend.member.mypage.application.MyPageService; -import java.util.Set; - @RestController @RequiredArgsConstructor @RequestMapping("/api/members") @@ -35,9 +32,10 @@ public RspTemplate update(@CurrentUserEmail String email, @GetMapping("/mypage/dashboard-challenges") public RspTemplate getTeamDashboardsAndChallenges(@CurrentUserEmail String email, + @RequestParam(name = "requestEmail") String requestEmail, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "10") int size) { - TeamDashboardsAndChallengesResDto response = myPageService.findTeamDashboardsAndChallenges(email, PageRequest.of(page, size)); - return new RspTemplate<>(HttpStatus.OK, "팀 대시보드와 챌린지 정보 조회", response); + TeamDashboardsAndChallengesResDto response = myPageService.findTeamDashboardsAndChallenges(email, requestEmail, PageRequest.of(page, size)); + return new RspTemplate<>(HttpStatus.OK, "대시보드와 챌린지 정보 조회", response); } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java index abbb70c5..2c5f96b9 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/domain/Member.java @@ -12,6 +12,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import shop.kkeujeok.kkeujeokbackend.challenge.domain.Challenge; +import shop.kkeujeok.kkeujeokbackend.challenge.domain.ChallengeMemberMapping; import shop.kkeujeok.kkeujeokbackend.global.entity.BaseEntity; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.notification.domain.Notification; @@ -50,6 +51,9 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "receiver", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) private List notifications = new ArrayList<>(); + @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private List challengeMemberMappings; + @Builder private Member(Status status, Role role, String email, String name, diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/api/dto/response/TeamDashboardsAndChallengesResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/api/dto/response/TeamDashboardsAndChallengesResDto.java index 093700d0..fc845c1e 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/api/dto/response/TeamDashboardsAndChallengesResDto.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/api/dto/response/TeamDashboardsAndChallengesResDto.java @@ -1,14 +1,19 @@ package shop.kkeujeok.kkeujeokbackend.member.mypage.api.dto.response; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeListResDto; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardPageListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardListResDto; public record TeamDashboardsAndChallengesResDto( + PersonalDashboardPageListResDto personalDashboardList, TeamDashboardListResDto teamDashboardList, ChallengeListResDto challengeList ) { - public static TeamDashboardsAndChallengesResDto of(TeamDashboardListResDto teamDashboardList, ChallengeListResDto challengeList) { + public static TeamDashboardsAndChallengesResDto of(PersonalDashboardPageListResDto personalDashboardList, + TeamDashboardListResDto teamDashboardList, + ChallengeListResDto challengeList) { return new TeamDashboardsAndChallengesResDto( + personalDashboardList, teamDashboardList, challengeList ); diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageService.java index 3e670846..04cf6a88 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageService.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageService.java @@ -7,6 +7,8 @@ import shop.kkeujeok.kkeujeokbackend.auth.exception.EmailNotFoundException; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeListResDto; import shop.kkeujeok.kkeujeokbackend.challenge.application.ChallengeService; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardPageListResDto; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.application.PersonalDashboardService; import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.team.application.TeamDashboardService; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -22,6 +24,7 @@ public class MyPageService { private final MemberRepository memberRepository; + private final PersonalDashboardService personalDashboardService; private final TeamDashboardService teamDashboardService; private final ChallengeService challengeService; @@ -46,13 +49,24 @@ public MyPageInfoResDto update(String email, MyPageUpdateReqDto myPageUpdateReqD return MyPageInfoResDto.From(member); } - // 팀 대시보드 & 챌린지 정보 조회 + // 대시보드 & 챌린지 정보 조회 @Transactional(readOnly = true) - public TeamDashboardsAndChallengesResDto findTeamDashboardsAndChallenges(String email, Pageable pageable) { - TeamDashboardListResDto teamDashboardListResDto = teamDashboardService.findForTeamDashboard(email, pageable); - ChallengeListResDto challengeListResDto = challengeService.findChallengeForMemberId(email, pageable); + public TeamDashboardsAndChallengesResDto findTeamDashboardsAndChallenges(String email, + String requestEmail, + Pageable pageable) { + TeamDashboardListResDto teamDashboardListResDto = teamDashboardService.findForTeamDashboard(requestEmail, pageable); + ChallengeListResDto challengeListResDto = challengeService.findChallengeForMemberId(requestEmail, pageable); - return TeamDashboardsAndChallengesResDto.of(teamDashboardListResDto, challengeListResDto); + PersonalDashboardPageListResDto personalDashboardPageListResDto; + + if (email.equals(requestEmail)) { + // 본인이면 isPublic과 상관없이 모든 대시보드 및 챌린지 조회 + personalDashboardPageListResDto = personalDashboardService.findForPersonalDashboard(requestEmail, pageable); + } else { + // 타인이면 isPublic이 true인 대시보드 및 챌린지 조회 + personalDashboardPageListResDto = personalDashboardService.findPublicPersonalDashboards(requestEmail, pageable); + } + return TeamDashboardsAndChallengesResDto.of(personalDashboardPageListResDto,teamDashboardListResDto, challengeListResDto); } private boolean isNicknameChanged(Member member, String newNickname) { diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationController.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationController.java index a69686a8..ce8860d5 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationController.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationController.java @@ -1,11 +1,10 @@ package shop.kkeujeok.kkeujeokbackend.notification.api; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import shop.kkeujeok.kkeujeokbackend.global.annotation.CurrentUserEmail; @@ -26,11 +25,15 @@ public SseEmitter streamNotifications(@CurrentUserEmail String email) { } @GetMapping - public RspTemplate findAllNotifications(@CurrentUserEmail String email, - @RequestParam(defaultValue = "0", name = "page") int page, - @RequestParam(defaultValue = "10", name = "size") int size) { + public RspTemplate findAllNotifications(@CurrentUserEmail String email) { return new RspTemplate<>(HttpStatus.OK, "알림 조회 성공", - notificationService.findAllNotificationsFromMember(email, PageRequest.of(page, size))); + notificationService.findAllNotificationsFromMember(email)); + } + + @PatchMapping + public RspTemplate markAllNotificationsAsRead(@CurrentUserEmail String email) { + notificationService.markAllNotificationsAsRead(email); + return new RspTemplate<>(HttpStatus.OK, "모든 알림이 읽음으로 표시되었습니다."); } /*@GetMapping("/{notificationId}") diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/dto/response/NotificationListResDto.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/dto/response/NotificationListResDto.java index f145a5f9..52010fab 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/dto/response/NotificationListResDto.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/api/dto/response/NotificationListResDto.java @@ -2,19 +2,15 @@ import java.util.List; import lombok.Builder; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; @Builder public record NotificationListResDto( - List notificationInfoResDto, - PageInfoResDto pageInfoResDto + List notificationInfoResDto ) { - public static NotificationListResDto of(List notificationInfoResDtoList, - PageInfoResDto pageInfoResDto) { + public static NotificationListResDto of(List notificationInfoResDtoList) { return NotificationListResDto.builder() .notificationInfoResDto(notificationInfoResDtoList) - .pageInfoResDto(pageInfoResDto) .build(); } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationService.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationService.java index 1a51f65e..d2921f27 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationService.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationService.java @@ -2,12 +2,9 @@ import java.util.List; 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; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.repository.MemberRepository; import shop.kkeujeok.kkeujeokbackend.member.exception.MemberNotFoundException; @@ -45,17 +42,25 @@ public void sendNotification(Member member, String message) { } @Transactional(readOnly = true) - public NotificationListResDto findAllNotificationsFromMember(String email, Pageable pageable) { + public NotificationListResDto findAllNotificationsFromMember(String email) { Member member = findMemberByEmail(email); - Page notifications = notificationRepository.findAllNotifications(member, pageable); - - List notificationList = notifications.stream() + List notifications = notificationRepository.findAllByReceiver(member) + .stream() .map(NotificationInfoResDto::from) .toList(); - return NotificationListResDto.of(notificationList, PageInfoResDto.from(notifications)); + return NotificationListResDto.of(notifications); } + @Transactional + public void markAllNotificationsAsRead(String email) { + Member member = findMemberByEmail(email); + List notifications = notificationRepository.findAllByReceiver(member); + + notifications.forEach(Notification::markAsRead); + } + + /*@Transactional public NotificationInfoResDto findByNotificationId(Long notificationId) { Notification notification = notificationRepository.findById(notificationId) diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepository.java index 3c053fc7..d1a0c718 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepository.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepository.java @@ -1,10 +1,4 @@ package shop.kkeujeok.kkeujeokbackend.notification.domain.repository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import shop.kkeujeok.kkeujeokbackend.member.domain.Member; -import shop.kkeujeok.kkeujeokbackend.notification.domain.Notification; - public interface NotificationCustomRepository { - Page findAllNotifications(Member member, Pageable pageable); } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepositoryImpl.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepositoryImpl.java index 663e63a2..63eb63a3 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepositoryImpl.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationCustomRepositoryImpl.java @@ -1,52 +1,11 @@ package shop.kkeujeok.kkeujeokbackend.notification.domain.repository; -import com.querydsl.jpa.impl.JPAQueryFactory; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import shop.kkeujeok.kkeujeokbackend.member.domain.Member; -import shop.kkeujeok.kkeujeokbackend.notification.domain.Notification; -import shop.kkeujeok.kkeujeokbackend.notification.domain.QNotification; @Repository @RequiredArgsConstructor @Transactional(readOnly = true) public class NotificationCustomRepositoryImpl implements NotificationCustomRepository { - - private final JPAQueryFactory queryFactory; - - @Override - public Page findAllNotifications(Member member, Pageable pageable) { - QNotification notification = QNotification.notification; - - long total = getTotal(notification, member); - List notifications = getNotifications(notification, member, pageable); - - return new PageImpl<>(notifications, pageable, total); - } - - private long getTotal(QNotification notification, Member member) { - return Optional.ofNullable( - queryFactory - .select(notification.count()) - .from(notification) - .where(notification.receiver.eq(member)) - .fetchOne() - ).orElse(0L); - } - - private List getNotifications(QNotification notification, Member member, Pageable pageable) { - return queryFactory - .selectFrom(notification) - .where(notification.receiver.eq(member)) - .orderBy(notification.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - } } diff --git a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationRepository.java b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationRepository.java index b2ab3b69..f533528f 100644 --- a/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationRepository.java +++ b/src/main/java/shop/kkeujeok/kkeujeokbackend/notification/domain/repository/NotificationRepository.java @@ -5,6 +5,6 @@ import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.notification.domain.Notification; -public interface NotificationRepository extends JpaRepository, NotificationCustomRepository { +public interface NotificationRepository extends JpaRepository { List findAllByReceiver(Member member); } diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeControllerTest.java index 4761a577..d6866e7e 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeControllerTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/api/ChallengeControllerTest.java @@ -12,17 +12,19 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.requestFields; import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.responseFields; import java.time.LocalDate; @@ -36,9 +38,12 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.multipart.MultipartFile; import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.TokenReqDto; import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockInfoResDto; import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; @@ -89,25 +94,29 @@ void setUp(RestDocumentationContextProvider restDocumentation) { .build(); challengeSaveReqDto = new ChallengeSaveReqDto( - "1일 1커밋", - "1일 1커밋하기", + "챌린지 제목", + "챌린지 내용", Category.CREATIVITY_AND_ARTS, Cycle.WEEKLY, List.of(CycleDetail.MON, CycleDetail.TUE), LocalDate.now(), - LocalDate.now().plusDays(30), - "대표 이미지"); + "블록 이름"); challenge = Challenge.builder() .title(challengeSaveReqDto.title()) .contents(challengeSaveReqDto.title()) + .category(challengeSaveReqDto.category()) + .cycle(challengeSaveReqDto.cycle()) .cycleDetails(challengeSaveReqDto.cycleDetails()) - .startDate(challengeSaveReqDto.startDate()) .endDate(challengeSaveReqDto.endDate()) - .representImage(challengeSaveReqDto.representImage()) .member(member) + .blockName(challengeSaveReqDto.blockName()) + .representImage("대표 이미지") .build(); + ReflectionTestUtils.setField(challenge, "id", 1L); + ReflectionTestUtils.setField(challenge, "startDate", LocalDate.now()); + challengeUpdateReqDto = new ChallengeSaveReqDto( "업데이트 제목", "업데이트 내용", @@ -115,10 +124,9 @@ void setUp(RestDocumentationContextProvider restDocumentation) { Cycle.WEEKLY, List.of(CycleDetail.MON), LocalDate.now(), - LocalDate.now().plusDays(30), - "업데이트 이미지"); + "1일 1커밋"); - challengeSearchReqDto = new ChallengeSearchReqDto("1일"); + challengeSearchReqDto = new ChallengeSearchReqDto("챌린지", "CREATIVITY_AND_ARTS"); challengeController = new ChallengeController(challengeService); @@ -131,91 +139,105 @@ void setUp(RestDocumentationContextProvider restDocumentation) { .thenReturn("kkeujeok@gmail.com"); } + @Test @DisplayName("챌린지 생성 성공 시 상태코드 201 반환") void 챌린지_생성_성공_시_상태코드_201_반환() throws Exception { // given ChallengeInfoResDto response = ChallengeInfoResDto.from(challenge); - given(challengeService.save(anyString(), any(ChallengeSaveReqDto.class))) + + given(challengeService.save(anyString(), any(ChallengeSaveReqDto.class), any(MultipartFile.class))) .willReturn(response); + MockMultipartFile file = new MockMultipartFile("representImage", "test-image.jpg", "image/jpeg", + "test image content".getBytes()); + + String challengeDtoJson = objectMapper.writeValueAsString(challengeSaveReqDto); + + MockMultipartFile dtoPart = new MockMultipartFile("challengeSaveReqDto", "challengeSaveReqDto.json", + "application/json", challengeDtoJson.getBytes()); + // when & then mockMvc.perform( - post("/api/challenges") - .header(AUTHORIZATION_HEADER_NAME, - AUTHORIZATION_HEADER_VALUE) + multipart("/api/challenges") + .file(file) + .file(dtoPart) + .header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE) .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(challengeSaveReqDto))) + .contentType(MediaType.MULTIPART_FORM_DATA) + ) .andDo(print()) .andDo(document("challenge/save", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")), - requestFields( - fieldWithPath("title").description("챌린지 제목"), - fieldWithPath("contents").description("챌린지 내용"), - fieldWithPath("category").description("챌린지 카테고리"), - fieldWithPath("cycle").description("챌린지 주기"), - fieldWithPath("cycleDetails").description("주기 상세정보"), - fieldWithPath("startDate").description("시작 날짜"), - fieldWithPath("endDate").description("종료 날짜"), - fieldWithPath("representImage").description("대표 사진")), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.challengeId").description("챌린지 id"), - fieldWithPath("data.title").description("챌린지 제목"), - fieldWithPath("data.contents").description("챌린지 내용"), - fieldWithPath("data.category").description("챌린지 카테고리"), - fieldWithPath("data.cycle").description("챌린지 주기"), - fieldWithPath("data.cycleDetails").description("주기 상세정보"), - fieldWithPath("data.startDate").description("시작 날짜"), - fieldWithPath("data.endDate").description("종료 날짜"), - fieldWithPath("data.representImage").description("대표 사진"), - fieldWithPath("data.authorName").description("챌린지 작성자 이름"), - fieldWithPath("data.authorProfileImage").description("챌린지 작성자 프로필 이미지") - )) + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")), + requestParts( + partWithName("challengeSaveReqDto").description("챌린지 DTO"), + partWithName("representImage").description("대표 이미지") + ), + responseFields( + fieldWithPath("statusCode").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.challengeId").description("챌린지 id"), + fieldWithPath("data.title").description("챌린지 제목"), + fieldWithPath("data.contents").description("챌린지 내용"), + fieldWithPath("data.category").description("챌린지 카테고리"), + fieldWithPath("data.cycle").description("챌린지 주기"), + fieldWithPath("data.cycleDetails").description("주기 상세정보"), + fieldWithPath("data.startDate").description("시작 날짜"), + fieldWithPath("data.endDate").description("종료 날짜"), + fieldWithPath("data.representImage").description("대표 사진"), + fieldWithPath("data.authorName").description("챌린지 작성자 이름"), + fieldWithPath("data.authorProfileImage").description("챌린지 작성자 프로필 이미지"), + fieldWithPath("data.blockName").description("블록 이름"), + fieldWithPath("data.participantCount").description("참여자 수"), + fieldWithPath("data.isParticipant").description("참여 여부"), + fieldWithPath("data.isAuthor").description("작성자 여부"), + fieldWithPath("data.completedMembers[]").description("완료한 회원 목록") + ) + ) ) .andExpect(status().isOk()); } @Test - @DisplayName("블록 수정에 성공하면 상태코드 200 반환") - void 블록_수정에_성공하면_상태코드_200_반환() throws Exception { + @DisplayName("챌린지 수정에 성공하면 상태코드 200 반환") + void 챌린지_수정에_성공하면_상태코드_200_반환() throws Exception { // given - challenge.update(challengeUpdateReqDto.title(), - challengeUpdateReqDto.contents(), - challengeUpdateReqDto.cycleDetails(), - challengeUpdateReqDto.startDate(), - challengeUpdateReqDto.endDate(), - challengeUpdateReqDto.representImage()); + MockMultipartFile file = new MockMultipartFile("representImage", "test-image.jpg", "image/jpeg", + "test image content".getBytes()); + + String challengeDtoJson = objectMapper.writeValueAsString(challengeUpdateReqDto); + + MockMultipartFile dtoPart = new MockMultipartFile("challengeSaveReqDto", "challengeSaveReqDto.json", + "application/json", challengeDtoJson.getBytes()); + ChallengeInfoResDto response = ChallengeInfoResDto.from(challenge); - given(challengeService.update(anyString(), anyLong(), any(ChallengeSaveReqDto.class))) + given(challengeService.update(anyString(), anyLong(), any(ChallengeSaveReqDto.class), any(MultipartFile.class))) .willReturn(response); // when & then - mockMvc.perform( - patch("/api/challenges/{challengeId}", 1L) - .header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(challengeSaveReqDto))) + mockMvc.perform(multipart("/api/challenges/{challengeId}", 1L) + .file(file) // 이미지 파일 포함 + .file(dtoPart) // DTO 포함 + .header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.MULTIPART_FORM_DATA) + .with(request -> { + request.setMethod("PATCH"); + return request; + }) + ) .andDo(print()) .andDo(document("challenge/update", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")), pathParameters(parameterWithName("challengeId").description("챌린지 ID")), - requestFields( - fieldWithPath("title").description("챌린지 제목"), - fieldWithPath("contents").description("챌린지 내용"), - fieldWithPath("category").description("챌린지 카테고리"), - fieldWithPath("cycle").description("챌린지 주기"), - fieldWithPath("cycleDetails").description("주기 상세정보"), - fieldWithPath("startDate").description("시작 날짜"), - fieldWithPath("endDate").description("종료 날짜"), - fieldWithPath("representImage").description("대표 사진")), + requestParts( + partWithName("challengeSaveReqDto").description("챌린지 DTO"), + partWithName("representImage").description("대표 이미지") + ), responseFields( fieldWithPath("statusCode").description("상태 코드"), fieldWithPath("message").description("응답 메시지"), @@ -229,10 +251,18 @@ void setUp(RestDocumentationContextProvider restDocumentation) { fieldWithPath("data.endDate").description("종료 날짜"), fieldWithPath("data.representImage").description("대표 사진"), fieldWithPath("data.authorName").description("챌린지 작성자 이름"), - fieldWithPath("data.authorProfileImage").description("챌린지 작성자 프로필 이미지")))) + fieldWithPath("data.authorProfileImage").description("챌린지 작성자 프로필 이미지"), + fieldWithPath("data.blockName").description("블록 이름"), + fieldWithPath("data.participantCount").description("참여자 수"), + fieldWithPath("data.isParticipant").description("참여 여부"), + fieldWithPath("data.isAuthor").description("작성자 여부"), + fieldWithPath("data.completedMembers[]").description("완료한 회원 목록") + )) + ) .andExpect(status().isOk()); } + @Test @DisplayName("챌린지 전체 조회에 성공하면 상태코드 200 반환") void 챌린지_전체_조회에_성공하면_상태코드_200_반환() throws Exception { @@ -248,139 +278,137 @@ void setUp(RestDocumentationContextProvider restDocumentation) { // when & then mockMvc.perform( get("/api/challenges") + .param("page", "0") + .param("size", "10") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) - .andDo(document("challenge/findAll", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.challengeInfoResDto[].challengeId").description("챌린지 id"), - fieldWithPath("data.challengeInfoResDto[].title").description("챌린지 제목"), - fieldWithPath("data.challengeInfoResDto[].contents").description("챌린지 내용"), - fieldWithPath("data.challengeInfoResDto[].category").description("챌린지 카테고리"), - fieldWithPath("data.challengeInfoResDto[].cycle").description("챌린지 주기"), - fieldWithPath("data.challengeInfoResDto[].cycleDetails[]").description("주기 상세정보"), - fieldWithPath("data.challengeInfoResDto[].startDate").description("시작 날짜"), - fieldWithPath("data.challengeInfoResDto[].endDate").description("종료 날짜"), - fieldWithPath("data.challengeInfoResDto[].representImage").description("대표 사진"), - fieldWithPath("data.challengeInfoResDto[].authorName").description("챌린지 작성자 이름"), - fieldWithPath("data.challengeInfoResDto[].authorProfileImage").description( - "챌린지 작성자 프로필 이미지"), - fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지"), - fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지"), - fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템") - - ))).andExpect(status().isOk()); + .andDo( + document("challenge/findAll", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("page").description("페이지 번호"), + parameterWithName("size").description("페이지 크기") + ), + responseFields( + fieldWithPath("statusCode").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.challengeInfoResDto[].challengeId").description("챌린지 id"), + fieldWithPath("data.challengeInfoResDto[].title").description("챌린지 제목"), + fieldWithPath("data.challengeInfoResDto[].contents").description("챌린지 내용"), + fieldWithPath("data.challengeInfoResDto[].category").description("챌린지 카테고리"), + fieldWithPath("data.challengeInfoResDto[].cycle").description("챌린지 주기"), + fieldWithPath("data.challengeInfoResDto[].cycleDetails[]").description( + "주기 상세정보"), + fieldWithPath("data.challengeInfoResDto[].startDate").description("시작 날짜"), + fieldWithPath("data.challengeInfoResDto[].endDate").description("종료 날짜"), + fieldWithPath("data.challengeInfoResDto[].representImage").description("대표 사진"), + fieldWithPath("data.challengeInfoResDto[].authorName").description( + "챌린지 작성자 이름"), + fieldWithPath("data.challengeInfoResDto[].authorProfileImage").description( + "챌린지 작성자 프로필 이미지"), + fieldWithPath("data.challengeInfoResDto[].participantCount").description( + "참여자 수"), + fieldWithPath("data.challengeInfoResDto[].isParticipant").description("참여 여부"), + fieldWithPath("data.challengeInfoResDto[].isAuthor").description("작성자 여부"), + fieldWithPath("data.challengeInfoResDto[].blockName").description("블록 이름"), + fieldWithPath("data.challengeInfoResDto[].completedMembers[]").description( + "완료한 회원 목록"), + fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지"), + fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지"), + fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템") + ) + ) + ).andExpect(status().isOk()); } @Test - @DisplayName("검색에 성공하면 상태코드 200 반환") - void 검색에_성공하면_상태코드_200_반환() throws Exception { + @DisplayName("카테고리 별 검색에 성공하면 상태코드 200 반환") + void 카테고리_별_검색에_성공하면_상태코드_200_반환() throws Exception { // given ChallengeInfoResDto challengeInfoResDto = ChallengeInfoResDto.from(challenge); Page challengePage = new PageImpl<>(List.of(challenge), PageRequest.of(0, 10), 1); ChallengeListResDto response = ChallengeListResDto.of(List.of(challengeInfoResDto), PageInfoResDto.from(challengePage)); - given(challengeService.findChallengesByKeyWord(any(ChallengeSearchReqDto.class), any(PageRequest.class))) + given(challengeService.findChallengesByCategoryAndKeyword(any(ChallengeSearchReqDto.class), + any(PageRequest.class))) .willReturn(response); // when & then mockMvc.perform( - get("/api/challenges/search?keyword=%s", - challengeSearchReqDto.keyWord()) + get("/api/challenges/search") + .param("category", challengeSearchReqDto.category()) + .param("keyword", challengeSearchReqDto.keyWord()) + .param("page", "0") + .param("size", "10") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) - .andDo(document("challenge/search", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.challengeInfoResDto[].challengeId").description("챌린지 id"), - fieldWithPath("data.challengeInfoResDto[].title").description("챌린지 제목"), - fieldWithPath("data.challengeInfoResDto[].contents").description("챌린지 내용"), - fieldWithPath("data.challengeInfoResDto[].category").description("챌린지 카테고리"), - fieldWithPath("data.challengeInfoResDto[].cycle").description("챌린지 주기"), - fieldWithPath("data.challengeInfoResDto[].cycleDetails[]").description("주기 상세정보"), - fieldWithPath("data.challengeInfoResDto[].startDate").description("시작 날짜"), - fieldWithPath("data.challengeInfoResDto[].endDate").description("종료 날짜"), - fieldWithPath("data.challengeInfoResDto[].representImage").description("대표 사진"), - fieldWithPath("data.challengeInfoResDto[].authorName").description("챌린지 작성자 이름"), - fieldWithPath("data.challengeInfoResDto[].authorProfileImage") - .description("챌린지 작성자 프로필 이미지"), - fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지"), - fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지"), - fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템") - - )) + .andDo( + document("challenge/search", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("category").description("카테고리"), + parameterWithName("keyword").description("검색 키워드"), + parameterWithName("page").description("페이지 번호"), + parameterWithName("size").description("페이지 크기") + ), + responseFields( + fieldWithPath("statusCode").description("상태 코드"), + fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.challengeInfoResDto[].challengeId").description("챌린지 id"), + fieldWithPath("data.challengeInfoResDto[].title").description("챌린지 제목"), + fieldWithPath("data.challengeInfoResDto[].contents").description("챌린지 내용"), + fieldWithPath("data.challengeInfoResDto[].category").description("챌린지 카테고리"), + fieldWithPath("data.challengeInfoResDto[].cycle").description("챌린지 주기"), + fieldWithPath("data.challengeInfoResDto[].cycleDetails[]").description( + "주기 상세정보"), + fieldWithPath("data.challengeInfoResDto[].startDate").description("시작 날짜"), + fieldWithPath("data.challengeInfoResDto[].endDate").description("종료 날짜"), + fieldWithPath("data.challengeInfoResDto[].representImage").description("대표 사진"), + fieldWithPath("data.challengeInfoResDto[].authorName").description( + "챌린지 작성자 이름"), + fieldWithPath("data.challengeInfoResDto[].authorProfileImage") + .description("챌린지 작성자 프로필 이미지"), + fieldWithPath("data.challengeInfoResDto[].blockName").description("블록 이름"), + fieldWithPath("data.challengeInfoResDto[].participantCount").description( + "참여자 수"), + fieldWithPath("data.challengeInfoResDto[].isParticipant").description("참여 여부"), + fieldWithPath("data.challengeInfoResDto[].isAuthor").description("작성자 여부"), + fieldWithPath("data.challengeInfoResDto[].completedMembers[]").description( + "완료한 회원 목록"), + fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지"), + fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지"), + fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템") + ) + ) ) .andExpect(status().isOk()); } - @Test - @DisplayName("챌린지 카테고리 별 검색에 성공하면 상태코드 200 반환") - void 챌린지_카테고리_별_검색에_성공하면_상태코드_200_반환() throws Exception { - // given - ChallengeInfoResDto challengeInfoResDto = ChallengeInfoResDto.from(challenge); - Page challengePage = new PageImpl<>(List.of(challenge), - PageRequest.of(0, 10), 1); - ChallengeListResDto response = ChallengeListResDto.of(List.of(challengeInfoResDto), - PageInfoResDto.from(challengePage)); - - given(challengeService.findByCategory(anyString(), any(PageRequest.class))) - .willReturn(response); - - // when & then - mockMvc.perform( - get("/api/challenges/find?category=%s", - Category.CREATIVITY_AND_ARTS) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON)) - .andDo(print()) - .andDo(document("challenge/category", preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.challengeInfoResDto[].challengeId").description("챌린지 id"), - fieldWithPath("data.challengeInfoResDto[].title").description("챌린지 제목"), - fieldWithPath("data.challengeInfoResDto[].contents").description("챌린지 내용"), - fieldWithPath("data.challengeInfoResDto[].category").description("챌린지 카테고리"), - fieldWithPath("data.challengeInfoResDto[].cycle").description("챌린지 주기"), - fieldWithPath("data.challengeInfoResDto[].cycleDetails[]").description("주기 상세정보"), - fieldWithPath("data.challengeInfoResDto[].startDate").description("시작 날짜"), - fieldWithPath("data.challengeInfoResDto[].endDate").description("종료 날짜"), - fieldWithPath("data.challengeInfoResDto[].representImage").description("대표 사진"), - fieldWithPath("data.challengeInfoResDto[].authorName").description("챌린지 작성자 이름"), - fieldWithPath("data.challengeInfoResDto[].authorProfileImage") - .description("챌린지 작성자 프로필 이미지"), - fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지"), - fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지"), - fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템") - )) - ).andExpect(status().isOk()); - } @Test @DisplayName("챌린지 상세 정보 조회에 성공하면 상태코드 200 반환") void 챌린지_상세_조회에_성공하면_상태코드_200_반환() throws Exception { // given ChallengeInfoResDto response = ChallengeInfoResDto.from(challenge); - given(challengeService.findById(anyLong())) + given(challengeService.findById(anyString(), anyLong())) .willReturn(response); // when & then mockMvc.perform( get("/api/challenges/{challengeId}", 1L) + .header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE) .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andDo(document("challenge/findById", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")), pathParameters(parameterWithName("challengeId").description("챌린지 ID")), responseFields( fieldWithPath("statusCode").description("상태 코드"), @@ -395,7 +423,13 @@ void setUp(RestDocumentationContextProvider restDocumentation) { fieldWithPath("data.endDate").description("종료 날짜"), fieldWithPath("data.representImage").description("대표 사진"), fieldWithPath("data.authorName").description("챌린지 작성자 이름"), - fieldWithPath("data.authorProfileImage").description("챌린지 작성자 프로필 이미지") + fieldWithPath("data.authorProfileImage").description("챌린지 작성자 프로필 이미지"), + fieldWithPath("data.blockName").description("블록 이름"), + fieldWithPath("data.participantCount").description("참여자 수"), + fieldWithPath("data.isParticipant").description("참여 여부"), + fieldWithPath("data.isAuthor").description("작성자 여부"), + fieldWithPath("data.completedMembers[]").description("완료한 회원 목록") + )) ) .andExpect(status().isOk()); @@ -441,8 +475,7 @@ void setUp(RestDocumentationContextProvider restDocumentation) { "picture", 0); - given(challengeService.addChallengeToPersonalDashboard(anyString(), anyLong(), anyLong())) - .willReturn(blockInfoResDto); + willDoNothing().given(challengeService).addChallengeToPersonalDashboard(anyString(), anyLong(), anyLong()); // when & then mockMvc.perform(post("/api/challenges/{challengeId}/{dashboardId}", 1L, 1L) @@ -453,27 +486,35 @@ void setUp(RestDocumentationContextProvider restDocumentation) { .content(objectMapper.writeValueAsString(blockInfoResDto))) .andDo(print()) .andDo(document("challenge/addChallengeToPersonalDashboard", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")), + pathParameters(parameterWithName("challengeId").description("챌린지 ID"), + parameterWithName("dashboardId").description("대시보드 ID")) + ) + ) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("챌린지 탈퇴에 성공하면 상태코드 200 반환") + void 챌린지_탈퇴에_성공하면_상태코드_200_반환() throws Exception { + // given + willDoNothing().given(challengeService).withdrawFromChallenge(anyString(), anyLong()); + + // when & then + mockMvc.perform(delete("/api/challenges/{challengeId}/withdraw", 1L) + .header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("challenge/withdraw", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")), - pathParameters(parameterWithName("challengeId").description("챌린지 ID"), - parameterWithName("dashboardId").description("대시보드 ID")), - responseFields(fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.blockId").description("블록 ID"), - fieldWithPath("data.title").description("블록 제목"), - fieldWithPath("data.contents").description("블록 내용"), - fieldWithPath("data.progress").description("블록 진행도"), - fieldWithPath("data.type").description( - "블록 타입(일반(General) 블록인지 챌린지(Challenge) 블록인지 구별)"), - fieldWithPath("data.dType").description("개인 대시보드, 팀 대시보드를 구별"), - fieldWithPath("data.startDate").description("블록 시작기한"), - fieldWithPath("data.deadLine").description("블록 마감기한"), - fieldWithPath("data.nickname").description("블록 작성자 닉네임"), - fieldWithPath("data.picture").description("블록 작성자 사진"), - fieldWithPath("data.dDay").description("블록 디데이") - )) - ) + pathParameters(parameterWithName("challengeId").description("챌린지 ID")) + )) .andExpect(status().isOk()); } + } \ No newline at end of file diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeServiceTest.java index b42632f4..a4823bd0 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeServiceTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/ChallengeServiceTest.java @@ -24,7 +24,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import shop.kkeujeok.kkeujeokbackend.block.api.dto.response.BlockInfoResDto; +import org.springframework.web.multipart.MultipartFile; import shop.kkeujeok.kkeujeokbackend.block.domain.Block; import shop.kkeujeok.kkeujeokbackend.block.domain.Progress; import shop.kkeujeok.kkeujeokbackend.block.domain.Type; @@ -43,6 +43,7 @@ import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.request.PersonalDashboardSaveReqDto; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.PersonalDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.domain.repository.PersonalDashboardRepository; +import shop.kkeujeok.kkeujeokbackend.global.aws.S3Service; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.Role; @@ -78,6 +79,12 @@ class ChallengeServiceTest { @Mock private NotificationService notificationService; + @Mock + private MultipartFile multipartFile; + + @Mock + private S3Service s3Service; + @InjectMocks private ChallengeService challengeService; @@ -104,9 +111,8 @@ void setUp() { Category.CREATIVITY_AND_ARTS, Cycle.WEEKLY, List.of(CycleDetail.MON, CycleDetail.TUE), - LocalDate.now(), LocalDate.now().plusDays(30), - "대표 이미지" + "1일 1커밋" ); challenge = Challenge.builder() @@ -114,9 +120,9 @@ void setUp() { .contents(challengeSaveReqDto.contents()) .cycle(challengeSaveReqDto.cycle()) .cycleDetails(challengeSaveReqDto.cycleDetails()) - .startDate(challengeSaveReqDto.startDate()) + .startDate(LocalDate.now()) .endDate(challengeSaveReqDto.endDate()) - .representImage(challengeSaveReqDto.representImage()) + .representImage("대표 이미지") .member(member) .build(); @@ -126,9 +132,8 @@ void setUp() { Category.CREATIVITY_AND_ARTS, Cycle.WEEKLY, List.of(CycleDetail.MON), - LocalDate.now(), LocalDate.now().plusDays(30), - "업데이트 이미지" + "1일 1커밋" ); block = Block.builder() @@ -158,14 +163,38 @@ void setUp() { } @Test - @DisplayName("인증된 회원은 챌린지를 생성할 수 있다") - void 인증된_회원은_챌린지를_생성할_수_있다() { + @DisplayName("챌린지를 생성할 때 이미지를 업로드할 수 있다") + void 챌린지를_생성할_때_이미지를_업로드할_수_있다() { + // given + String imageUrl = "https://example.com/image.jpg"; + when(s3Service.uploadChallengeImage(any(MultipartFile.class))) + .thenReturn(imageUrl); + + // when + ChallengeInfoResDto result = challengeService.save(member.getEmail(), challengeSaveReqDto, multipartFile); + + // then + assertAll(() -> { + assertThat(result.title()).isEqualTo("1일 1커밋"); + assertThat(result.contents()).isEqualTo("1일 1커밋하기"); + assertThat(result.cycleDetails()).isEqualTo(List.of(CycleDetail.MON, CycleDetail.TUE)); + assertThat(result.startDate()).isEqualTo(LocalDate.now()); + assertThat(result.endDate()).isEqualTo(LocalDate.now().plusDays(30)); + assertThat(result.authorName()).isEqualTo("동동"); + assertThat(result.authorProfileImage()).isEqualTo("기본 프로필"); + assertThat(result.representImage()).isEqualTo(imageUrl); + }); + } + + @Test + @DisplayName("이미지 업로드 없이 챌린지를 생성할 수 있다") + void 이미지_업로드_없이_챌린지를_생성할_수_있다() { // given when(challengeRepository.save(any(Challenge.class))) .thenReturn(challenge); // when - ChallengeInfoResDto result = challengeService.save(member.getEmail(), challengeSaveReqDto); + ChallengeInfoResDto result = challengeService.save(member.getEmail(), challengeSaveReqDto, multipartFile); // then assertAll(() -> { @@ -174,7 +203,7 @@ void setUp() { assertThat(result.cycleDetails()).isEqualTo(List.of(CycleDetail.MON, CycleDetail.TUE)); assertThat(result.startDate()).isEqualTo(LocalDate.now()); assertThat(result.endDate()).isEqualTo(LocalDate.now().plusDays(30)); - assertThat(result.representImage()).isEqualTo("대표 이미지"); + assertThat(result.representImage()).isEqualTo(null); assertThat(result.authorName()).isEqualTo("동동"); assertThat(result.authorProfileImage()).isEqualTo("기본 프로필"); }); @@ -191,12 +220,12 @@ void setUp() { Cycle.MONTHLY, List.of(CycleDetail.MON, CycleDetail.TUE), LocalDate.now(), - LocalDate.now().plusDays(30), - "대표 이미지" + "1일 1커밋" ); // when & then - assertThatThrownBy(() -> challengeService.save(member.getEmail(), wrongChallengeSaveReqDto)) + assertThatThrownBy( + () -> challengeService.save(member.getEmail(), wrongChallengeSaveReqDto, any(MultipartFile.class))) .isInstanceOf(InvalidCycleException.class); } @@ -210,7 +239,7 @@ void setUp() { .thenReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> challengeService.save(errorEmail, challengeSaveReqDto)) + assertThatThrownBy(() -> challengeService.save(errorEmail, challengeSaveReqDto, multipartFile)) .isInstanceOf(MemberNotFoundException.class); } @@ -223,16 +252,14 @@ void setUp() { .thenReturn(Optional.of(challenge)); // when - ChallengeInfoResDto result = challengeService.update(member.getEmail(), challengeId, updateDto); + ChallengeInfoResDto result = challengeService.update(member.getEmail(), challengeId, updateDto, multipartFile); // then assertAll(() -> { assertThat(result.title()).isEqualTo("업데이트 제목"); assertThat(result.contents()).isEqualTo("업데이트 내용"); assertThat(result.cycleDetails()).containsExactly(CycleDetail.MON); - assertThat(result.startDate()).isEqualTo(LocalDate.now()); assertThat(result.endDate()).isEqualTo(LocalDate.now().plusDays(30)); - assertThat(result.representImage()).isEqualTo("업데이트 이미지"); }); } @@ -258,7 +285,8 @@ void setUp() { .thenReturn(Optional.of(otherMember)); // when & then - assertThatThrownBy(() -> challengeService.update("other@example.com", challengeId, updateDto)) + assertThatThrownBy( + () -> challengeService.update("other@example.com", challengeId, updateDto, any(MultipartFile.class))) .isInstanceOf(ChallengeAccessDeniedException.class); } @@ -282,38 +310,20 @@ void setUp() { }); } - @Test - @DisplayName("챌린지 목록을 검색할 수 있다") - void 챌린지_목록을_검색할_수_있다() { - // given - Pageable pageable = PageRequest.of(0, 10); - ChallengeSearchReqDto searchReqDto = ChallengeSearchReqDto.from("1일"); - Page page = new PageImpl<>(List.of(challenge), pageable, 1); - when(challengeRepository.findChallengesByKeyWord(any(ChallengeSearchReqDto.class), any(PageRequest.class))) - .thenReturn(page); - - // when - ChallengeListResDto result = challengeService.findChallengesByKeyWord(searchReqDto, pageable); - - // then - assertAll(() -> { - assertThat(result.challengeInfoResDto().size()).isEqualTo(1); - assertThat(result.pageInfoResDto().totalPages()).isEqualTo(1); - assertThat(result.pageInfoResDto().totalItems()).isEqualTo(1); - }); - } - @Test @DisplayName("챌린지를 카테고리 별로 검색할 수 있다") void 챌린지를_카테고리_별로_검색할_수_있다() { //given Pageable pageable = PageRequest.of(0, 10); Page page = new PageImpl<>(List.of(challenge), pageable, 1); - when(challengeRepository.findChallengesByCategory(anyString(), any(PageRequest.class))) + ChallengeSearchReqDto searchReqDto = ChallengeSearchReqDto.from("1일", "CREATIVITY_AND_ARTS"); + + when(challengeRepository.findChallengesByCategoryAndKeyword(any(ChallengeSearchReqDto.class), + any(PageRequest.class))) .thenReturn(page); // when - ChallengeListResDto result = challengeService.findByCategory("CREATIVITY_AND_ARTS", pageable); + ChallengeListResDto result = challengeService.findChallengesByCategoryAndKeyword(searchReqDto, pageable); // then assertAll(() -> { @@ -331,7 +341,7 @@ void setUp() { when(challengeRepository.findById(challengeId)).thenReturn(Optional.of(challenge)); // when - ChallengeInfoResDto result = challengeService.findById(challengeId); + ChallengeInfoResDto result = challengeService.findById(anyString(), challengeId); // then assertAll(() -> { @@ -381,19 +391,18 @@ void setUp() { when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(member)); when(challengeRepository.findById(anyLong())).thenReturn(Optional.of(challenge)); when(personalDashboardRepository.findById(anyLong())).thenReturn(Optional.of(personalDashboard)); - when(blockRepository.save(any(Block.class))).thenReturn(block); // when - BlockInfoResDto result = challengeService.addChallengeToPersonalDashboard(member.getEmail(), - personalDashboardId, challengeId); + challengeService.addChallengeToPersonalDashboard(member.getEmail(), personalDashboardId, challengeId); // then assertAll(() -> { - assertThat(result.title()).isEqualTo("1일 1커밋"); - assertThat(result.contents()).isEqualTo("1일 1커밋하기"); - assertThat(result.progress()).isEqualTo(Progress.NOT_STARTED); - assertThat(result.deadLine()).isEqualTo( + assertThat(block.getTitle()).isEqualTo("1일 1커밋"); + assertThat(block.getContents()).isEqualTo("1일 1커밋하기"); + assertThat(block.getProgress()).isEqualTo(Progress.NOT_STARTED); + assertThat(block.getDeadLine()).isEqualTo( LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd 23:59"))); }); } + } diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtilTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtilTest.java index 630f4995..f167b432 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtilTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/application/util/ChallengeBlockStatusUtilTest.java @@ -10,6 +10,8 @@ class ChallengeBlockStatusUtilTest { + ChallengeBlockStatusUtil challengeBlockStatusUtil = ChallengeBlockStatusUtil.getInstance(); + @Test @DisplayName("DAILY 주기는 항상 true를 반환한다") void DAILY_주기는_항상_true를_반환한다() { @@ -17,7 +19,7 @@ class ChallengeBlockStatusUtilTest { List cycleDetails = List.of(CycleDetail.DAILY); // when - Boolean result = ChallengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.DAILY, cycleDetails); + Boolean result = challengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.DAILY, cycleDetails); // then assertThat(result).isTrue(); @@ -31,7 +33,7 @@ class ChallengeBlockStatusUtilTest { CycleDetail.FRI, CycleDetail.SAT, CycleDetail.SUN); // when - Boolean result = ChallengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.WEEKLY, activeDays); + Boolean result = challengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.WEEKLY, activeDays); // then assertThat(result).isTrue(); @@ -44,7 +46,7 @@ class ChallengeBlockStatusUtilTest { List activeDays = List.of(); // when - Boolean result = ChallengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.WEEKLY, activeDays); + Boolean result = challengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.WEEKLY, activeDays); // then assertThat(result).isFalse(); @@ -68,7 +70,7 @@ class ChallengeBlockStatusUtilTest { CycleDetail.THIRTY_FIRST); // when - Boolean result = ChallengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.MONTHLY, activeDays); + Boolean result = challengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.MONTHLY, activeDays); // then assertThat(result).isTrue(); @@ -81,7 +83,7 @@ class ChallengeBlockStatusUtilTest { List activeDays = List.of(); // when - Boolean result = ChallengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.MONTHLY, activeDays); + Boolean result = challengeBlockStatusUtil.isChallengeBlockActiveToday(Cycle.MONTHLY, activeDays); // then assertThat(result).isFalse(); diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeTest.java index 8d7977e4..7f4e93b3 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/challenge/domain/ChallengeTest.java @@ -15,6 +15,7 @@ class ChallengeTest { private Challenge challenge; + private final String imageUrl = "대표 사진"; @BeforeEach void setUp() { @@ -33,11 +34,13 @@ void setUp() { .status(Status.ACTIVE) .title("제목") .contents("내용") + .cycle(Cycle.WEEKLY) .cycleDetails(List.of(CycleDetail.MON, CycleDetail.TUE)) .startDate(LocalDate.now()) .endDate(LocalDate.now().plusDays(30)) .representImage("대표 사진") .member(member) + .blockName("블록 이름") .build(); } @@ -47,23 +50,26 @@ void setUp() { // given String updateTitle = "수정된 제목"; String updateContents = "수정된 내용"; + Cycle updateCycle = Cycle.WEEKLY; List updateCycleDetails = List.of(CycleDetail.WED, CycleDetail.THU); - LocalDate updateStartDate = LocalDate.now().plusDays(1); LocalDate updateEndDate = LocalDate.now().plusDays(31); String updateRepresentImage = "수정된 대표 사진"; + String updateBlockName = "수정된 블록 이름"; // when - challenge.update(updateTitle, updateContents, updateCycleDetails, updateStartDate, updateEndDate, + challenge.update(updateTitle, updateContents, updateCycleDetails, updateEndDate, updateBlockName, updateRepresentImage); // then assertAll(() -> { assertThat(challenge.getTitle()).isEqualTo(updateTitle); assertThat(challenge.getContents()).isEqualTo(updateContents); + assertThat(challenge.getCycle()).isEqualTo(updateCycle); assertThat(challenge.getCycleDetails()).isEqualTo(updateCycleDetails); - assertThat(challenge.getStartDate()).isEqualTo(updateStartDate); assertThat(challenge.getEndDate()).isEqualTo(updateEndDate); assertThat(challenge.getRepresentImage()).isEqualTo(updateRepresentImage); + assertThat(challenge.getBlockName()).isEqualTo(updateBlockName); + assertThat(challenge.getRepresentImage()).isEqualTo(updateRepresentImage); }); } @@ -74,17 +80,17 @@ void setUp() { String updateTitle = "수정된 제목"; // when - challenge.update(updateTitle, challenge.getContents(), challenge.getCycleDetails(), challenge.getStartDate(), - challenge.getEndDate(), challenge.getRepresentImage()); + challenge.update(updateTitle, challenge.getContents(), challenge.getCycleDetails(), + challenge.getEndDate(), challenge.getBlockName(), imageUrl); // then assertAll(() -> { assertThat(challenge.getTitle()).isEqualTo(updateTitle); assertThat(challenge.getContents()).isEqualTo("내용"); assertThat(challenge.getCycleDetails()).isEqualTo(List.of(CycleDetail.MON, CycleDetail.TUE)); - assertThat(challenge.getStartDate()).isEqualTo(LocalDate.now()); assertThat(challenge.getEndDate()).isEqualTo(LocalDate.now().plusDays(30)); assertThat(challenge.getRepresentImage()).isEqualTo("대표 사진"); + assertThat(challenge.getBlockName()).isEqualTo("블록 이름"); }); } @@ -95,15 +101,14 @@ void setUp() { String updateContents = "수정된 내용"; // when - challenge.update(challenge.getTitle(), updateContents, challenge.getCycleDetails(), challenge.getStartDate(), - challenge.getEndDate(), challenge.getRepresentImage()); + challenge.update(challenge.getTitle(), updateContents, challenge.getCycleDetails(), + challenge.getEndDate(), challenge.getBlockName(), imageUrl); // then assertAll(() -> { assertThat(challenge.getTitle()).isEqualTo("제목"); assertThat(challenge.getContents()).isEqualTo(updateContents); assertThat(challenge.getCycleDetails()).isEqualTo(List.of(CycleDetail.MON, CycleDetail.TUE)); - assertThat(challenge.getStartDate()).isEqualTo(LocalDate.now()); assertThat(challenge.getEndDate()).isEqualTo(LocalDate.now().plusDays(30)); assertThat(challenge.getRepresentImage()).isEqualTo("대표 사진"); }); @@ -117,14 +122,13 @@ void setUp() { // when challenge.update(challenge.getTitle(), challenge.getContents(), challenge.getCycleDetails(), - challenge.getStartDate(), updateEndDate, challenge.getRepresentImage()); + updateEndDate, challenge.getBlockName(), imageUrl); // then assertAll(() -> { assertThat(challenge.getTitle()).isEqualTo("제목"); assertThat(challenge.getContents()).isEqualTo("내용"); assertThat(challenge.getCycleDetails()).isEqualTo(List.of(CycleDetail.MON, CycleDetail.TUE)); - assertThat(challenge.getStartDate()).isEqualTo(LocalDate.now()); assertThat(challenge.getEndDate()).isEqualTo(updateEndDate); assertThat(challenge.getRepresentImage()).isEqualTo("대표 사진"); }); @@ -149,4 +153,3 @@ void setUp() { assertThat(challenge.getStatus()).isEqualTo(Status.ACTIVE); } } - diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/common/annotation/ControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/common/annotation/ControllerTest.java index 7d6eb289..cb06e527 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/common/annotation/ControllerTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/common/annotation/ControllerTest.java @@ -20,8 +20,7 @@ import shop.kkeujeok.kkeujeokbackend.challenge.application.ChallengeService; import shop.kkeujeok.kkeujeokbackend.dashboard.personal.application.PersonalDashboardService; import shop.kkeujeok.kkeujeokbackend.dashboard.team.application.TeamDashboardService; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application.DocumentService; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application.FileService; +import shop.kkeujeok.kkeujeokbackend.global.aws.S3Service; import shop.kkeujeok.kkeujeokbackend.global.jwt.TokenProvider; import shop.kkeujeok.kkeujeokbackend.member.api.MemberControllerTest; import shop.kkeujeok.kkeujeokbackend.member.mypage.application.MyPageService; @@ -81,15 +80,12 @@ public abstract class ControllerTest { @MockBean protected MyPageService myPageService; - @MockBean - protected FileService fileService; - - @MockBean - protected DocumentService documentService; - @MockBean protected NotificationService notificationService; @MockBean protected SseEmitterManager sseEmitterManager; + + @MockBean + S3Service s3Service; } diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardServiceTest.java index 35c0b859..f62ae5a0 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardServiceTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/team/application/TeamDashboardServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; @@ -29,6 +30,8 @@ import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.repository.TeamDashboardRepository; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.exception.AlreadyJoinedTeamException; +import shop.kkeujeok.kkeujeokbackend.dashboard.team.exception.NotMemberOfTeamException; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; @@ -240,6 +243,20 @@ void setUp() { }); } + @DisplayName("이미 팀에 참여한 멤버가 다시 참여할 경우 예외가 발생합니다") + @Test + void 팀_대시보드_중복_참여_예외() { + // given + Long dashboardId = 1L; + when(teamDashboardRepository.findById(dashboardId)).thenReturn(Optional.of(teamDashboard)); + + teamDashboardService.joinTeam(member.getEmail(), dashboardId); + + // when & then + assertThatThrownBy(() -> teamDashboardService.joinTeam(member.getEmail(), dashboardId)) + .isInstanceOf(AlreadyJoinedTeamException.class); + } + @DisplayName("팀 대시보드를 탈퇴합니다.") @Test void 팀_대시보드_탈퇴() { @@ -261,6 +278,18 @@ void setUp() { }); } + @DisplayName("참여하지 않은 팀에서 탈퇴를 시도할 때 예외가 발생합니다.") + @Test + void 참여하지_않은_팀에서_탈퇴_시도() { + // given + Long dashboardId = 1L; + when(teamDashboardRepository.findById(dashboardId)).thenReturn(Optional.of(teamDashboard)); + + // when & then + assertThatThrownBy(() -> teamDashboardService.leaveTeam(member.getEmail(), dashboardId)) + .isInstanceOf(NotMemberOfTeamException.class); + } + @DisplayName("팀원 초대 리스트를 조회합니다.") @Test void 팀_초대_멤버_조회() { diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentControllerTest.java deleted file mode 100644 index 73ddce11..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/DocumentControllerTest.java +++ /dev/null @@ -1,212 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.requestFields; -import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.responseFields; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import shop.kkeujeok.kkeujeokbackend.common.annotation.ControllerTest; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentUpdateReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.global.annotationresolver.CurrentUserEmailArgumentResolver; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; -import shop.kkeujeok.kkeujeokbackend.global.error.ControllerAdvice; - -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; - -import java.util.Collections; -import java.util.List; - -class DocumentControllerTest extends ControllerTest { - - @InjectMocks - DocumentController documentController; - - private Document document; - - @BeforeEach - void setUp(RestDocumentationContextProvider restDocumentation) { - documentController = new DocumentController(documentService); - - mockMvc = MockMvcBuilders.standaloneSetup(documentController) - .apply(documentationConfiguration(restDocumentation)) - .setCustomArgumentResolvers(new CurrentUserEmailArgumentResolver(tokenProvider)) - .setControllerAdvice(new ControllerAdvice()) - .build(); - - document = Document.builder() - .title("DocumentTitle") - .build(); - - ReflectionTestUtils.setField(document, "id", 1L); - } - - @DisplayName("POST 팀 문서 저장 컨트롤러 로직 확인") - @Test - void POST_팀_문서_저장_컨트롤러_로직_확인() throws Exception { - // given - DocumentInfoReqDto request = new DocumentInfoReqDto(1L, "DocumentTitle"); - DocumentInfoResDto response = new DocumentInfoResDto(1L, "DocumentTitle"); - - given(documentService.save(any(DocumentInfoReqDto.class))).willReturn(response); - - // when & then - mockMvc.perform(post("/api/documents") - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andDo(document("document/save", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName("Authorization").description("JWT 토큰") - ), - requestFields( - fieldWithPath("teamDashboardId").description("팀 대시보드 ID"), - fieldWithPath("title").description("문서 제목") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.documentId").description("문서 ID"), - fieldWithPath("data.title").description("문서 제목") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("PATCH 팀 문서 수정 컨트롤러 로직 확인") - @Test - void PATCH_팀_문서_수정_컨트롤러_로직_확인() throws Exception { - // given - Long documentId = 1L; - DocumentUpdateReqDto request = new DocumentUpdateReqDto("Updated Document Title"); - DocumentInfoResDto response = new DocumentInfoResDto(documentId, "Updated Document Title"); - - given(documentService.update(anyLong(), any(DocumentUpdateReqDto.class))).willReturn(response); - - // when & then - mockMvc.perform(patch("/api/documents/{documentId}", documentId) - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andDo(document("document/update", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - pathParameters( - parameterWithName("documentId").description("문서 ID") - ), - requestFields( - fieldWithPath("title").description("문서 제목") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.documentId").description("문서 ID"), - fieldWithPath("data.title").description("문서 제목") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("GET 팀 문서 리스트 조회 컨트롤러 로직 확인") - @Test - void GET_팀_문서_리스트_조회_컨트롤러_로직_확인() throws Exception { - // given - PageRequest pageRequest = PageRequest.of(0, 10); - Page documentPage = new PageImpl<>(List.of(document)); - - DocumentListResDto response = new DocumentListResDto( - Collections.singletonList(new DocumentInfoResDto(1L, "DocumentTitle")), - PageInfoResDto.from(documentPage) - ); - - given(documentService.findDocumentByTeamDashboardId(anyLong(), any(PageRequest.class))).willReturn(response); - - // when & then - mockMvc.perform(get("/api/documents") - .param("teamDashboardId", "1") - .param("page", "0") - .param("size", "10") - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON)) - .andDo(print()) - .andDo(document("document/findForDocumentByTeamDashboardId", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - queryParameters( - parameterWithName("teamDashboardId").description("팀 대시보드 ID"), - parameterWithName("page").description("페이지 번호"), - parameterWithName("size").description("페이지 크기") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.documentInfoResDtos[].documentId").description("문서 ID"), - fieldWithPath("data.documentInfoResDtos[].title").description("문서 제목"), - fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지 번호"), - fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지 수"), - fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템 수") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("DELETE 팀 문서 삭제 컨트롤러 로직 확인") - @Test - void DELETE_팀_문서_삭제_컨트롤러_로직_확인() throws Exception { - // given - Long documentId = 1L; - doNothing().when(documentService).delete(anyLong()); - - // when & then - mockMvc.perform(delete("/api/documents/{documentId}", documentId) - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON)) - .andDo(print()) - .andDo(document("document/delete", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - pathParameters( - parameterWithName("documentId").description("문서 ID") - ) - )) - .andExpect(status().isOk()); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileControllerTest.java deleted file mode 100644 index 067fb75c..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/api/FileControllerTest.java +++ /dev/null @@ -1,268 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.requestFields; -import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.responseFields; - -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.MockitoAnnotations; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.http.MediaType; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import shop.kkeujeok.kkeujeokbackend.common.annotation.ControllerTest; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.FileInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application.FileService; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; -import shop.kkeujeok.kkeujeokbackend.global.annotationresolver.CurrentUserEmailArgumentResolver; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; -import shop.kkeujeok.kkeujeokbackend.global.error.ControllerAdvice; - -class FileControllerTest extends ControllerTest{ - - @InjectMocks - FileController fileController; - private Document document; - private File file; - - @BeforeEach - void setUp(RestDocumentationContextProvider restDocumentation) { - fileController = new FileController(fileService); - - mockMvc = MockMvcBuilders.standaloneSetup(fileController) - .apply(documentationConfiguration(restDocumentation)) - .setCustomArgumentResolvers(new CurrentUserEmailArgumentResolver(tokenProvider)) - .setControllerAdvice(new ControllerAdvice()) - .build(); - - document = Document.builder() - .title("DocumentTitle") - .build(); - - ReflectionTestUtils.setField(document, "id", 1L); - - file = File.builder() - .email("email") - .title("FileTitle") - .content("FileContent") - .document(document) - .build(); - - when(tokenProvider.getUserEmailFromToken(any())).thenReturn("email"); - } - - @DisplayName("POST 파일 저장 컨트롤러 로직 확인") - @Test - void POST_파일_저장_컨트롤러_로직_확인() throws Exception { - // given - FileInfoReqDto request = new FileInfoReqDto(1L, "email", "title", "content"); - FileInfoResDto response = new FileInfoResDto(1L, "title", "content", "email"); - - given(fileService.save(anyString(), any(FileInfoReqDto.class))).willReturn(response); - - // when & then - mockMvc.perform(post("/api/files/") - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andDo(document("file/save", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - requestHeaders( - headerWithName("Authorization").description("JWT 토큰") - ), - requestFields( - fieldWithPath("documentId").description("문서 ID"), - fieldWithPath("email").description("파일 생성자 이메일"), - fieldWithPath("title").description("파일 제목"), - fieldWithPath("content").description("파일 내용") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.fileId").description("파일 ID"), - fieldWithPath("data.title").description("파일 제목"), - fieldWithPath("data.content").description("파일 내용"), - fieldWithPath("data.email").description("파일 생성자 이메일") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("PATCH 파일 수정 컨트롤러 로직 확인") - @Test - void PATCH_파일_수정_컨트롤러_로직_확인() throws Exception { - // given - Long fileId = 1L; - FileInfoReqDto request = new FileInfoReqDto(1L, "email", "updatedTitle", "updatedContent"); - FileInfoResDto response = new FileInfoResDto(fileId, "updatedTitle", "updatedContent", "email"); - - given(fileService.update(anyLong(), any(FileInfoReqDto.class))).willReturn(response); - - // when & then - mockMvc.perform(patch("/api/files/{fileId}", fileId) - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andDo(print()) - .andDo(document("file/update", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - pathParameters( - parameterWithName("fileId").description("파일 ID") - ), - requestFields( - fieldWithPath("documentId").description("문서 ID"), - fieldWithPath("email").description("파일 생성자 이메일"), - fieldWithPath("title").description("파일 제목"), - fieldWithPath("content").description("파일 내용") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.fileId").description("파일 ID"), - fieldWithPath("data.title").description("파일 제목"), - fieldWithPath("data.content").description("파일 내용"), - fieldWithPath("data.email").description("파일 생성자 이메일") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("GET 파일 리스트 조회 컨트롤러 로직 확인") - @Test - void GET_파일_리스트_조회_컨트롤러_로직_확인() throws Exception { - // given - PageRequest pageRequest = PageRequest.of(0, 10); - Page filePage = new PageImpl<>(List.of(file)); - - FileListResDto response = new FileListResDto( - Collections.singletonList(new FileInfoResDto(1L, "title", "content", "email")), - PageInfoResDto.from(filePage) - ); - - given(fileService.findForFile(anyLong(), any(PageRequest.class))).willReturn(response); - - // when & then - mockMvc.perform(get("/api/files") - .param("documentId", "1") - .param("page", "0") - .param("size", "10") - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON)) - .andDo(print()) - .andDo(document("file/findForFile", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - queryParameters( - parameterWithName("documentId").description("문서 ID"), - parameterWithName("page").description("페이지 번호"), - parameterWithName("size").description("페이지 크기") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.fileInfoResDto[].fileId").description("파일 ID"), - fieldWithPath("data.fileInfoResDto[].title").description("파일 제목"), - fieldWithPath("data.fileInfoResDto[].content").description("파일 내용"), - fieldWithPath("data.fileInfoResDto[].email").description("파일 생성자 이메일"), - fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지 번호"), - fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지 수"), - fieldWithPath("data.pageInfoResDto.totalItems").description("전체 아이템 수") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("GET 파일 상세보기 컨트롤러 로직 확인") - @Test - void GET_파일_상세보기_컨트롤러_로직_확인() throws Exception { - // given - Long fileId = 1L; - FileInfoResDto response = new FileInfoResDto(fileId, "title", "content", "email"); - - given(fileService.findById(anyLong())).willReturn(response); - - // when & then - mockMvc.perform(get("/api/files/{fileId}", fileId) - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON)) - .andDo(print()) - .andDo(document("file/findById", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - pathParameters( - parameterWithName("fileId").description("파일 ID") - ), - responseFields( - fieldWithPath("statusCode").description("상태 코드"), - fieldWithPath("message").description("응답 메시지"), - fieldWithPath("data.fileId").description("파일 ID"), - fieldWithPath("data.title").description("파일 제목"), - fieldWithPath("data.content").description("파일 내용"), - fieldWithPath("data.email").description("파일 생성자 이메일") - ) - )) - .andExpect(status().isOk()); - } - - @DisplayName("DELETE 파일 삭제 컨트롤러 로직 확인") - @Test - void DELETE_파일_삭제_컨트롤러_로직_확인() throws Exception { - // given - Long fileId = 1L; - doNothing().when(fileService).delete(anyLong()); - - // when & then - mockMvc.perform(delete("/api/files/{fileId}", fileId) - .header("Authorization", "Bearer valid-token") - .accept(MediaType.APPLICATION_JSON)) - .andDo(print()) - .andDo(document("file/delete", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint()), - pathParameters( - parameterWithName("fileId").description("파일 ID") - ) - )) - .andExpect(status().isOk()); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentServiceTest.java deleted file mode 100644 index 9b5f78a4..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/DocumentServiceTest.java +++ /dev/null @@ -1,180 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.test.util.ReflectionTestUtils; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.repository.TeamDashboardRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.DocumentUpdateReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.DocumentListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository.DocumentRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.DocumentNotFoundException; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.TeamDashboardNotFoundException; - -import java.util.List; -import java.util.Optional; - -class DocumentServiceTest { - - @Mock - private DocumentRepository documentRepository; - - @Mock - private TeamDashboardRepository teamDashboardRepository; - - @InjectMocks - private DocumentService documentService; - - private Document document; - private TeamDashboard teamDashboard; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - - teamDashboard = TeamDashboard.builder() - .title("Team Dashboard Title") - .description("Team Dashboard Description") - .build(); - - ReflectionTestUtils.setField(teamDashboard, "id", 1L); - - document = Document.builder() - .title("Document Title") - .teamDashboard(teamDashboard) - .build(); - - ReflectionTestUtils.setField(document, "id", 1L); - } - - @DisplayName("문서를 생성합니다.") - @Test - void 문서를_생성합니다() { - // given - DocumentInfoReqDto documentInfoReqDto = new DocumentInfoReqDto(1L, "Document Title"); - - when(teamDashboardRepository.findById(anyLong())).thenReturn(Optional.of(teamDashboard)); - when(documentRepository.save(any(Document.class))).thenReturn(document); - - // when - DocumentInfoResDto result = documentService.save(documentInfoReqDto); - - // then - assertThat(result.title()).isEqualTo(document.getTitle()); - - verify(teamDashboardRepository).findById(documentInfoReqDto.teamDashboardId()); - verify(documentRepository).save(any(Document.class)); - } - - @DisplayName("존재하지 않는 팀 대시보드에 문서를 생성 시 예외가 발생합니다.") - @Test - void 존재하지_않는_팀_대시보드에_문서를_생성_시_예외가_발생합니다() { - // given - DocumentInfoReqDto documentInfoReqDto = new DocumentInfoReqDto(999L, "Document Title"); - - when(teamDashboardRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> documentService.save(documentInfoReqDto)) - .isInstanceOf(TeamDashboardNotFoundException.class); - - verify(teamDashboardRepository).findById(documentInfoReqDto.teamDashboardId()); - verify(documentRepository, never()).save(any(Document.class)); - } - - @DisplayName("문서를 성공적으로 수정합니다.") - @Test - void 문서를_성공적으로_수정합니다() { - // given - DocumentUpdateReqDto documentUpdateReqDto = new DocumentUpdateReqDto("Updated Document Title"); - - when(documentRepository.findById(anyLong())).thenReturn(Optional.of(document)); - - // when - DocumentInfoResDto result = documentService.update(1L, documentUpdateReqDto); - - // then - assertThat(result.title()).isEqualTo(documentUpdateReqDto.title()); - - verify(documentRepository).findById(1L); - } - - @DisplayName("존재하지 않는 문서를 수정 시 예외가 발생합니다.") - @Test - void 존재하지_않는_문서를_수정_시_예외가_발생합니다() { - // given - DocumentUpdateReqDto documentUpdateReqDto = new DocumentUpdateReqDto("Updated Document Title"); - - when(documentRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> documentService.update(1L, documentUpdateReqDto)) - .isInstanceOf(DocumentNotFoundException.class); - - verify(documentRepository).findById(1L); - } - - @DisplayName("팀 대시보드 ID로 문서를 성공적으로 조회합니다.") - @Test - void 팀_대시보드_ID로_문서를_성공적으로_조회합니다() { - // given - Pageable pageable = PageRequest.of(0, 10); - Page documents = new PageImpl<>(List.of(document)); - - when(documentRepository.findByDocumentWithTeamDashboard(anyLong(), any(Pageable.class))).thenReturn(documents); - - // when - DocumentListResDto result = documentService.findDocumentByTeamDashboardId(1L, pageable); - - // then - assertThat(result.documentInfoResDtos().size()).isEqualTo(1); - assertThat(result.documentInfoResDtos().get(0).title()).isEqualTo(document.getTitle()); - - verify(documentRepository).findByDocumentWithTeamDashboard(1L, pageable); - } - - @DisplayName("문서를 논리적으로 삭제합니다.") - @Test - void 문서를_논리적으로_삭제합니다() { - // given - when(documentRepository.findById(anyLong())).thenReturn(Optional.of(document)); - - // when - documentService.delete(1L); - - // then - assertThat(document.getStatus()).isEqualTo(shop.kkeujeok.kkeujeokbackend.global.entity.Status.DELETED); - - verify(documentRepository).findById(1L); - } - - @DisplayName("존재하지 않는 문서를 삭제 시 예외가 발생합니다.") - @Test - void 존재하지_않는_문서를_삭제_시_예외가_발생합니다() { - // given - when(documentRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> documentService.delete(1L)) - .isInstanceOf(DocumentNotFoundException.class); - - verify(documentRepository).findById(1L); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileServiceTest.java deleted file mode 100644 index 933af8dd..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/application/FileServiceTest.java +++ /dev/null @@ -1,210 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.application; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.test.util.ReflectionTestUtils; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.request.FileInfoReqDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileInfoResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.api.dto.response.FileListResDto; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository.DocumentRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository.FileRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.DocumentNotFoundException; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.exception.FileNotFoundException; - -import java.util.List; -import java.util.Optional; - -class FileServiceTest { - - @Mock - private DocumentRepository documentRepository; - - @Mock - private FileRepository fileRepository; - - @InjectMocks - private FileService fileService; - - private File file; - - private Document document; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - - document = Document.builder() - .title("DocumentTitle") - .build(); - - ReflectionTestUtils.setField(document, "id", 1L); - - file = File.builder() - .email("email") - .title("FileTitle") - .content("FileContent") - .document(document) - .build(); - } - - @DisplayName("파일을 생성합니다.") - @Test - void 파일을_생성합니다() { - // given - FileInfoReqDto fileInfoReqDto = new FileInfoReqDto(document.getId(), "email", "FileTitle", "FileContent"); - - when(documentRepository.findById(anyLong())).thenReturn(Optional.of(document)); - when(fileRepository.save(any(File.class))).thenReturn(file); - - // when - FileInfoResDto result = fileService.save("New email", fileInfoReqDto); - - // then - assertThat(result.title()).isEqualTo(file.getTitle()); - assertThat(result.content()).isEqualTo(file.getContent()); - - verify(documentRepository).findById(fileInfoReqDto.documentId()); - verify(fileRepository).save(any(File.class)); - } - - @DisplayName("존재하지 않는 문서에 파일 생성 시 예외가 발생합니다.") - @Test - void 존재하지_않는_문서에_파일_생성_시_예외가_발생합니다() { - // given - FileInfoReqDto fileInfoReqDto = new FileInfoReqDto(999L, "email", "New File", "New Content"); - - when(documentRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> fileService.save("noExistEmail", fileInfoReqDto)) - .isInstanceOf(DocumentNotFoundException.class); - - verify(documentRepository).findById(fileInfoReqDto.documentId()); - verify(fileRepository, never()).save(any(File.class)); - } - - @DisplayName("파일을 성공적으로 수정합니다.") - @Test - void 파일을_성공적으로_수정합니다() { - // given - FileInfoReqDto fileInfoReqDto = new FileInfoReqDto(document.getId(), "email", "Updated Title", "Updated Content"); - - when(fileRepository.findById(anyLong())).thenReturn(Optional.of(file)); - - // when - FileInfoResDto result = fileService.update(1L, fileInfoReqDto); - - // then - assertThat(result.title()).isEqualTo(fileInfoReqDto.title()); - assertThat(result.content()).isEqualTo(fileInfoReqDto.content()); - - verify(fileRepository).findById(1L); - } - - @DisplayName("존재하지 않는 파일 수정 시 예외가 발생합니다.") - @Test - void 존재하지_않는_파일_수정_시_예외가_발생합니다() { - // given - FileInfoReqDto fileInfoReqDto = new FileInfoReqDto(document.getId(), "email", "Updated Title", "Updated Content"); - - when(fileRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> fileService.update(1L, fileInfoReqDto)) - .isInstanceOf(FileNotFoundException.class); - - verify(fileRepository).findById(1L); - } - - @DisplayName("파일 리스트를 성공적으로 조회합니다.") - @Test - void 파일_리스트를_성공적으로_조회합니다() { - // given - Pageable pageable = PageRequest.of(0, 10); - Page files = new PageImpl<>(List.of(file)); - - when(fileRepository.findByFilesWithDocumentId(anyLong(), any(Pageable.class))).thenReturn(files); - - // when - FileListResDto result = fileService.findForFile(1L, pageable); - - // then - assertThat(result.fileInfoResDto().size()).isEqualTo(1); - assertThat(result.fileInfoResDto().get(0).title()).isEqualTo(file.getTitle()); - - verify(fileRepository).findByFilesWithDocumentId(1L, pageable); - } - - @DisplayName("파일을 성공적으로 조회합니다.") - @Test - void 파일을_성공적으로_조회합니다() { - // given - when(fileRepository.findById(anyLong())).thenReturn(Optional.of(file)); - - // when - FileInfoResDto result = fileService.findById(1L); - - // then - assertThat(result.title()).isEqualTo(file.getTitle()); - assertThat(result.content()).isEqualTo(file.getContent()); - - verify(fileRepository).findById(1L); - } - - @DisplayName("존재하지 않는 파일 조회 시 예외가 발생합니다.") - @Test - void 존재하지_않는_파일_조회_시_예외가_발생합니다() { - // given - when(fileRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> fileService.findById(1L)) - .isInstanceOf(FileNotFoundException.class); - - verify(fileRepository).findById(1L); - } - - @DisplayName("파일을 논리적으로 삭제합니다.") - @Test - void 파일을_논리적으로_삭제합니다() { - // given - when(fileRepository.findById(anyLong())).thenReturn(Optional.of(file)); - - // when - fileService.delete(1L); - - // then - assertThat(file.getStatus()).isEqualTo(shop.kkeujeok.kkeujeokbackend.global.entity.Status.DELETED); - - verify(fileRepository).findById(1L); - } - - @DisplayName("존재하지 않는 파일 삭제 시 예외가 발생합니다.") - @Test - void 존재하지_않는_파일_삭제_시_예외가_발생합니다() { - // given - when(fileRepository.findById(anyLong())).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> fileService.delete(1L)) - .isInstanceOf(FileNotFoundException.class); - - verify(fileRepository).findById(1L); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/DocumentTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/DocumentTest.java deleted file mode 100644 index 39f26f64..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/DocumentTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; - -class DocumentTest { - - private TeamDashboard teamDashboard; - private Document document; - - @BeforeEach - void setUp() { - teamDashboard = TeamDashboard.builder() - .title("teamDashboardTitle") - .description("teamDashboardDescription") - .build(); - - document = Document.builder() - .title("documentTitle") - .teamDashboard(teamDashboard) - .build(); - } - - @DisplayName("문서의 모든 값을 수정합니다.") - @Test - void 문서의_모든_값을_수정합니다() { - // given - String updateTitle = "Updated Document Title"; - - // when - document.update(updateTitle); - - // then - assertThat(document.getTitle()).isEqualTo(updateTitle); - } - - @DisplayName("문서의 논리 삭제 상태를 수정합니다.") - @Test - void 문서의_논리_삭제_상태를_수정합니다() { - // given - assertThat(document.getStatus()).isEqualTo(Status.ACTIVE); - - // when - document.statusUpdate(); - - // then - assertThat(document.getStatus()).isEqualTo(Status.DELETED); - - // when - document.statusUpdate(); - - // then - assertThat(document.getStatus()).isEqualTo(Status.ACTIVE); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/FileTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/FileTest.java deleted file mode 100644 index ab29b51b..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/FileTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import shop.kkeujeok.kkeujeokbackend.global.entity.Status; - -class FileTest { - - private Document document; - private File file; - - @BeforeEach - void setUp() { - document = Document.builder() - .title("documentTitle") - .build(); - - file = File.builder() - .email("email") - .title("fileTitle") - .content("content") - .document(document) - .build(); - } - - @DisplayName("파일의 모든 값을 수정합니다.") - @Test - void 파일_수정() { - // given - String updateTitle = "Updated Title"; - String updateContent = "Updated Content"; - - // when - file.update(updateTitle, updateContent); - - // then - assertAll(() -> { - assertThat(file.getTitle()).isEqualTo(updateTitle); - assertThat(file.getContent()).isEqualTo(updateContent); - }); - } - - @DisplayName("파일의 논리 삭제 상태를 수정합니다.") - @Test - void 파일의_논리_삭제_상태를_수정합니다() { - // given - assertThat(file.getStatus()).isEqualTo(Status.ACTIVE); - - // when - file.statusUpdate(); - - // then - assertThat(file.getStatus()).isEqualTo(Status.DELETED); - - // when - file.statusUpdate(); - - // then - assertThat(file.getStatus()).isEqualTo(Status.ACTIVE); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepositoryTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepositoryTest.java deleted file mode 100644 index cf984f20..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/DocumentRepositoryTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -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; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.test.context.ActiveProfiles; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.TeamDashboard; -import shop.kkeujeok.kkeujeokbackend.dashboard.team.domain.repository.TeamDashboardRepository; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.global.config.JpaAuditingConfig; -import shop.kkeujeok.kkeujeokbackend.global.config.QuerydslConfig; - -@DataJpaTest -@Import({JpaAuditingConfig.class, QuerydslConfig.class}) -@ActiveProfiles("test") -class DocumentRepositoryTest { - - @Autowired - private DocumentRepository documentRepository; - - @Autowired - private TeamDashboardRepository teamDashboardRepository; - - private TeamDashboard teamDashboard; - private Document document1; - private Document document2; - private Document document3; - - @BeforeEach - void setUp() { - teamDashboard = TeamDashboard.builder() - .title("title") - .description("description") - .build(); - - document1 = Document.builder() - .title("Document Title 1") - .teamDashboard(teamDashboard) - .build(); - - document2 = Document.builder() - .title("Document Title 2") - .teamDashboard(teamDashboard) - .build(); - - document3 = Document.builder() - .title("Document Title 3") - .teamDashboard(teamDashboard) - .build(); - - teamDashboardRepository.save(teamDashboard); - documentRepository.save(document1); - documentRepository.save(document2); - documentRepository.save(document3); - } - - @DisplayName("팀 대시보드 ID로 문서를 전체 조회합니다.") - @Test - void 팀_대시보드_ID로_문서를_전체_조회합니다() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - Page documents = documentRepository.findByDocumentWithTeamDashboard(teamDashboard.getId(), pageable); - - // then - assertThat(documents.getContent().size()).isEqualTo(3); - assertThat(documents.getContent()).extracting("teamDashboard.Id").containsOnly(teamDashboard.getId()); - } - - @DisplayName("문서를 논리 삭제 상태별로 전체 조회합니다.") - @Test - void 문서를_논리_삭제_상태별로_전체_조회합니다() { - // given - Pageable pageable = PageRequest.of(0, 10); - document1.statusUpdate(); - - // when - Page documents = documentRepository.findByDocumentWithTeamDashboard(teamDashboard.getId(), pageable); - - // then - assertThat(documents.getContent().size()).isEqualTo(2); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepositoryTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepositoryTest.java deleted file mode 100644 index ccfc535b..00000000 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/dashboard/teamdocument/domain/repository/FileRepositoryTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -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; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.test.context.ActiveProfiles; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.Document; -import shop.kkeujeok.kkeujeokbackend.dashboard.teamdocument.domain.File; -import shop.kkeujeok.kkeujeokbackend.global.config.JpaAuditingConfig; -import shop.kkeujeok.kkeujeokbackend.global.config.QuerydslConfig; - -@DataJpaTest -@Import({JpaAuditingConfig.class, QuerydslConfig.class}) -@ActiveProfiles("test") -class FileRepositoryTest { - - @Autowired - private FileRepository fileRepository; - - @Autowired - private DocumentRepository documentRepository; - - private Document document; - private File file1; - private File file2; - private File file3; - - @BeforeEach - void setUp() { - document = Document.builder() - .title("Document Title") - .build(); - - file1 = File.builder() - .email("email1") - .title("title1") - .content("content1") - .document(document) - .build(); - - file2 = File.builder() - .email("email2") - .title("title2") - .content("content2") - .document(document) - .build(); - - file3 = File.builder() - .email("email3") - .title("title3") - .content("content3") - .document(document) - .build(); - - documentRepository.save(document); - fileRepository.save(file1); - fileRepository.save(file2); - fileRepository.save(file3); - } - - @DisplayName("Document ID로 파일을 전체 조회합니다.") - @Test - void Document_ID로_파일을_전체_조회합니다() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - Page files = fileRepository.findByFilesWithDocumentId(document.getId(), pageable); - - // then - assertThat(files.getContent().size()).isEqualTo(3); - assertThat(files.getContent()).extracting("document.id").containsOnly(document.getId()); - } - - @DisplayName("파일을 논리 삭제 상태별로 전체 조회합니다.") - @Test - void 파일을_논리_삭제_상태별로_전체_조회합니다() { - // given - Pageable pageable = PageRequest.of(0, 10); - file1.statusUpdate(); - - // when - Page files = fileRepository.findByFilesWithDocumentId(document.getId(), pageable); - - // then - assertThat(files.getContent().size()).isEqualTo(2); - } -} diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberControllerTest.java index 8d391b41..20fcb1ff 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberControllerTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/member/api/MemberControllerTest.java @@ -5,14 +5,12 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; -import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeListResDto; import shop.kkeujeok.kkeujeokbackend.common.annotation.ControllerTest; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.api.dto.response.PersonalDashboardPageListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardListResDto; import shop.kkeujeok.kkeujeokbackend.global.annotationresolver.CurrentUserEmailArgumentResolver; import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.TokenReqDto; @@ -21,16 +19,13 @@ import shop.kkeujeok.kkeujeokbackend.member.domain.Member; import shop.kkeujeok.kkeujeokbackend.member.domain.Role; import shop.kkeujeok.kkeujeokbackend.member.domain.SocialType; -import shop.kkeujeok.kkeujeokbackend.member.mypage.api.dto.request.MyPageUpdateReqDto; import shop.kkeujeok.kkeujeokbackend.member.mypage.api.dto.response.MyPageInfoResDto; import shop.kkeujeok.kkeujeokbackend.member.mypage.api.dto.response.TeamDashboardsAndChallengesResDto; import java.util.ArrayList; -import java.util.Collections; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.when; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; @@ -49,7 +44,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.hamcrest.Matchers.is; -import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.requestFields; import static shop.kkeujeok.kkeujeokbackend.global.restdocs.RestDocsHandler.responseFields; public class MemberControllerTest extends ControllerTest { @@ -121,19 +115,24 @@ void setUp(RestDocumentationContextProvider restDocumentation) { .andExpect(jsonPath("$.data").exists()); } - @DisplayName("팀 대시보드와 챌린지 정보를 가져옵니다.") + @DisplayName("대시보드와 챌린지 정보를 가져옵니다.") @Test void 팀_대시보드와_챌린지_정보를_가져옵니다() throws Exception { + // Given TeamDashboardsAndChallengesResDto resDto = new TeamDashboardsAndChallengesResDto( + new PersonalDashboardPageListResDto(new ArrayList<>(), new PageInfoResDto(0, 0, 0)), new TeamDashboardListResDto(new ArrayList<>(), new PageInfoResDto(0, 0, 0)), new ChallengeListResDto(new ArrayList<>(), new PageInfoResDto(0, 0, 0)) ); - when(myPageService.findTeamDashboardsAndChallenges(anyString(), any())).thenReturn(resDto); - when(tokenProvider.getUserEmailFromToken(any(TokenReqDto.class))).thenReturn("email"); + when(myPageService.findTeamDashboardsAndChallenges(anyString(), anyString(), any(PageRequest.class))) + .thenReturn(resDto); + + when(tokenProvider.getUserEmailFromToken(any(TokenReqDto.class))).thenReturn("test@example.com"); mockMvc.perform(get("/api/members/mypage/dashboard-challenges") .header("Authorization", "Bearer valid-token") + .param("requestEmail", "request@example.com") .param("page", "0") .param("size", "10")) .andDo(print()) @@ -144,12 +143,17 @@ void setUp(RestDocumentationContextProvider restDocumentation) { headerWithName("Authorization").description("JWT 토큰") ), queryParameters( + parameterWithName("requestEmail").description("조회할 사용자의 이메일"), parameterWithName("page").description("페이지 번호 (기본값: 0)"), parameterWithName("size").description("페이지 당 항목 수 (기본값: 10)") ), responseFields( fieldWithPath("statusCode").description("상태 코드"), fieldWithPath("message").description("응답 메시지"), + fieldWithPath("data.personalDashboardList.personalDashboardInfoResDto").description("개인 대시보드 정보 목록"), + fieldWithPath("data.personalDashboardList.pageInfoResDto.currentPage").description("현재 페이지 번호"), + fieldWithPath("data.personalDashboardList.pageInfoResDto.totalPages").description("총 페이지 수"), + fieldWithPath("data.personalDashboardList.pageInfoResDto.totalItems").description("총 항목 수"), fieldWithPath("data.teamDashboardList.teamDashboardInfoResDto").description("팀 대시보드 정보 목록"), fieldWithPath("data.teamDashboardList.pageInfoResDto.currentPage").description("현재 페이지 번호"), fieldWithPath("data.teamDashboardList.pageInfoResDto.totalPages").description("총 페이지 수"), @@ -161,7 +165,7 @@ void setUp(RestDocumentationContextProvider restDocumentation) { ) )) .andExpect(status().isOk()) - .andExpect(jsonPath("$.message", is("팀 대시보드와 챌린지 정보 조회"))) + .andExpect(jsonPath("$.message", is("대시보드와 챌린지 정보 조회"))) .andExpect(jsonPath("$.data").exists()); } diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageServiceTest.java index d81ba853..d07beec3 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageServiceTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/member/mypage/application/MyPageServiceTest.java @@ -11,6 +11,7 @@ import org.springframework.data.domain.Pageable; import shop.kkeujeok.kkeujeokbackend.challenge.api.dto.response.ChallengeListResDto; import shop.kkeujeok.kkeujeokbackend.challenge.application.ChallengeService; +import shop.kkeujeok.kkeujeokbackend.dashboard.personal.application.PersonalDashboardService; import shop.kkeujeok.kkeujeokbackend.dashboard.team.api.dto.response.TeamDashboardListResDto; import shop.kkeujeok.kkeujeokbackend.dashboard.team.application.TeamDashboardService; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -23,12 +24,10 @@ import java.util.Collections; import java.util.Optional; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.times; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class MyPageServiceTest { @@ -36,6 +35,9 @@ public class MyPageServiceTest { @Mock private MemberRepository memberRepository; + @Mock + private PersonalDashboardService personalDashboardService; + @Mock private TeamDashboardService teamDashboardService; @@ -123,7 +125,7 @@ void setUp() { when(challengeService.findChallengeForMemberId(email, pageable)).thenReturn(challengeListResDto); // When - TeamDashboardsAndChallengesResDto result = myPageService.findTeamDashboardsAndChallenges(email, pageable); + TeamDashboardsAndChallengesResDto result = myPageService.findTeamDashboardsAndChallenges(email,"test@example.com", pageable); // Then assertEquals(teamDashboardListResDto, result.teamDashboardList()); diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationControllerTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationControllerTest.java index 14db02f5..c04e78eb 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationControllerTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/api/NotificationControllerTest.java @@ -2,12 +2,14 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.Mockito.when; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; @@ -23,18 +25,15 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import shop.kkeujeok.kkeujeokbackend.auth.api.dto.request.TokenReqDto; import shop.kkeujeok.kkeujeokbackend.common.annotation.ControllerTest; import shop.kkeujeok.kkeujeokbackend.global.annotationresolver.CurrentUserEmailArgumentResolver; -import shop.kkeujeok.kkeujeokbackend.global.dto.PageInfoResDto; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.global.error.ControllerAdvice; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -80,6 +79,8 @@ void setUp(RestDocumentationContextProvider restDocumentation) { .isRead(false) .build(); + ReflectionTestUtils.setField(notification, "id", 1L); + mockMvc = MockMvcBuilders.standaloneSetup(notificationController) .apply(documentationConfiguration(restDocumentation)) .setCustomArgumentResolvers(new CurrentUserEmailArgumentResolver(tokenProvider)) @@ -115,11 +116,9 @@ void setUp(RestDocumentationContextProvider restDocumentation) { void 알림_전체_조회에_성공하면_상태코드_200_반환() throws Exception { // given NotificationInfoResDto notificationInfoResDto = NotificationInfoResDto.from(notification); - Page notificationPage = new PageImpl<>(List.of(notification), PageRequest.of(0, 10), 1); - NotificationListResDto response = NotificationListResDto.of(List.of(notificationInfoResDto), - PageInfoResDto.from(notificationPage)); + NotificationListResDto response = NotificationListResDto.of(List.of(notificationInfoResDto)); - given(notificationService.findAllNotificationsFromMember(member.getEmail(), PageRequest.of(0, 10))) + given(notificationService.findAllNotificationsFromMember(member.getEmail())) .willReturn(response); // when & then @@ -139,15 +138,35 @@ void setUp(RestDocumentationContextProvider restDocumentation) { fieldWithPath("message").description("응답 메시지"), fieldWithPath("data.notificationInfoResDto[].id").description("알림 아이디"), fieldWithPath("data.notificationInfoResDto[].message").description("알림 메시지"), - fieldWithPath("data.notificationInfoResDto[].isRead").description("알림 읽은 여부"), - fieldWithPath("data.pageInfoResDto.currentPage").description("현재 페이지"), - fieldWithPath("data.pageInfoResDto.totalPages").description("전체 페이지"), - fieldWithPath("data.pageInfoResDto.totalItems").description("전체 개수") + fieldWithPath("data.notificationInfoResDto[].isRead").description("알림 읽은 여부") )) ) .andExpect(status().isOk()); } + @Test + @DisplayName("모든 알림 읽음으로 바꾸는데 성공하면 상태코드 200 반환") + void 모든_알림_읽음으로_바꾸는데_성공하면_상태코드_200_반환() throws Exception { + // given + // 모든 알림을 읽음 처리하는 메서드 호출에 대한 설정 + willDoNothing().given(notificationService).markAllNotificationsAsRead(member.getEmail()); + + // when & then + mockMvc.perform( + patch("/api/notifications") + .header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andDo(document("notification/markAsAllRead", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders(headerWithName(AUTHORIZATION_HEADER_NAME).description("JWT 토큰")) + )) + .andExpect(status().isOk()); + } + + /*@Test @DisplayName("알림 상세 조회에 성공하면 상태코드 200 반환") void 알림_상세_조회에_성공하면_상태코드_200_반환() throws Exception { diff --git a/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationServiceTest.java b/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationServiceTest.java index 5191ce27..3966ddc2 100644 --- a/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationServiceTest.java +++ b/src/test/java/shop/kkeujeok/kkeujeokbackend/notification/application/NotificationServiceTest.java @@ -14,8 +14,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import shop.kkeujeok.kkeujeokbackend.global.entity.Status; import shop.kkeujeok.kkeujeokbackend.member.domain.Member; @@ -120,17 +118,32 @@ void setUp() { void 회원의_모든_알림을_조회할_수_있다() { // given List notifications = List.of(notification); - when(notificationRepository.findAllNotifications(any(Member.class), any(Pageable.class))) - .thenReturn(new PageImpl<>(notifications)); + when(notificationRepository.findAllByReceiver(any(Member.class))) + .thenReturn(notifications); when(memberRepository.findByEmail(anyString())) .thenReturn(Optional.of(member)); // when - NotificationListResDto result = notificationService.findAllNotificationsFromMember(member.getEmail(), - Pageable.unpaged()); + NotificationListResDto result = notificationService.findAllNotificationsFromMember(member.getEmail()); // then assertThat(result.notificationInfoResDto()).isNotEmpty(); - assertThat(result.pageInfoResDto().totalItems()).isEqualTo(notifications.size()); + } + + @Test + @DisplayName("모든 알림을 읽음으로 처리할 수 있다") + void 모든_알림을_읽음으로_처리할_수_있다() { + // given + List notifications = List.of(notification); + when(memberRepository.findByEmail(anyString())) + .thenReturn(Optional.of(member)); + when(notificationRepository.findAllByReceiver(any(Member.class))) + .thenReturn(notifications); + + // when + notificationService.markAllNotificationsAsRead(member.getEmail()); + + // then + assertThat(notification.getIsRead()).isTrue(); } }