diff --git a/Dockerfile b/Dockerfile index 064a76df8..285837938 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ FROM openjdk:21 EXPOSE 8080 -COPY build/libs/clab-prod.jar /clab.jar +COPY build/libs/clab.jar /clab.jar ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=stage", "/clab.jar"] diff --git a/README.md b/README.md index 6722d9d19..e41338718 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - **PostgreSQL**: 주 데이터베이스로 사용. - **Redis**: 캐싱 및 JWT 관리를 위해 사용. - **Thymeleaf**: 메일 전송을 위한 템플릿 엔진. -- **GeoIP2**: IP 주소 기반 위치 정보 조회. +- **IPInfo**: IP 주소 기반 위치 정보 조회. - **Google Authenticator**: 2단계 인증을 위한 라이브러리. - **Slack API**: 각종 보안 알림을 위해 사용. - **Swagger**: API 문서 자동화. diff --git a/build.gradle b/build.gradle index f285ee96e..282b33693 100644 --- a/build.gradle +++ b/build.gradle @@ -1,113 +1,112 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.2.1' - id 'io.spring.dependency-management' version '1.1.4' + id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' } group = 'page.clab' version = '0.0.1-SNAPSHOT' jar { - enabled = false + enabled = false } bootJar { - archivesBaseName = "clab" - archiveVersion = "1.0.0" - - from("/src/main/resources") { - into 'BOOT-INF/classes' - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - - if (project.hasProperty('prod')) { - archiveFileName = "clab-prod.jar" - } else if (project.hasProperty('stage')) { - archiveFileName = "clab-stage.jar" - } else { - archiveFileName = "clab.jar" - } + archivesBaseName = "clab" + archiveFileName = "clab.jar" + archiveVersion = "1.0.0" + + if (project.hasProperty('prod')) { + archiveFileName = "clab-prod.jar" + } else if (project.hasProperty('stage')) { + archiveFileName = "clab-stage.jar" + } else { + archiveFileName = "clab.jar" + } +} + +test { + systemProperty 'spring.profiles.active', findProperty('env') ?: 'dev' } java { - sourceCompatibility = '21' + sourceCompatibility = '21' } repositories { - mavenCentral() - maven { url 'https://jitpack.io' } + mavenCentral() + maven { url 'https://jitpack.io' } } dependencies { - // Spring Project - implementation 'org.springframework.boot:spring-boot-starter-actuator' // 모니터링 - implementation 'org.springframework.boot:spring-boot-starter-web' // 웹 MVC - implementation 'org.springframework.boot:spring-boot-starter-validation' // 유효성 검사 - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // 템플릿 엔진 - implementation 'org.springframework.boot:spring-boot-starter-webflux' // WebFlux - developmentOnly 'org.springframework.boot:spring-boot-devtools' // 개발 도구 - - // Util - compileOnly 'org.projectlombok:lombok' // 롬복 - annotationProcessor 'org.projectlombok:lombok' // 롬복 - implementation 'com.google.code.gson:gson:2.10.1' // JSON 라이브러리 - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' // Swagger - implementation 'org.modelmapper:modelmapper:3.2.0' // ModelMapper - implementation 'commons-io:commons-io:2.15.1' // Apache Commons IO - implementation 'com.google.guava:guava:33.0.0-jre' // Google Core Libraries For Java - implementation 'org.springframework.boot:spring-boot-starter-mail' // Spring Mail - implementation 'com.google.zxing:core:3.4.1' // QR 코드 - implementation 'com.google.zxing:javase:3.4.1' // QR 코드 - implementation 'com.slack.api:slack-api-client:1.38.0' // Slack API - - // DB - implementation 'org.postgresql:postgresql:42.7.1' // PostgreSQL JDBC Driver - implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Redis - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA - implementation 'org.springframework.boot:spring-boot-starter-validation' // Hibernate Validator - implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' // Bean Validation - implementation 'jakarta.validation:jakarta.validation-api:3.0.2' // Jakarta Bean Validation - implementation 'com.maxmind.geoip2:geoip2:4.2.0' // GeoIP2 - - // QueryDSL - implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta" - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" - - // Security - implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security - implementation 'com.warrenstrange:googleauth:1.5.0' // Google Authenticator - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' // JWT 라이브러리 - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' // JWT 구현체 - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JWT Jackson 모듈 - - // XSS Filter - implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1' // Lucy XSS Servlet Filter - implementation 'com.navercorp.lucy:lucy-xss:1.6.3' // Lucy XSS Filter - implementation 'org.apache.commons:commons-text:1.11.0' // Apache Commons Text - - // Test - testImplementation 'org.springframework.boot:spring-boot-starter-test' // Spring Boot Test - testImplementation 'org.springframework.security:spring-security-test' // Spring Security Test + // Spring Project + implementation 'org.springframework.boot:spring-boot-starter-actuator' // 모니터링 + implementation 'org.springframework.boot:spring-boot-starter-web' // 웹 MVC + implementation 'org.springframework.boot:spring-boot-starter-validation' // 유효성 검사 + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // 템플릿 엔진 + implementation 'org.springframework.boot:spring-boot-starter-webflux' // WebFlux + developmentOnly 'org.springframework.boot:spring-boot-devtools' // 개발 도구 + + // Util + compileOnly 'org.projectlombok:lombok' // 롬복 + annotationProcessor 'org.projectlombok:lombok' // 롬복 + implementation 'com.google.code.gson:gson:2.10.1' // JSON 라이브러리 + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' // Swagger + implementation 'commons-io:commons-io:2.15.1' // Apache Commons IO + implementation 'com.google.guava:guava:33.0.0-jre' // Google Core Libraries For Java + implementation 'org.springframework.boot:spring-boot-starter-mail' // Spring Mail + implementation 'com.google.zxing:core:3.4.1' // QR 코드 + implementation 'com.google.zxing:javase:3.4.1' // QR 코드 + implementation 'com.slack.api:slack-api-client:1.38.0' // Slack API + implementation 'io.ipinfo:ipinfo-spring:0.3.1' // IPInfo + + + // DB + implementation 'org.postgresql:postgresql:42.7.1' // PostgreSQL JDBC Driver + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA + implementation 'org.springframework.boot:spring-boot-starter-validation' // Hibernate Validator + implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' // Bean Validation + implementation 'jakarta.validation:jakarta.validation-api:3.0.2' // Jakarta Bean Validation + + // QueryDSL + implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security + implementation 'com.warrenstrange:googleauth:1.5.0' // Google Authenticator + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' // JWT 라이브러리 + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' // JWT 구현체 + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JWT Jackson 모듈 + + // XSS Filter + implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1' // Lucy XSS Servlet Filter + implementation 'com.navercorp.lucy:lucy-xss:1.6.3' // Lucy XSS Filter + implementation 'org.apache.commons:commons-text:1.11.0' // Apache Commons Text + + // Test + testImplementation 'org.springframework.boot:spring-boot-starter-test' // Spring Boot Test + testImplementation 'org.springframework.security:spring-security-test' // Spring Security Test } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } def querydslDir = "$buildDir/generated/querydsl" sourceSets { - main.java.srcDirs += [ querydslDir ] + main.java.srcDirs += [ querydslDir ] } tasks.withType(JavaCompile) { - options.generatedSourceOutputDirectory = file(querydslDir) + options.generatedSourceOutputDirectory = file(querydslDir) } clean.doLast { - file(querydslDir).deleteDir() + file(querydslDir).deleteDir() } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..1ef6dbf5d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + postgresql: + image: postgres + restart: always + environment: + POSTGRES_DB: postgres + POSTGRES_USER: clab + POSTGRES_PASSWORD: clab@1362! + ports: + - "4000:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + + redis: + image: redis + restart: always + command: redis-server --requirepass clab@redis1362! + ports: + - "4001:6379" + volumes: + - redis-data:/data + + jenkins: + image: jenkins/jenkins:lts + user: root + restart: always + ports: + - "5010:8080" + - "5000:5000" + volumes: + - jenkins-data:/var/jenkins_home + + openjdk21: + image: openjdk:21-jdk + command: "/bin/bash" + volumes: + - /var/jenkins_home/workspace/clab-server:/app + + goofy_perlman: + image: openjdk:21-jdk + command: "/bin/bash" + +volumes: + postgres-data: + redis-data: + jenkins-data: + +networks: + default: + driver: bridge \ No newline at end of file diff --git a/src/main/java/page/clab/api/ApiApplication.java b/src/main/java/page/clab/api/ApiApplication.java index 2f1720b2a..749696735 100644 --- a/src/main/java/page/clab/api/ApiApplication.java +++ b/src/main/java/page/clab/api/ApiApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableJpaAuditing public class ApiApplication { public static void main(String[] args) { diff --git a/src/main/java/page/clab/api/domain/accuse/api/AccuseController.java b/src/main/java/page/clab/api/domain/accuse/api/AccuseController.java index 33e5f2f74..b9cd0d03f 100644 --- a/src/main/java/page/clab/api/domain/accuse/api/AccuseController.java +++ b/src/main/java/page/clab/api/domain/accuse/api/AccuseController.java @@ -20,12 +20,13 @@ import page.clab.api.domain.accuse.domain.AccuseStatus; import page.clab.api.domain.accuse.domain.TargetType; import page.clab.api.domain.accuse.dto.request.AccuseRequestDto; +import page.clab.api.domain.accuse.dto.response.AccuseMyResponseDto; import page.clab.api.domain.accuse.dto.response.AccuseResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; @RestController -@RequestMapping("/accuses") +@RequestMapping("/api/v1/accuses") @RequiredArgsConstructor @Tag(name = "Accuse", description = "신고") @Slf4j @@ -36,44 +37,53 @@ public class AccuseController { @Operation(summary = "[U] 신고하기", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createAccuse( - @Valid @RequestBody AccuseRequestDto accuseRequestDto + public ApiResponse createAccuse( + @Valid @RequestBody AccuseRequestDto requestDto ) { - Long id = accuseService.createAccuse(accuseRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = accuseService.createAccuse(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 신고 내역 조회(신고 대상, 처리 상태 기준)", description = "ROLE_ADMIN 이상의 권한이 필요함
" + "2개의 파라미터를 자유롭게 조합하여 필터링 가능
" + - "신고 대상, 처리 상태 중 하나라도 입력하지 않으면 전체 조회됨") + "신고 대상, 처리 상태 중 하나라도 입력하지 않으면 전체 조회됨
" + + "누적 횟수 기준으로 정렬할지 여부를 선택할 수 있음") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getAccusesByConditions( - @RequestParam(name = "targetType", required = false) TargetType targetType, - @RequestParam(name = "accuseStatus", required = false) AccuseStatus accuseStatus, + public ApiResponse> getAccusesByConditions( + @RequestParam(name = "targetType", required = false) TargetType type, + @RequestParam(name = "accuseStatus", required = false) AccuseStatus status, + @RequestParam(name = "countOrder", defaultValue = "false") boolean countOrder, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto accuses = accuseService.getAccusesByConditions(targetType, accuseStatus, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(accuses); - return responseModel; + PagedResponseDto accuses = accuseService.getAccusesByConditions(type, status, countOrder, pageable); + return ApiResponse.success(accuses); + } + + @Operation(summary = "[U] 나의 신고 내역 조회", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) + @GetMapping("/my") + public ApiResponse> getMyAccuses( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto accuses = accuseService.getMyAccuses(pageable); + return ApiResponse.success(accuses); } @Operation(summary = "[A] 신고 상태 변경", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) - @PatchMapping("/{accuseId}") - public ResponseModel updateAccuseStatus( - @PathVariable(name = "accuseId") Long accuseId, - @RequestParam(name = "accuseStatus") AccuseStatus accuseStatus + @PatchMapping("/{targetType}/{targetId}") + public ApiResponse updateAccuseStatus( + @PathVariable(name = "targetType") TargetType type, + @PathVariable(name = "targetId") Long targetId, + @RequestParam(name = "accuseStatus") AccuseStatus status ) { - Long id = accuseService.updateAccuseStatus(accuseId, accuseStatus); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = accuseService.updateAccuseStatus(type, targetId, status); + return ApiResponse.success(id); } } diff --git a/src/main/java/page/clab/api/domain/accuse/application/AccuseService.java b/src/main/java/page/clab/api/domain/accuse/application/AccuseService.java index 86dfc938f..65c7fe323 100644 --- a/src/main/java/page/clab/api/domain/accuse/application/AccuseService.java +++ b/src/main/java/page/clab/api/domain/accuse/application/AccuseService.java @@ -1,26 +1,38 @@ package page.clab.api.domain.accuse.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.accuse.dao.AccuseRepository; +import page.clab.api.domain.accuse.dao.AccuseTargetRepository; import page.clab.api.domain.accuse.domain.Accuse; import page.clab.api.domain.accuse.domain.AccuseStatus; +import page.clab.api.domain.accuse.domain.AccuseTarget; +import page.clab.api.domain.accuse.domain.AccuseTargetId; import page.clab.api.domain.accuse.domain.TargetType; import page.clab.api.domain.accuse.dto.request.AccuseRequestDto; +import page.clab.api.domain.accuse.dto.response.AccuseMemberInfo; +import page.clab.api.domain.accuse.dto.response.AccuseMyResponseDto; import page.clab.api.domain.accuse.dto.response.AccuseResponseDto; import page.clab.api.domain.accuse.exception.AccuseTargetTypeIncorrectException; import page.clab.api.domain.board.application.BoardService; +import page.clab.api.domain.board.domain.Board; import page.clab.api.domain.comment.application.CommentService; +import page.clab.api.domain.comment.domain.Comment; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.domain.review.application.ReviewService; +import page.clab.api.domain.review.domain.Review; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; + +import java.util.List; @Service @RequiredArgsConstructor @@ -37,56 +49,118 @@ public class AccuseService { private final NotificationService notificationService; + private final ValidationService validationService; + private final AccuseRepository accuseRepository; + private final AccuseTargetRepository accuseTargetRepository; + @Transactional - public Long createAccuse(AccuseRequestDto accuseRequestDto) { - TargetType accuseTargetType = accuseRequestDto.getTargetType(); - Long accuseTargetId = accuseRequestDto.getTargetId(); + public Long createAccuse(AccuseRequestDto requestDto) { + TargetType type = requestDto.getTargetType(); + Long targetId = requestDto.getTargetId(); + Member currentMember = memberService.getCurrentMember(); - if (!isAccuseRequestValid(accuseTargetType, accuseTargetId)) { - throw new NotFoundException(accuseTargetType.getDescription() + " " + accuseTargetId + "을 찾을 수 없습니다."); - } + validateAccuseRequest(type, targetId, currentMember); + + AccuseTarget target = getOrCreateAccuseTarget(requestDto, type, targetId); + validationService.checkValid(target); + accuseTargetRepository.save(target); - Member member = memberService.getCurrentMember(); - Accuse accuse = accuseRepository.findByMemberAndTargetTypeAndTargetId(member, accuseTargetType, accuseTargetId) - .orElseGet(() -> Accuse.create(accuseRequestDto, member)); - accuse.updateReason(accuseRequestDto.getReason()); + Accuse accuse = findOrCreateAccuse(requestDto, currentMember, target); + validationService.checkValid(accuse); - notificationService.sendNotificationToMember(member, "신고하신 내용이 접수되었습니다."); - notificationService.sendNotificationToSuperAdmins(member.getName() + "님이 신고를 접수하였습니다. 확인해주세요."); + notificationService.sendNotificationToMember(currentMember, "신고하신 내용이 접수되었습니다."); + notificationService.sendNotificationToSuperAdmins(currentMember.getName() + "님이 신고를 접수하였습니다. 확인해주세요."); return accuseRepository.save(accuse).getId(); } - public PagedResponseDto getAccusesByConditions(TargetType targetType, AccuseStatus accuseStatus, Pageable pageable) { - Page accuses = accuseRepository.findByConditions(targetType, accuseStatus, pageable); - return new PagedResponseDto<>(accuses.map(AccuseResponseDto::of)); + @Transactional(readOnly = true) + public PagedResponseDto getAccusesByConditions(TargetType type, AccuseStatus status, boolean countOrder, Pageable pageable) { + Page accuseTargets = accuseTargetRepository.findByConditions(type, status, countOrder, pageable); + List responseDtos = convertTargetsToResponseDtos(accuseTargets); + return new PagedResponseDto<>(responseDtos, pageable, responseDtos.size()); + } + + @Transactional(readOnly = true) + public PagedResponseDto getMyAccuses(Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + Page accuses = accuseRepository.findByMemberOrderByCreatedAtDesc(currentMember, pageable); + return new PagedResponseDto<>(accuses.map(AccuseMyResponseDto::toDto)); } @Transactional - public Long updateAccuseStatus(Long accuseId, AccuseStatus accuseStatus) { - Accuse accuse = getAccuseByIdOrThrow(accuseId); - accuse.updateStatus(accuseStatus); - notificationService.sendNotificationToMember(accuse.getMember(), "신고 상태가 " + accuseStatus.getDescription() + "로 변경되었습니다."); - return accuseRepository.save(accuse).getId(); + public Long updateAccuseStatus(TargetType type, Long targetId, AccuseStatus status) { + AccuseTarget target = getAccuseTargetByIdOrThrow(type, targetId); + target.updateStatus(status); + validationService.checkValid(target); + sendStatusUpdateNotifications(status, target); + return accuseTargetRepository.save(target).getTargetReferenceId(); } - private boolean isAccuseRequestValid(TargetType accuseTargetType, Long accuseTargetId) { - if (accuseTargetType == TargetType.BOARD) { - return boardService.isBoardExistById(accuseTargetId); - } - if (accuseTargetType == TargetType.COMMENT) { - return commentService.isCommentExistById(accuseTargetId); - } - if (accuseTargetType == TargetType.REVIEW) { - return reviewService.isReviewExistsById(accuseTargetId); + private AccuseTarget getAccuseTargetByIdOrThrow(TargetType type, Long targetId) { + return accuseTargetRepository.findById(AccuseTargetId.create(type, targetId)) + .orElseThrow(() -> new NotFoundException("존재하지 않는 신고 대상입니다.")); + } + + private void validateAccuseRequest(TargetType type, Long targetId, Member currentMember) { + switch (type) { + case BOARD: + Board board = boardService.getBoardByIdOrThrow(targetId); + if (board.isOwner(currentMember)) { + throw new AccuseTargetTypeIncorrectException("자신의 게시글은 신고할 수 없습니다."); + } + break; + case COMMENT: + Comment comment = commentService.getCommentByIdOrThrow(targetId); + if (comment.isOwner(currentMember)) { + throw new AccuseTargetTypeIncorrectException("자신의 댓글은 신고할 수 없습니다."); + } + break; + case REVIEW: + Review review = reviewService.getReviewByIdOrThrow(targetId); + if (review.isOwner(currentMember)) { + throw new AccuseTargetTypeIncorrectException("자신의 리뷰는 신고할 수 없습니다."); + } + break; + default: + throw new AccuseTargetTypeIncorrectException("신고 대상 유형이 올바르지 않습니다."); } - throw new AccuseTargetTypeIncorrectException("신고 대상 유형이 올바르지 않습니다."); } - private Accuse getAccuseByIdOrThrow(Long accuseId) { - return accuseRepository.findById(accuseId) - .orElseThrow(() -> new NotFoundException("존재하지 않는 신고입니다.")); + private AccuseTarget getOrCreateAccuseTarget(AccuseRequestDto requestDto, TargetType type, Long targetId) { + return accuseTargetRepository.findById(AccuseTargetId.create(type, targetId)) + .orElseGet(() -> AccuseRequestDto.toTargetEntity(requestDto)); + } + + private Accuse findOrCreateAccuse(AccuseRequestDto requestDto, Member currentMember, AccuseTarget target) { + return accuseRepository.findByMemberAndTarget(currentMember, target) + .map(existingAccuse -> { + existingAccuse.updateReason(requestDto.getReason()); + return existingAccuse; + }) + .orElseGet(() -> { + target.increaseAccuseCount(); + return AccuseRequestDto.toEntity(requestDto, currentMember, target); + }); + } + + @NotNull + private List convertTargetsToResponseDtos(Page accuseTargets) { + return accuseTargets.stream() + .map(accuseTarget -> { + List accuses = accuseRepository.findByTargetOrderByCreatedAtDesc(accuseTarget); + List members = AccuseMemberInfo.create(accuses); + return AccuseResponseDto.toDto(accuses.getFirst(), members); + }) + .toList(); + } + + private void sendStatusUpdateNotifications(AccuseStatus status, AccuseTarget target) { + List members = accuseRepository.findByTarget(target).stream() + .map(Accuse::getMember) + .toList(); + notificationService.sendNotificationToMembers(members, "신고 상태가 " + status.getDescription() + "(으)로 변경되었습니다."); } } diff --git a/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepository.java b/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepository.java index 240ce9d37..bb272d5b8 100644 --- a/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepository.java +++ b/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepository.java @@ -3,19 +3,25 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.accuse.domain.Accuse; -import page.clab.api.domain.accuse.domain.TargetType; +import page.clab.api.domain.accuse.domain.AccuseTarget; import page.clab.api.domain.member.domain.Member; +import java.util.List; import java.util.Optional; @Repository -public interface AccuseRepository extends JpaRepository, AccuseRepositoryCustom, QuerydslPredicateExecutor { +public interface AccuseRepository extends JpaRepository { Page findAllByOrderByCreatedAtDesc(Pageable pageable); - Optional findByMemberAndTargetTypeAndTargetId(Member member, TargetType targetType, Long targetId); + Optional findByMemberAndTarget(Member member, AccuseTarget target); + + List findByTargetOrderByCreatedAtDesc(AccuseTarget accuseTarget); + + Page findByMemberOrderByCreatedAtDesc(Member currentMember, Pageable pageable); + + List findByTarget(AccuseTarget target); } diff --git a/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepositoryImpl.java b/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepositoryImpl.java deleted file mode 100644 index 92a0e9144..000000000 --- a/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepositoryImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package page.clab.api.domain.accuse.dao; - -import com.querydsl.core.BooleanBuilder; -import com.querydsl.jpa.impl.JPAQueryFactory; -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 page.clab.api.domain.accuse.domain.Accuse; -import page.clab.api.domain.accuse.domain.AccuseStatus; -import page.clab.api.domain.accuse.domain.QAccuse; -import page.clab.api.domain.accuse.domain.TargetType; - -import java.util.List; - -@Repository -@RequiredArgsConstructor -public class AccuseRepositoryImpl implements AccuseRepositoryCustom { - - private final JPAQueryFactory queryFactory; - - @Override - public Page findByConditions(TargetType targetType, AccuseStatus accuseStatus, Pageable pageable) { - QAccuse accuse = QAccuse.accuse; - BooleanBuilder builder = new BooleanBuilder(); - - if (targetType != null) { - builder.and(accuse.targetType.eq(targetType)); - } - if (accuseStatus != null) { - builder.and(accuse.accuseStatus.eq(accuseStatus)); - } - - List result = queryFactory.selectFrom(accuse) - .where(builder) - .orderBy(accuse.createdAt.desc()) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - long total = queryFactory.selectFrom(accuse) - .where(builder) - .fetchCount(); - - return new PageImpl<>(result, pageable, total); - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepository.java b/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepository.java new file mode 100644 index 000000000..f3d3fa7e7 --- /dev/null +++ b/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepository.java @@ -0,0 +1,10 @@ +package page.clab.api.domain.accuse.dao; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import page.clab.api.domain.accuse.domain.AccuseTarget; +import page.clab.api.domain.accuse.domain.AccuseTargetId; + +public interface AccuseTargetRepository extends JpaRepository, AccuseTargetRepositoryCustom, QuerydslPredicateExecutor { + +} diff --git a/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepositoryCustom.java b/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepositoryCustom.java similarity index 53% rename from src/main/java/page/clab/api/domain/accuse/dao/AccuseRepositoryCustom.java rename to src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepositoryCustom.java index 3f81ecdbf..0a92e5e53 100644 --- a/src/main/java/page/clab/api/domain/accuse/dao/AccuseRepositoryCustom.java +++ b/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepositoryCustom.java @@ -2,12 +2,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import page.clab.api.domain.accuse.domain.Accuse; import page.clab.api.domain.accuse.domain.AccuseStatus; +import page.clab.api.domain.accuse.domain.AccuseTarget; import page.clab.api.domain.accuse.domain.TargetType; -public interface AccuseRepositoryCustom { +public interface AccuseTargetRepositoryCustom { - Page findByConditions(TargetType targetType, AccuseStatus accuseStatus, Pageable pageable); + Page findByConditions(TargetType type, AccuseStatus status, boolean countOrder, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepositoryImpl.java b/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepositoryImpl.java new file mode 100644 index 000000000..4f6a07ae0 --- /dev/null +++ b/src/main/java/page/clab/api/domain/accuse/dao/AccuseTargetRepositoryImpl.java @@ -0,0 +1,49 @@ +package page.clab.api.domain.accuse.dao; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +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 page.clab.api.domain.accuse.domain.AccuseStatus; +import page.clab.api.domain.accuse.domain.AccuseTarget; +import page.clab.api.domain.accuse.domain.QAccuseTarget; +import page.clab.api.domain.accuse.domain.TargetType; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class AccuseTargetRepositoryImpl implements AccuseTargetRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findByConditions(TargetType type, AccuseStatus status, boolean countOrder, Pageable pageable) { + QAccuseTarget qAccuseTarget = QAccuseTarget.accuseTarget; + BooleanExpression predicate = qAccuseTarget.isNotNull(); + + if (type != null) { + predicate = predicate.and(qAccuseTarget.targetType.eq(type)); + } + if (status != null) { + predicate = predicate.and(qAccuseTarget.accuseStatus.eq(status)); + } + + List accuseTargets = queryFactory.selectFrom(qAccuseTarget) + .where(predicate) + .orderBy(countOrder ? qAccuseTarget.accuseCount.desc() : qAccuseTarget.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = queryFactory.selectFrom(qAccuseTarget) + .where(predicate) + .fetchCount(); + + return new PageImpl<>(accuseTargets, pageable, total); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/accuse/domain/Accuse.java b/src/main/java/page/clab/api/domain/accuse/domain/Accuse.java index 353c70a92..90f5024d0 100644 --- a/src/main/java/page/clab/api/domain/accuse/domain/Accuse.java +++ b/src/main/java/page/clab/api/domain/accuse/domain/Accuse.java @@ -2,34 +2,31 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import lombok.ToString; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.accuse.dto.request.AccuseRequestDto; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; - -import java.time.LocalDateTime; +import page.clab.api.global.common.domain.BaseEntity; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -public class Accuse { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(callSuper = false) +public class Accuse extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -39,42 +36,19 @@ public class Accuse { @JoinColumn(name = "member_id", nullable = false) private Member member; - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private TargetType targetType; + @ManyToOne + @JoinColumns({ + @JoinColumn(name = "target_type", nullable = false), + @JoinColumn(name = "target_reference_id", nullable = false) + }) + private AccuseTarget target; @Column(nullable = false) - private Long targetId; - - @Column(nullable = false, length = 1000) + @Size(min = 1, max = 1000, message = "{size.accuse.reason}") private String reason; - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private AccuseStatus accuseStatus; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static Accuse of(AccuseRequestDto accuseRequestDto) { - return ModelMapperUtil.getModelMapper().map(accuseRequestDto, Accuse.class); - } - - public static Accuse create(AccuseRequestDto accuseRequestDto, Member member) { - Accuse accuse = ModelMapperUtil.getModelMapper().map(accuseRequestDto, Accuse.class); - accuse.setId(null); - accuse.setMember(member); - accuse.setAccuseStatus(AccuseStatus.PENDING); - return accuse; - } - public void updateReason(String reason) { this.reason = reason; } - public void updateStatus(AccuseStatus newStatus) { - this.accuseStatus = newStatus; - } - } diff --git a/src/main/java/page/clab/api/domain/accuse/domain/AccuseTarget.java b/src/main/java/page/clab/api/domain/accuse/domain/AccuseTarget.java new file mode 100644 index 000000000..c26e5ead8 --- /dev/null +++ b/src/main/java/page/clab/api/domain/accuse/domain/AccuseTarget.java @@ -0,0 +1,49 @@ +package page.clab.api.domain.accuse.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import page.clab.api.global.common.domain.BaseEntity; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@IdClass(AccuseTargetId.class) +public class AccuseTarget extends BaseEntity { + + @Id + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private TargetType targetType; + + @Id + @Column(nullable = false) + private Long targetReferenceId; + + private Long accuseCount; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private AccuseStatus accuseStatus; + + public void increaseAccuseCount() { + this.accuseCount++; + } + + public void updateStatus(AccuseStatus newStatus) { + this.accuseStatus = newStatus; + } + +} diff --git a/src/main/java/page/clab/api/domain/accuse/domain/AccuseTargetId.java b/src/main/java/page/clab/api/domain/accuse/domain/AccuseTargetId.java new file mode 100644 index 000000000..f2ab24149 --- /dev/null +++ b/src/main/java/page/clab/api/domain/accuse/domain/AccuseTargetId.java @@ -0,0 +1,27 @@ +package page.clab.api.domain.accuse.domain; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Embeddable +public class AccuseTargetId implements Serializable { + + @EqualsAndHashCode.Include + private TargetType targetType; + + @EqualsAndHashCode.Include + private Long targetReferenceId; + + public static AccuseTargetId create(TargetType targetType, Long targetReferenceId) { + return new AccuseTargetId(targetType, targetReferenceId); + } + +} diff --git a/src/main/java/page/clab/api/domain/accuse/dto/request/AccuseRequestDto.java b/src/main/java/page/clab/api/domain/accuse/dto/request/AccuseRequestDto.java index 87892c563..34f60c50d 100644 --- a/src/main/java/page/clab/api/domain/accuse/dto/request/AccuseRequestDto.java +++ b/src/main/java/page/clab/api/domain/accuse/dto/request/AccuseRequestDto.java @@ -2,21 +2,16 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.accuse.domain.Accuse; +import page.clab.api.domain.accuse.domain.AccuseStatus; +import page.clab.api.domain.accuse.domain.AccuseTarget; import page.clab.api.domain.accuse.domain.TargetType; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.member.domain.Member; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class AccuseRequestDto { @NotNull(message = "{notNull.accuse.targetType}") @@ -28,12 +23,24 @@ public class AccuseRequestDto { private Long targetId; @NotNull(message = "{notNull.accuse.reason}") - @Size(min = 1, max = 1000, message = "{size.accuse.reason}") @Schema(description = "신고 사유", example = "부적절한 게시글입니다.", required = true) private String reason; - public static AccuseRequestDto of(Accuse accuse) { - return ModelMapperUtil.getModelMapper().map(accuse, AccuseRequestDto.class); + public static Accuse toEntity(AccuseRequestDto requestDto, Member member, AccuseTarget target) { + return Accuse.builder() + .member(member) + .target(target) + .reason(requestDto.getReason()) + .build(); + } + + public static AccuseTarget toTargetEntity(AccuseRequestDto requestDto) { + return AccuseTarget.builder() + .targetType(requestDto.getTargetType()) + .targetReferenceId(requestDto.getTargetId()) + .accuseCount(1L) + .accuseStatus(AccuseStatus.PENDING) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseMemberInfo.java b/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseMemberInfo.java new file mode 100644 index 000000000..139747c6d --- /dev/null +++ b/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseMemberInfo.java @@ -0,0 +1,34 @@ +package page.clab.api.domain.accuse.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.accuse.domain.Accuse; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class AccuseMemberInfo { + + private String memberId; + + private String name; + + private LocalDateTime createdAt; + + public static List create(List accuses) { + return accuses.stream() + .map(AccuseMemberInfo::create) + .toList(); + } + + public static AccuseMemberInfo create(Accuse accuse) { + return AccuseMemberInfo.builder() + .memberId(accuse.getMember().getId()) + .name(accuse.getMember().getName()) + .createdAt(accuse.getCreatedAt()) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseMyResponseDto.java b/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseMyResponseDto.java new file mode 100644 index 000000000..84b372dc8 --- /dev/null +++ b/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseMyResponseDto.java @@ -0,0 +1,35 @@ +package page.clab.api.domain.accuse.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.accuse.domain.Accuse; +import page.clab.api.domain.accuse.domain.AccuseStatus; +import page.clab.api.domain.accuse.domain.TargetType; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class AccuseMyResponseDto { + + private TargetType targetType; + + private Long targetId; + + private String reason; + + private AccuseStatus accuseStatus; + + private LocalDateTime createdAt; + + public static AccuseMyResponseDto toDto(Accuse accuse) { + return AccuseMyResponseDto.builder() + .targetType(accuse.getTarget().getTargetType()) + .targetId(accuse.getTarget().getTargetReferenceId()) + .reason(accuse.getReason()) + .accuseStatus(accuse.getTarget().getAccuseStatus()) + .createdAt(accuse.getTarget().getCreatedAt()) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseResponseDto.java b/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseResponseDto.java index 317e50bbe..b546afd8c 100644 --- a/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseResponseDto.java +++ b/src/main/java/page/clab/api/domain/accuse/dto/response/AccuseResponseDto.java @@ -1,26 +1,19 @@ package page.clab.api.domain.accuse.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.accuse.domain.Accuse; import page.clab.api.domain.accuse.domain.AccuseStatus; import page.clab.api.domain.accuse.domain.TargetType; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; +import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class AccuseResponseDto { - private String memberId; - - private String name; + private List members; private TargetType targetType; @@ -30,12 +23,20 @@ public class AccuseResponseDto { private AccuseStatus accuseStatus; + private Long accuseCount; + private LocalDateTime createdAt; - public static AccuseResponseDto of(Accuse accuse) { - AccuseResponseDto accuseResponseDto = ModelMapperUtil.getModelMapper().map(accuse, AccuseResponseDto.class); - accuseResponseDto.setMemberId(accuse.getMember().getId()); - accuseResponseDto.setName(accuse.getMember().getName()); - return accuseResponseDto; + public static AccuseResponseDto toDto(Accuse accuse, List members) { + return AccuseResponseDto.builder() + .members(members) + .targetType(accuse.getTarget().getTargetType()) + .targetId(accuse.getTarget().getTargetReferenceId()) + .reason(accuse.getReason()) + .accuseStatus(accuse.getTarget().getAccuseStatus()) + .accuseCount(accuse.getTarget().getAccuseCount()) + .createdAt(accuse.getTarget().getCreatedAt()) + .build(); } + } diff --git a/src/main/java/page/clab/api/domain/accuse/exception/AccuseSearchArgumentLackException.java b/src/main/java/page/clab/api/domain/accuse/exception/AccuseSearchArgumentLackException.java deleted file mode 100644 index 62f72a37f..000000000 --- a/src/main/java/page/clab/api/domain/accuse/exception/AccuseSearchArgumentLackException.java +++ /dev/null @@ -1,9 +0,0 @@ -package page.clab.api.domain.accuse.exception; - -public class AccuseSearchArgumentLackException extends RuntimeException { - - public AccuseSearchArgumentLackException(String message) { - super(message); - } - -} diff --git a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupAdminController.java b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupAdminController.java index dce7304d7..be4fd70f3 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupAdminController.java +++ b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupAdminController.java @@ -24,14 +24,16 @@ import page.clab.api.domain.activityGroup.dto.request.ActivityGroupRequestDto; import page.clab.api.domain.activityGroup.dto.request.ActivityGroupUpdateRequestDto; import page.clab.api.domain.activityGroup.dto.response.ActivityGroupMemberWithApplyReasonResponseDto; +import page.clab.api.domain.activityGroup.dto.response.ActivityGroupResponseDto; +import page.clab.api.domain.award.dto.response.AwardResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.exception.PermissionDeniedException; import java.util.List; @RestController -@RequestMapping("/activity-group/admin") +@RequestMapping("/api/v1/activity-group/admin") @RequiredArgsConstructor @Tag(name = "ActivityGroupAdmin", description = "활동 그룹 관리") @Slf4j @@ -42,108 +44,104 @@ public class ActivityGroupAdminController { @Operation(summary = "[U] 활동 생성", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createActivityGroup( - @Valid @RequestBody ActivityGroupRequestDto activityGroupRequestDto + public ApiResponse createActivityGroup( + @Valid @RequestBody ActivityGroupRequestDto requestDto ) { - Long id = activityGroupAdminService.createActivityGroup(activityGroupRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupAdminService.createActivityGroup(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 활동 수정", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{activityGroupId}") - public ResponseModel updateActivityGroup( + public ApiResponse updateActivityGroup( @PathVariable(name = "activityGroupId") Long activityGroupId, - @Valid @RequestBody ActivityGroupUpdateRequestDto activityGroupUpdateRequestDto + @Valid @RequestBody ActivityGroupUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = activityGroupAdminService.updateActivityGroup(activityGroupId, activityGroupUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupAdminService.updateActivityGroup(activityGroupId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 활동 상태 변경", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("manage/{activityGroupId}") - public ResponseModel manageActivityGroupStatus( + public ApiResponse manageActivityGroupStatus( @PathVariable(name = "activityGroupId") Long activityGroupId, - @RequestParam(name = "activityGroupStatus") ActivityGroupStatus activityGroupStatus + @RequestParam(name = "activityGroupStatus") ActivityGroupStatus status ) { - Long id = activityGroupAdminService.manageActivityGroup(activityGroupId, activityGroupStatus); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupAdminService.manageActivityGroup(activityGroupId, status); + return ApiResponse.success(id); } @Operation(summary = "[A] 활동 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{activityGroupId}") - public ResponseModel deleteActivityGroup( + public ApiResponse deleteActivityGroup( @PathVariable(name = "activityGroupId") Long activityGroupId ) { Long id = activityGroupAdminService.deleteActivityGroup(activityGroupId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[U] 프로젝트 진행도 수정", description = "ROLE_USER 이상의 권한이 필요함
" + "진행도는 0~100 사이의 값으로 입력해야 함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/progress/{activityGroupId}") - public ResponseModel updateProjectProgress( + public ApiResponse updateProjectProgress( @PathVariable(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "progress") Long progress ) throws PermissionDeniedException { Long id = activityGroupAdminService.updateProjectProgress(activityGroupId, progress); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[U] 커리큘럼 및 일정 생성", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/schedule") - public ResponseModel addSchedule( + public ApiResponse addSchedule( @RequestParam(name = "activityGroupId") Long activityGroupId, - @Valid @RequestBody List groupScheduleDto + @Valid @RequestBody List scheduleDtos ) throws PermissionDeniedException { - Long id = activityGroupAdminService.addSchedule(activityGroupId, groupScheduleDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupAdminService.addSchedule(activityGroupId, scheduleDtos); + return ApiResponse.success(id); } @Operation(summary = "[U] 활동 멤버 및 지원서 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "관리자 또는 리더만 조회 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/members") - public ResponseModel getApplyGroupMemberList( + public ApiResponse> getApplyGroupMemberList( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) throws PermissionDeniedException { Pageable pageable = PageRequest.of(page, size); PagedResponseDto groupMembers = activityGroupAdminService.getGroupMembersWithApplyReason(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(groupMembers); - return responseModel; + return ApiResponse.success(groupMembers); } @Operation(summary = "[U] 신청 멤버 상태 변경", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/accept") - public ResponseModel acceptGroupMember( + public ApiResponse acceptGroupMember( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "memberId") String memberId, @RequestParam(name = "status") GroupMemberStatus status ) throws PermissionDeniedException { String id = activityGroupAdminService.manageGroupMemberStatus(activityGroupId, memberId, status); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 활동그룹 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedActivityGroups( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto activityGroups = activityGroupAdminService.getDeletedActivityGroups(pageable); + return ApiResponse.success(activityGroups); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupBoardController.java b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupBoardController.java index 37db80596..35c7e3d1f 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupBoardController.java +++ b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupBoardController.java @@ -5,6 +5,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; @@ -23,14 +24,15 @@ import page.clab.api.domain.activityGroup.dto.response.ActivityGroupBoardResponseDto; import page.clab.api.domain.activityGroup.dto.response.ActivityGroupBoardUpdateResponseDto; import page.clab.api.domain.activityGroup.dto.response.AssignmentSubmissionWithFeedbackResponseDto; +import page.clab.api.domain.award.dto.response.AwardResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.exception.PermissionDeniedException; import java.util.List; @RestController -@RequestMapping("/activity-group/boards") +@RequestMapping("/api/v1/activity-group/boards") @RequiredArgsConstructor @Tag(name = "ActivityGroupBoard", description = "활동 그룹 게시판 관리") @Slf4j @@ -47,109 +49,105 @@ public class ActivityGroupBoardController { ) @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createActivityGroupBoard( + public ApiResponse createActivityGroupBoard( @RequestParam(name = "parentId", required = false) Long parentId, @RequestParam(name = "activityGroupId") Long activityGroupId, - @Valid @RequestBody ActivityGroupBoardRequestDto activityGroupBoardRequestDto + @Valid @RequestBody ActivityGroupBoardRequestDto requestDto ) throws PermissionDeniedException { - Long id = activityGroupBoardService.createActivityGroupBoard(parentId, activityGroupId, activityGroupBoardRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupBoardService.createActivityGroupBoard(parentId, activityGroupId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 활동 그룹 게시판 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/list") - public ResponseModel getActivityGroupBoardList( + public ApiResponse> getActivityGroupBoardList( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { - Pageable pageable = Pageable.ofSize(size).withPage(page); - PagedResponseDto allBoards = activityGroupBoardService.getAllActivityGroupBoard(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(allBoards); - return responseModel; + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto boards = activityGroupBoardService.getAllActivityGroupBoard(pageable); + return ApiResponse.success(boards); } @Operation(summary = "[U] 활동 그룹 게시판 단일 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getActivityGroupBoardById( + public ApiResponse getActivityGroupBoardById( @RequestParam(name = "activityGroupBoardId") Long activityGroupBoardId ) { ActivityGroupBoardResponseDto board = activityGroupBoardService.getActivityGroupBoardById(activityGroupBoardId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(board); - return responseModel; + return ApiResponse.success(board); } @Operation(summary = "[U] 활동 그룹 ID에 대한 카테고리별 게시판 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/by-category") - public ResponseModel getActivityGroupBoardByCategory( + public ApiResponse> getActivityGroupBoardByCategory( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "category") ActivityGroupBoardCategory category, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { - Pageable pageable = Pageable.ofSize(size).withPage(page); + Pageable pageable = PageRequest.of(page, size); PagedResponseDto boards = activityGroupBoardService.getActivityGroupBoardByCategory(activityGroupId, category, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(boards); - return responseModel; + return ApiResponse.success(boards); } @Operation(summary = "[U] 활동 그룹 게시판 계층 구조적 조회, 부모 및 자식 게시판 함께 반환", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/by-parent") - public ResponseModel getActivityGroupBoardByParent( + public ApiResponse> getActivityGroupBoardByParent( @RequestParam(name = "parentId") Long parentId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) throws PermissionDeniedException { - Pageable pageable = Pageable.ofSize(size).withPage(page); + Pageable pageable = PageRequest.of(page, size); PagedResponseDto boards = activityGroupBoardService.getActivityGroupBoardByParent(parentId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(boards); - return responseModel; + return ApiResponse.success(boards); } @Operation(summary = "[U] 나의 제출 과제 및 피드백 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my-assignment") - public ResponseModel getMyAssignmentBoardWithFeedback( + public ApiResponse> getMyAssignmentBoardWithFeedback( @RequestParam(name = "parentId") Long parentId ) { - List submissionWithFeedbacks = activityGroupBoardService.getMyAssignmentsWithFeedbacks(parentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(submissionWithFeedbacks); - return responseModel; + List boards = activityGroupBoardService.getMyAssignmentsWithFeedbacks(parentId); + return ApiResponse.success(boards); } @Operation(summary = "[U] 활동 그룹 게시판 수정", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("") - public ResponseModel updateActivityGroupBoard( + public ApiResponse updateActivityGroupBoard( @RequestParam(name = "activityGroupBoardId") Long activityGroupBoardId, - @Valid @RequestBody ActivityGroupBoardUpdateRequestDto activityGroupBoardUpdateRequestDto + @Valid @RequestBody ActivityGroupBoardUpdateRequestDto requestDto ) throws PermissionDeniedException { - ActivityGroupBoardUpdateResponseDto responseDto = activityGroupBoardService.updateActivityGroupBoard(activityGroupBoardId, activityGroupBoardUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDto); - return responseModel; + ActivityGroupBoardUpdateResponseDto board = activityGroupBoardService.updateActivityGroupBoard(activityGroupBoardId, requestDto); + return ApiResponse.success(board); } @Operation(summary = "[U] 활동 그룹 게시판 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("") - public ResponseModel deleteActivityGroupBoard( + public ApiResponse deleteActivityGroupBoard( @RequestParam Long activityGroupBoardId ) throws PermissionDeniedException { Long id = activityGroupBoardService.deleteActivityGroupBoard(activityGroupBoardId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 활동 그룹 게시판 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedActivityGroupBoards( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto activityBoards = activityGroupBoardService.getDeletedActivityGroupBoards(pageable); + return ApiResponse.success(activityBoards); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupMemberController.java b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupMemberController.java index 15e86e5dc..d9366fa4b 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupMemberController.java +++ b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupMemberController.java @@ -2,7 +2,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.mail.MessagingException; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,10 +24,10 @@ import page.clab.api.domain.activityGroup.dto.response.ActivityGroupStatusResponseDto; import page.clab.api.domain.activityGroup.dto.response.GroupMemberResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; @RestController -@RequestMapping("/activity-group/member") +@RequestMapping("/api/v1/activity-group/member") @RequiredArgsConstructor @Tag(name = "ActivityGroupMember", description = "활동 그룹 멤버") @Slf4j @@ -38,99 +37,97 @@ public class ActivityGroupMemberController { @Operation(summary = "활동 전체 목록 조회", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @GetMapping("") - public ResponseModel getActivityGroups( + public ApiResponse> getActivityGroups( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto activityGroups = activityGroupMemberService.getActivityGroups(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(activityGroups); - return responseModel; + return ApiResponse.success(activityGroups); } @Operation(summary = "활동 상세 조회", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @GetMapping("/{activityGroupId}") - public ResponseModel getActivityGroup( + public ApiResponse getActivityGroup( @PathVariable(name = "activityGroupId") Long activityGroupId ) { Object activityGroup = activityGroupMemberService.getActivityGroup(activityGroupId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(activityGroup); - return responseModel; + return ApiResponse.success(activityGroup); + } + + @Operation(summary = "[U] 나의 활동 목록 조회", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) + @GetMapping("/my") + public ApiResponse> getMyActivityGroups( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto activityGroups = activityGroupMemberService.getMyActivityGroups(pageable); + return ApiResponse.success(activityGroups); } @Operation(summary = "[U] 활동 상태별 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/status") - public ResponseModel getActivityGroupsByStatus ( - @RequestParam(name = "activityGroupStatus") ActivityGroupStatus activityGroupStatus, + public ApiResponse> getActivityGroupsByStatus ( + @RequestParam(name = "activityGroupStatus") ActivityGroupStatus status, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto activityGroupList = activityGroupMemberService.getActivityGroupsByStatus(activityGroupStatus, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(activityGroupList); - return responseModel; + PagedResponseDto activityGroups = activityGroupMemberService.getActivityGroupsByStatus(status, pageable); + return ApiResponse.success(activityGroups); } @Operation(summary = "카테고리별 활동 목록 조회", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @GetMapping("/list") - public ResponseModel getActivityGroupsByCategory( + public ApiResponse> getActivityGroupsByCategory( @RequestParam(name = "category") ActivityGroupCategory category, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto activityGroups = activityGroupMemberService.getActivityGroupsByCategory(category, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(activityGroups); - return responseModel; + return ApiResponse.success(activityGroups); } @Operation(summary = "[U] 활동 일정 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/schedule") - public ResponseModel getGroupScheduleList( + public ApiResponse> getGroupScheduleList( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto groupSchedules = activityGroupMemberService.getGroupSchedules(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(groupSchedules); - return responseModel; + return ApiResponse.success(groupSchedules); } @Operation(summary = "[U] 활동 멤버 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "활동에 참여(수락)된 멤버만 조회 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/members") - public ResponseModel getActivityGroupMemberList( + public ApiResponse> getActivityGroupMemberList( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto activityGroupMembers = activityGroupMemberService.getActivityGroupMembers(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(activityGroupMembers); - return responseModel; + return ApiResponse.success(activityGroupMembers); } @Operation(summary = "[U] 활동 신청", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/apply") - public ResponseModel applyActivityGroup( + public ApiResponse applyActivityGroup( @RequestParam Long activityGroupId, - @Valid @RequestBody ApplyFormRequestDto formRequestDto - ) throws MessagingException { - Long id = activityGroupMemberService.applyActivityGroup(activityGroupId, formRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + @Valid @RequestBody ApplyFormRequestDto requestDto + ) { + Long id = activityGroupMemberService.applyActivityGroup(activityGroupId, requestDto); + return ApiResponse.success(id); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupReportController.java b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupReportController.java index 212db3a5f..5c7f9af70 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupReportController.java +++ b/src/main/java/page/clab/api/domain/activityGroup/api/ActivityGroupReportController.java @@ -21,12 +21,12 @@ import page.clab.api.domain.activityGroup.dto.request.ActivityGroupReportRequestDto; import page.clab.api.domain.activityGroup.dto.request.ActivityGroupReportUpdateRequestDto; import page.clab.api.domain.activityGroup.dto.response.ActivityGroupReportResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/activity-group/report") +@RequestMapping("/api/v1/activity-group/report") @RequiredArgsConstructor @Tag(name = "ActivityGroupReport", description = "활동 그룹 보고서") @Slf4j @@ -37,67 +37,70 @@ public class ActivityGroupReportController { @Operation(summary = "[U] 활동 보고서 작성", description = "ROLE_USER 이상의 권한이 필요함") @PostMapping("") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel writeReport( - @Valid @RequestBody ActivityGroupReportRequestDto reportRequestDto + public ApiResponse writeReport( + @Valid @RequestBody ActivityGroupReportRequestDto requestDto ) throws PermissionDeniedException, IllegalAccessException { - Long id = activityGroupReportService.writeReport(reportRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupReportService.writeReport(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 특정 그룹의 활동 보고서 전체 조회", description = "ROLE_USER 이상의 권한이 필요함") @GetMapping("") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel getReports( + public ApiResponse> getReports( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size - ){ + ) { Pageable pageable = PageRequest.of(page,size); - PagedResponseDto reportResponseDtos = activityGroupReportService.getReports(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(reportResponseDtos); - return responseModel; + PagedResponseDto reports = activityGroupReportService.getReports(activityGroupId, pageable); + return ApiResponse.success(reports); } @Operation(summary = "[U] 특정 그룹의 특정 차시 활동 보고서 검색", description = "ROLE_USER 이상의 권한이 필요함") @GetMapping("/search") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel searchReport( + public ApiResponse searchReport( @RequestParam(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "turn") Long turn - ){ + ) { ActivityGroupReportResponseDto report = activityGroupReportService.searchReport(activityGroupId, turn); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(report); - return responseModel; + return ApiResponse.success(report); } @Operation(summary = "[U] 활동 보고서 수정", description = "ROLE_USER 이상의 권한이 필요함") @PatchMapping("/{reportId}") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel updateReport( + public ApiResponse updateReport( @PathVariable(name = "reportId") Long reportId, @RequestParam(name = "activityGroupId") Long activityGroupId, - @Valid @RequestBody ActivityGroupReportUpdateRequestDto reportRequestDto + @Valid @RequestBody ActivityGroupReportUpdateRequestDto requestDto ) throws PermissionDeniedException, IllegalAccessException { - Long id = activityGroupReportService.updateReport(reportId, activityGroupId, reportRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityGroupReportService.updateReport(reportId, activityGroupId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 활동보고서 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{reportId}") - public ResponseModel deleteAward( + public ApiResponse deleteAward( @PathVariable(name = "reportId") Long reportId ) throws PermissionDeniedException { Long id = activityGroupReportService.deleteReport(reportId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 활동보고서 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedActivityGroupReports( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto activityGroupReports = activityGroupReportService.getDeletedActivityGroupReports(pageable); + return ApiResponse.success(activityGroupReports); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/api/AttendanceController.java b/src/main/java/page/clab/api/domain/activityGroup/api/AttendanceController.java index 1988b417c..13e6faa12 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/api/AttendanceController.java +++ b/src/main/java/page/clab/api/domain/activityGroup/api/AttendanceController.java @@ -9,6 +9,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -21,13 +22,13 @@ import page.clab.api.domain.activityGroup.dto.response.AttendanceResponseDto; import page.clab.api.domain.activityGroup.exception.DuplicateAbsentExcuseException; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.exception.PermissionDeniedException; import java.io.IOException; @RestController -@RequestMapping("/attendance") +@RequestMapping("/api/v1/attendance") @RequiredArgsConstructor @Tag(name = "Attendance", description = "출석체크") @Slf4j @@ -38,82 +39,70 @@ public class AttendanceController { @Operation(summary = "[U] 출석체크 QR 생성", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "") - public ResponseModel generateAttendanceQRCode ( + public ApiResponse generateAttendanceQRCode ( @RequestParam(name = "activityGroupId") Long activityGroupId ) throws IOException, WriterException, PermissionDeniedException, IllegalAccessException { String QRCodeURL = attendanceService.generateAttendanceQRCode(activityGroupId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(QRCodeURL); - return responseModel; + return ApiResponse.success(QRCodeURL); } @Operation(summary = "[U] 출석 인증", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/check-in") - public ResponseModel checkInAttendance( - @RequestBody AttendanceRequestDto attendanceRequestDto + public ApiResponse checkInAttendance( + @RequestBody AttendanceRequestDto requestDto ) throws IllegalAccessException { - Long id = attendanceService.checkMemberAttendance(attendanceRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = attendanceService.checkMemberAttendance(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 내 출석기록 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping({"/my-attendance"}) - public ResponseModel searchMyAttendance( + public ApiResponse> searchMyAttendance( @RequestParam(name = "activityGroupId", defaultValue = "1") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) throws IllegalAccessException { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto attendanceResponseDtos = attendanceService.getMyAttendances(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(attendanceResponseDtos); - return responseModel; + PagedResponseDto myAttendances = attendanceService.getMyAttendances(activityGroupId, pageable); + return ApiResponse.success(myAttendances); } @Operation(summary = "[U] 특정 그룹의 출석기록 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping({"/group-attendance"}) - public ResponseModel searchGroupAttendance( + public ApiResponse> searchGroupAttendance( @RequestParam(name = "activityGroupId", defaultValue = "1") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) throws PermissionDeniedException { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto attendanceResponseDtos = attendanceService.getGroupAttendances(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(attendanceResponseDtos); - return responseModel; + PagedResponseDto attendances = attendanceService.getGroupAttendances(activityGroupId, pageable); + return ApiResponse.success(attendances); } @Operation(summary = "[U] 불참 사유서 등록", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping({"/absent"}) - public ResponseModel writeAbsentExcuse( - @RequestBody AbsentRequestDto absentRequestDto + public ApiResponse writeAbsentExcuse( + @RequestBody AbsentRequestDto requestDto ) throws IllegalAccessException, DuplicateAbsentExcuseException { - Long id = attendanceService.writeAbsentExcuse(absentRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = attendanceService.writeAbsentExcuse(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 그룹의 불참 사유서 열람", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - @GetMapping({"/absent/{ActivityGroupId}"}) - public ResponseModel getActivityGroupAbsentExcuses( - @RequestParam(name = "activityGroupId") Long activityGroupId, + @GetMapping({"/absent/{activityGroupId}"}) + public ApiResponse> getActivityGroupAbsentExcuses( + @PathVariable(name = "activityGroupId") Long activityGroupId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) throws PermissionDeniedException { Pageable pageable = PageRequest.of(page, size); PagedResponseDto absentExcuses = attendanceService.getActivityGroupAbsentExcuses(activityGroupId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(absentExcuses); - return responseModel; + return ApiResponse.success(absentExcuses); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupAdminService.java b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupAdminService.java index 6b403f5d8..1c716c78b 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupAdminService.java +++ b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupAdminService.java @@ -1,11 +1,11 @@ package page.clab.api.domain.activityGroup.application; -import jakarta.transaction.Transactional; 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.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.dao.ActivityGroupRepository; import page.clab.api.domain.activityGroup.dao.ApplyFormRepository; import page.clab.api.domain.activityGroup.dao.GroupScheduleRepository; @@ -20,12 +20,14 @@ import page.clab.api.domain.activityGroup.dto.request.ActivityGroupRequestDto; import page.clab.api.domain.activityGroup.dto.request.ActivityGroupUpdateRequestDto; import page.clab.api.domain.activityGroup.dto.response.ActivityGroupMemberWithApplyReasonResponseDto; +import page.clab.api.domain.activityGroup.dto.response.ActivityGroupResponseDto; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; import java.util.List; import java.util.Map; @@ -39,130 +41,146 @@ public class ActivityGroupAdminService { private final ActivityGroupMemberService activityGroupMemberService; + private final NotificationService notificationService; + + private final ValidationService validationService; + private final ActivityGroupRepository activityGroupRepository; private final GroupScheduleRepository groupScheduleRepository; private final ApplyFormRepository applyFormRepository; - private final NotificationService notificationService; - @Transactional - public Long createActivityGroup(ActivityGroupRequestDto activityGroupRequestDto) { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = ActivityGroup.create(activityGroupRequestDto); + public Long createActivityGroup(ActivityGroupRequestDto requestDto) { + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = ActivityGroupRequestDto.toEntity(requestDto); + validationService.checkValid(activityGroup); activityGroupRepository.save(activityGroup); - GroupMember groupLeader = GroupMember.create(member, activityGroup, ActivityGroupRole.LEADER, GroupMemberStatus.ACCEPTED); + + GroupMember groupLeader = GroupMember.create(currentMember, activityGroup, ActivityGroupRole.LEADER, GroupMemberStatus.ACCEPTED); + validationService.checkValid(groupLeader); activityGroupMemberService.save(groupLeader); - notificationService.sendNotificationToMember( - member, - "활동 그룹 생성이 완료되었습니다. 활동 승인이 완료되면 활동 그룹을 이용할 수 있습니다." - ); + + notificationService.sendNotificationToMember(currentMember, "활동 그룹 생성이 완료되었습니다. 활동 승인이 완료되면 활동 그룹을 이용할 수 있습니다."); return activityGroup.getId(); } - public Long updateActivityGroup(Long activityGroupId, ActivityGroupUpdateRequestDto activityGroupUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + @Transactional + public Long updateActivityGroup(Long activityGroupId, ActivityGroupUpdateRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); - if (!isMemberGroupLeaderRole(activityGroup, member)) { + if (!isMemberGroupLeaderRole(activityGroup, currentMember)) { throw new PermissionDeniedException("해당 활동을 수정할 권한이 없습니다."); } - activityGroup.update(activityGroupUpdateRequestDto); + activityGroup.update(requestDto); + validationService.checkValid(activityGroup); return activityGroupRepository.save(activityGroup).getId(); } + @Transactional public Long manageActivityGroup(Long activityGroupId, ActivityGroupStatus status) { ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); activityGroup.updateStatus(status); + validationService.checkValid(activityGroup); activityGroupRepository.save(activityGroup); + GroupMember groupLeader = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroupId, ActivityGroupRole.LEADER); if (groupLeader != null) { - notificationService.sendNotificationToMember( - groupLeader.getMember(), - "활동 그룹이 [" + status.getDescription() + "] 상태로 변경되었습니다." - ); + notificationService.sendNotificationToMember(groupLeader.getMember(), "활동 그룹이 [" + status.getDescription() + "] 상태로 변경되었습니다."); } return activityGroup.getId(); } + @Transactional(readOnly = true) + public PagedResponseDto getDeletedActivityGroups(Pageable pageable) { + Page activityGroups = activityGroupRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(activityGroups.map(ActivityGroupResponseDto::toDto)); + } + @Transactional public Long deleteActivityGroup(Long activityGroupId) { - List groupMemberList = activityGroupMemberService.getGroupMemberByActivityGroupId(activityGroupId); - List groupScheduleList = groupScheduleRepository.findAllByActivityGroupIdOrderByIdDesc(activityGroupId); ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + List groupMembers = activityGroupMemberService.getGroupMemberByActivityGroupId(activityGroupId); + List groupSchedules = groupScheduleRepository.findAllByActivityGroupIdOrderByIdDesc(activityGroupId); GroupMember groupLeader = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroupId, ActivityGroupRole.LEADER); - activityGroupMemberService.deleteAll(groupMemberList); - groupScheduleRepository.deleteAll(groupScheduleList); + + activityGroupMemberService.deleteAll(groupMembers); + groupScheduleRepository.deleteAll(groupSchedules); activityGroupRepository.delete(activityGroup); + if (groupLeader != null) { - notificationService.sendNotificationToMember( - groupLeader.getMember(), - "활동 그룹 [" + activityGroup.getName() + "]이 삭제되었습니다." - ); + notificationService.sendNotificationToMember(groupLeader.getMember(), "활동 그룹 [" + activityGroup.getName() + "]이 삭제되었습니다."); } return activityGroup.getId(); } + @Transactional public Long updateProjectProgress(Long activityGroupId, Long progress) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); - if (!isMemberGroupLeaderRole(activityGroup, member)) { + if (!isMemberGroupLeaderRole(activityGroup, currentMember)) { throw new PermissionDeniedException("해당 활동을 수정할 권한이 없습니다."); } - activityGroup.setProgress(progress); + activityGroup.updateProgress(progress); + validationService.checkValid(activityGroup); return activityGroupRepository.save(activityGroup).getId(); } @Transactional - public Long addSchedule(Long activityGroupId, List groupScheduleDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + public Long addSchedule(Long activityGroupId, List scheduleDtos) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); - if (!isMemberGroupLeaderRole(activityGroup, member)) { + if (!isMemberGroupLeaderRole(activityGroup, currentMember)) { throw new PermissionDeniedException("해당 일정을 등록할 권한이 없습니다."); } - groupScheduleDto.stream() - .map(scheduleDto -> GroupSchedule.of(activityGroup, scheduleDto)) - .forEach(groupScheduleRepository::save); + List groupSchedules = scheduleDtos.stream() + .map(scheduleDto -> GroupScheduleDto.toEntity(scheduleDto, activityGroup)) + .toList(); + groupScheduleRepository.saveAll(groupSchedules); return activityGroup.getId(); } + @Transactional(readOnly = true) public PagedResponseDto getGroupMembersWithApplyReason(Long activityGroupId, Pageable pageable) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); if (!(isMemberGroupLeaderRole(activityGroup, currentMember) || currentMember.isAdminRole())) { throw new PermissionDeniedException("해당 활동의 멤버를 조회할 권한이 없습니다."); } + List applyForms = applyFormRepository.findAllByActivityGroup(activityGroup); Map memberIdToApplyReasonMap = applyForms.stream() .collect(Collectors.toMap( applyForm -> applyForm.getMember().getId(), ApplyForm::getApplyReason )); + Page groupMembers = activityGroupMemberService.getGroupMemberByActivityGroupId(activityGroupId, pageable); - List dtos = groupMembers.getContent().stream() + List groupMembersWithApplyReason = groupMembers.getContent().stream() .map(groupMember -> { String applyReason = memberIdToApplyReasonMap.getOrDefault(groupMember.getMember().getId(), ""); return ActivityGroupMemberWithApplyReasonResponseDto.create(groupMember, applyReason); }).toList(); - return new PagedResponseDto<>(new PageImpl<>(dtos, pageable, groupMembers.getTotalElements())); + + return new PagedResponseDto<>(new PageImpl<>(groupMembersWithApplyReason, pageable, groupMembers.getTotalElements())); } + @Transactional public String manageGroupMemberStatus(Long activityGroupId, String memberId, GroupMemberStatus status) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); if (!isMemberGroupLeaderRole(activityGroup, currentMember)) { throw new PermissionDeniedException("해당 활동의 신청 멤버를 조회할 권한이 없습니다."); } + Member member = memberService.getMemberByIdOrThrow(memberId); GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMemberOrThrow(activityGroup, member); groupMember.validateAccessPermission(); groupMember.updateStatus(status); activityGroupMemberService.save(groupMember); - notificationService.sendNotificationToMember( - member, - "활동 그룹 신청이 [" + status.getDescription() + "] 상태로 변경되었습니다." - ); + notificationService.sendNotificationToMember(member, "활동 그룹 신청이 [" + status.getDescription() + "] 상태로 변경되었습니다."); return groupMember.getMember().getId(); } @@ -176,10 +194,9 @@ public boolean isMemberGroupLeaderRole(ActivityGroup activityGroup, Member membe return groupMember.isLeader() && member.isAdminRole(); } - public boolean isMemberHasRoleInActivityGroup(Member member, ActivityGroupRole role, Long activityGroupId){ + public boolean isMemberHasRoleInActivityGroup(Member member, ActivityGroupRole role, Long activityGroupId) { List groupMemberList = activityGroupMemberService.getGroupMemberByMember(member); ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupByIdOrThrow(activityGroupId); - return groupMemberList.stream() .anyMatch(groupMember -> groupMember.isSameRoleAndActivityGroup(role, activityGroup)); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupBoardService.java b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupBoardService.java index cb95a13a3..1baba30db 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupBoardService.java +++ b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupBoardService.java @@ -1,13 +1,12 @@ package page.clab.api.domain.activityGroup.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.dao.ActivityGroupBoardRepository; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; @@ -22,19 +21,20 @@ import page.clab.api.domain.activityGroup.dto.response.AssignmentSubmissionWithFeedbackResponseDto; import page.clab.api.domain.activityGroup.dto.response.FeedbackResponseDto; import page.clab.api.domain.activityGroup.exception.InvalidParentBoardException; +import page.clab.api.domain.award.domain.Award; +import page.clab.api.domain.award.dto.response.AwardResponseDto; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.file.application.FileService; +import page.clab.api.global.common.file.application.UploadedFileService; import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -51,57 +51,67 @@ public class ActivityGroupBoardService { private final NotificationService notificationService; - private final FileService fileService; + private final ValidationService validationService; + + private final UploadedFileService uploadedFileService; @Transactional - public Long createActivityGroupBoard(Long parentId, Long activityGroupId, ActivityGroupBoardRequestDto dto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + public Long createActivityGroupBoard(Long parentId, Long activityGroupId, ActivityGroupBoardRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); - if (!activityGroupMemberService.isGroupMember(activityGroup, member)) { + if (!activityGroupMemberService.isGroupMember(activityGroup, currentMember)) { throw new PermissionDeniedException("활동 그룹 멤버만 게시글을 등록할 수 있습니다."); } - validateParentBoard(dto.getCategory(), parentId); - List uploadedFiles = prepareUploadedFiles(dto.getFileUrls()); + + validateParentBoard(requestDto.getCategory(), parentId); + List uploadedFiles = uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrls()); + ActivityGroupBoard parentBoard = parentId != null ? getActivityGroupBoardByIdOrThrow(parentId) : null; - ActivityGroupBoard board = ActivityGroupBoard.create(dto, member, activityGroup, parentBoard, uploadedFiles); + ActivityGroupBoard board = ActivityGroupBoardRequestDto.toEntity(requestDto, currentMember, activityGroup, parentBoard, uploadedFiles); + validationService.checkValid(board); if (parentId != null) { parentBoard.addChild(board); activityGroupBoardRepository.save(parentBoard); } activityGroupBoardRepository.save(board); - notifyMembersAboutNewBoard(activityGroupId, activityGroup, member); + + notifyMembersAboutNewBoard(activityGroupId, activityGroup, currentMember); return board.getId(); } + @Transactional(readOnly = true) public PagedResponseDto getAllActivityGroupBoard(Pageable pageable) { Page boards = activityGroupBoardRepository.findAllByOrderByCreatedAtDesc(pageable); - return new PagedResponseDto<>(boards.map(ActivityGroupBoardResponseDto::of)); + return new PagedResponseDto<>(boards.map(ActivityGroupBoardResponseDto::toDto)); } + @Transactional(readOnly = true) public ActivityGroupBoardResponseDto getActivityGroupBoardById(Long activityGroupBoardId) { ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); - return ActivityGroupBoardResponseDto.of(board); + return ActivityGroupBoardResponseDto.toDto(board); } + @Transactional(readOnly = true) public PagedResponseDto getActivityGroupBoardByCategory(Long activityGroupId, ActivityGroupBoardCategory category, Pageable pageable) { Page boards = activityGroupBoardRepository.findAllByActivityGroup_IdAndCategoryOrderByCreatedAtDesc(activityGroupId, category, pageable); - return new PagedResponseDto<>(boards.map(ActivityGroupBoardResponseDto::of)); + return new PagedResponseDto<>(boards.map(ActivityGroupBoardResponseDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getActivityGroupBoardByParent(Long parentId, Pageable pageable) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); ActivityGroupBoard parentBoard = getActivityGroupBoardByIdOrThrow(parentId); Long activityGroupId = parentBoard.getActivityGroup().getId(); - GroupMember leader = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroupId, ActivityGroupRole.LEADER); - parentBoard.validateAccessPermission(currentMember, leader); + GroupMember groupLeader = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroupId, ActivityGroupRole.LEADER); + parentBoard.validateAccessPermission(currentMember, groupLeader); List childBoards = getChildBoards(parentId); Page boards = new PageImpl<>(childBoards, pageable, childBoards.size()); - return new PagedResponseDto<>(boards.map(ActivityGroupBoardChildResponseDto::of)); + return new PagedResponseDto<>(boards.map(ActivityGroupBoardChildResponseDto::toDto)); } - @Transactional + @Transactional(readOnly = true) public List getMyAssignmentsWithFeedbacks(Long parentId) { Member currentMember = memberService.getCurrentMember(); ActivityGroupBoard parentBoard = getActivityGroupBoardByIdOrThrow(parentId); @@ -110,30 +120,39 @@ public List getMyAssignmentsWithFee .map(submission -> { List feedbackDtos = submission.getChildren().stream() .filter(ActivityGroupBoard::isFeedback) - .map(FeedbackResponseDto::of) - .collect(Collectors.toList()); - return AssignmentSubmissionWithFeedbackResponseDto.of(submission, feedbackDtos); + .map(FeedbackResponseDto::toDto) + .toList(); + return AssignmentSubmissionWithFeedbackResponseDto.toDto(submission, feedbackDtos); }) .toList(); } - public ActivityGroupBoardUpdateResponseDto updateActivityGroupBoard(Long activityGroupBoardId, ActivityGroupBoardUpdateRequestDto activityGroupBoardUpdateRequestDto) throws PermissionDeniedException { + @Transactional + public ActivityGroupBoardUpdateResponseDto updateActivityGroupBoard(Long activityGroupBoardId, ActivityGroupBoardUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); board.validateAccessPermission(currentMember); - board.update(activityGroupBoardUpdateRequestDto, fileService); + + board.update(requestDto, uploadedFileService); + validationService.checkValid(board); ActivityGroupBoard savedBoard = activityGroupBoardRepository.save(board); - return ActivityGroupBoardUpdateResponseDto.create(savedBoard); + return ActivityGroupBoardUpdateResponseDto.toDto(savedBoard); } public Long deleteActivityGroupBoard(Long activityGroupBoardId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); - board.validateAccessPermission(member); + board.validateAccessPermission(currentMember); activityGroupBoardRepository.delete(board); return board.getId(); } + @Transactional(readOnly = true) + public PagedResponseDto getDeletedActivityGroupBoards(Pageable pageable) { + Page activityGroupBoards = activityGroupBoardRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(activityGroupBoards.map(ActivityGroupBoardResponseDto::toDto)); + } + private ActivityGroupBoard getActivityGroupBoardByIdOrThrow(Long activityGroupBoardId) { return activityGroupBoardRepository.findById(activityGroupBoardId) .orElseThrow(() -> new NotFoundException("해당 게시글을 찾을 수 없습니다.")); @@ -141,19 +160,21 @@ private ActivityGroupBoard getActivityGroupBoardByIdOrThrow(Long activityGroupBo private List getChildBoards(Long activityGroupBoardId) { ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); - List allChildren = activityGroupBoardRepository.findAllChildrenByParentId(activityGroupBoardId); - allChildren.sort(Comparator.comparing(ActivityGroupBoard::getCreatedAt).reversed()); - return allChildren; + List children = activityGroupBoardRepository.findAllChildrenByParentId(activityGroupBoardId); + children.sort(Comparator.comparing(ActivityGroupBoard::getCreatedAt).reversed()); + return children; } private void validateParentBoard(ActivityGroupBoardCategory category, Long parentId) throws InvalidParentBoardException { - if (!(category == ActivityGroupBoardCategory.ASSIGNMENT || - category == ActivityGroupBoardCategory.SUBMIT || - category == ActivityGroupBoardCategory.FEEDBACK) && parentId != null) { - throw new InvalidParentBoardException("공지사항과 주차별활동 게시물은 부모 게시판을 가질 수 없습니다."); + if ((category == ActivityGroupBoardCategory.NOTICE || category == ActivityGroupBoardCategory.WEEKLY_ACTIVITY)) { + if (parentId != null) { + throw new InvalidParentBoardException(category.getDescription() + " 게시물은 부모 게시판을 가질 수 없습니다."); + } else { + return; + } } - if (parentId == null) { + if ((category == ActivityGroupBoardCategory.ASSIGNMENT || category == ActivityGroupBoardCategory.SUBMIT || category == ActivityGroupBoardCategory.FEEDBACK) && parentId == null) { throw new InvalidParentBoardException(category.getDescription() + " 게시물은 부모 게시판이 필요합니다."); } @@ -163,7 +184,7 @@ private void validateParentBoard(ActivityGroupBoardCategory category, Long paren case ASSIGNMENT -> ActivityGroupBoardCategory.WEEKLY_ACTIVITY; case SUBMIT -> ActivityGroupBoardCategory.ASSIGNMENT; case FEEDBACK -> ActivityGroupBoardCategory.SUBMIT; - default -> null; + default -> throw new InvalidParentBoardException("유효하지 않은 카테고리입니다."); }; if (parentBoard.getCategory() != expectedParentCategory) { @@ -177,14 +198,6 @@ private void validateParentBoard(ActivityGroupBoardCategory category, Long paren } } - @NotNull - private List prepareUploadedFiles(List fileUrls) { - if (fileUrls == null) return new ArrayList<>(); - return fileUrls.stream() - .map(fileService::getUploadedFileByUrl) - .collect(Collectors.toList()); - } - private void notifyMembersAboutNewBoard(Long activityGroupId, ActivityGroup activityGroup, Member member) { GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMemberOrThrow(activityGroup, member); if (groupMember.isLeader()) { @@ -192,19 +205,13 @@ private void notifyMembersAboutNewBoard(Long activityGroupId, ActivityGroup acti groupMembers .forEach(gMember -> { if (!gMember.isOwner(member)) { - notificationService.sendNotificationToMember( - gMember.getMember(), - "[" + activityGroup.getName() + "] " + member.getName() + "님이 새 게시글을 등록하였습니다." - ); + notificationService.sendNotificationToMember(gMember.getMember(), "[" + activityGroup.getName() + "] " + member.getName() + "님이 새 게시글을 등록하였습니다."); } }); } else { GroupMember groupLeader = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroupId, ActivityGroupRole.LEADER); if (groupLeader != null) { - notificationService.sendNotificationToMember( - groupLeader.getMember(), - "[" + activityGroup.getName() + "] " + member.getName() + "님이 새 게시글을 등록하였습니다." - ); + notificationService.sendNotificationToMember(groupLeader.getMember(), "[" + activityGroup.getName() + "] " + member.getName() + "님이 새 게시글을 등록하였습니다."); } } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupMemberService.java b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupMemberService.java index 3843f7c8c..da3db6f4e 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupMemberService.java +++ b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupMemberService.java @@ -1,11 +1,11 @@ package page.clab.api.domain.activityGroup.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.dao.ActivityGroupBoardRepository; import page.clab.api.domain.activityGroup.dao.ActivityGroupDetailsRepository; import page.clab.api.domain.activityGroup.dao.ActivityGroupRepository; @@ -60,11 +60,13 @@ public class ActivityGroupMemberService { private final ActivityGroupDetailsRepository activityGroupDetailsRepository; + @Transactional(readOnly = true) public PagedResponseDto getActivityGroups(Pageable pageable) { - Page activityGroupList = activityGroupRepository.findAllByOrderByCreatedAtDesc(pageable); - return new PagedResponseDto<>(activityGroupList.map(ActivityGroupResponseDto::of)); + Page activityGroups = activityGroupRepository.findAllByOrderByCreatedAtDesc(pageable); + return new PagedResponseDto<>(activityGroups.map(ActivityGroupResponseDto::toDto)); } + @Transactional(readOnly = true) public Object getActivityGroup(Long activityGroupId) { ActivityGroupDetails details = activityGroupDetailsRepository.fetchActivityGroupDetails(activityGroupId); Member currentMember = memberService.getCurrentMember(); @@ -81,31 +83,53 @@ public Object getActivityGroup(Long activityGroupId) { } } - public PagedResponseDto getActivityGroupsByStatus(ActivityGroupStatus activityGroupStatus, Pageable pageable) { - List activityGroups = activityGroupRepository.findActivityGroupsByStatus(activityGroupStatus); - List dtos = activityGroups.stream().map(activityGroup -> { + @Transactional(readOnly = true) + public PagedResponseDto getMyActivityGroups(Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + List groupMembers = getGroupMemberByMember(currentMember); + + List activityGroups = groupMembers.stream() + .filter(GroupMember::isAccepted) + .map(GroupMember::getActivityGroup) + .map(ActivityGroupResponseDto::toDto) + .toList(); + + return new PagedResponseDto<>(activityGroups, pageable, activityGroups.size()); + } + + @Transactional(readOnly = true) + public PagedResponseDto getActivityGroupsByStatus(ActivityGroupStatus status, Pageable pageable) { + List activityGroups = activityGroupRepository.findActivityGroupsByStatus(status); + + List activityGroupDtos = activityGroups.stream().map(activityGroup -> { Long participantCount = groupMemberRepository.countAcceptedMembersByActivityGroupId(activityGroup.getId()); GroupMember leader = groupMemberRepository.findLeaderByActivityGroupId(activityGroup.getId()); + Member leaderMember = leader != null ? leader.getMember() : null; Long weeklyActivityCount = activityGroupBoardRepository.countByActivityGroupIdAndCategory(activityGroup.getId(), ActivityGroupBoardCategory.WEEKLY_ACTIVITY); - return ActivityGroupStatusResponseDto.create(activityGroup, leaderMember, participantCount, weeklyActivityCount); + + return ActivityGroupStatusResponseDto.toDto(activityGroup, leaderMember, participantCount, weeklyActivityCount); }).toList(); - return new PagedResponseDto<>(dtos, pageable, dtos.size()); + + return new PagedResponseDto<>(activityGroupDtos, pageable, activityGroupDtos.size()); } + @Transactional(readOnly = true) public PagedResponseDto getActivityGroupsByCategory(ActivityGroupCategory category, Pageable pageable) { Page activityGroupList = getActivityGroupByCategory(category, pageable); - return new PagedResponseDto<>(activityGroupList.map(ActivityGroupResponseDto::of)); + return new PagedResponseDto<>(activityGroupList.map(ActivityGroupResponseDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getGroupSchedules(Long activityGroupId, Pageable pageable) { Page groupSchedules = getGroupScheduleByActivityGroupId(activityGroupId, pageable); - return new PagedResponseDto<>(groupSchedules.map(GroupScheduleDto::of)); + return new PagedResponseDto<>(groupSchedules.map(GroupScheduleDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getActivityGroupMembers(Long activityGroupId, Pageable pageable) { Page groupMembers = getGroupMemberByActivityGroupIdAndStatus(activityGroupId, GroupMemberStatus.ACCEPTED, pageable); - return new PagedResponseDto<>(groupMembers.map(GroupMemberResponseDto::of)); + return new PagedResponseDto<>(groupMembers.map(GroupMemberResponseDto::toDto)); } @Transactional @@ -116,16 +140,16 @@ public Long applyActivityGroup(Long activityGroupId, ApplyFormRequestDto formReq if (isGroupMember(activityGroup, currentMember)) { throw new AlreadyAppliedException("해당 활동에 신청한 내역이 존재합니다."); } - ApplyForm form = ApplyForm.create(formRequestDto, activityGroup, currentMember); + + ApplyForm form = ApplyFormRequestDto.toEntity(formRequestDto, activityGroup, currentMember); applyFormRepository.save(form); - GroupMember groupMember = GroupMember.create(currentMember, activityGroup, ActivityGroupRole.MEMBER, GroupMemberStatus.WAITING); + + GroupMember groupMember = GroupMember.create(currentMember, activityGroup, ActivityGroupRole.NONE, GroupMemberStatus.WAITING); groupMemberRepository.save(groupMember); + GroupMember groupLeader = getGroupMemberByActivityGroupIdAndRole(activityGroup.getId(), ActivityGroupRole.LEADER); if (groupLeader != null) { - notificationService.sendNotificationToMember( - groupLeader.getMember(), - "[" + activityGroup.getName() + "] " + currentMember.getName() + "님이 활동 참가 신청을 하였습니다." - ); + notificationService.sendNotificationToMember(groupLeader.getMember(), "[" + activityGroup.getName() + "] " + currentMember.getName() + "님이 활동 참가 신청을 하였습니다."); } return activityGroup.getId(); } @@ -165,7 +189,7 @@ public GroupMember getGroupMemberByActivityGroupIdAndRole(Long activityGroupId, .orElse(null); } - public List getGroupMemberByMember(Member member){ + public List getGroupMemberByMember(Member member) { return groupMemberRepository.findAllByMember(member); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupReportService.java b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupReportService.java index edff9fb9b..d8cf467e3 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupReportService.java +++ b/src/main/java/page/clab/api/domain/activityGroup/application/ActivityGroupReportService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.dao.ActivityGroupReportRepository; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupReport; @@ -18,64 +19,78 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor public class ActivityGroupReportService { - private final ActivityGroupReportRepository activityGroupReportRepository; - private final ActivityGroupAdminService activityGroupAdminService; private final MemberService memberService; - public Long writeReport(ActivityGroupReportRequestDto reportRequestDto) throws PermissionDeniedException, IllegalAccessException { + private final ValidationService validationService; + + private final ActivityGroupReportRepository activityGroupReportRepository; + + @Transactional + public Long writeReport(ActivityGroupReportRequestDto requestDto) throws PermissionDeniedException, IllegalAccessException { Member currentMember = memberService.getCurrentMember(); - Long activityGroupId = reportRequestDto.getActivityGroupId(); + Long activityGroupId = requestDto.getActivityGroupId(); ActivityGroup activityGroup = activityGroupAdminService.validateAndGetActivityGroupForReporting(activityGroupId, currentMember); - ActivityGroupReport report = validateReportCreationPermission(reportRequestDto, activityGroup); + ActivityGroupReport report = validateReportCreationPermission(requestDto, activityGroup); + validationService.checkValid(report); return activityGroupReportRepository.save(report).getId(); } - public PagedResponseDto getReports(Long activityGroupId, Pageable pageable){ + @Transactional(readOnly = true) + public PagedResponseDto getReports(Long activityGroupId, Pageable pageable) { ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); Page reports = activityGroupReportRepository.findAllByActivityGroup(activityGroup, pageable); - return new PagedResponseDto<>(reports.map(ActivityGroupReportResponseDto::of)); + return new PagedResponseDto<>(reports.map(ActivityGroupReportResponseDto::toDto)); } - public ActivityGroupReportResponseDto searchReport(Long activityGroupId, Long turn){ + @Transactional(readOnly = true) + public ActivityGroupReportResponseDto searchReport(Long activityGroupId, Long turn) { ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); ActivityGroupReport report = activityGroupReportRepository.findByActivityGroupAndTurn(activityGroup, turn); - return ActivityGroupReportResponseDto.of(report); + return ActivityGroupReportResponseDto.toDto(report); } - public Long updateReport(Long reportId, Long activityGroupId, ActivityGroupReportUpdateRequestDto reportRequestDto) throws PermissionDeniedException, IllegalAccessException { + @Transactional + public Long updateReport(Long reportId, Long activityGroupId, ActivityGroupReportUpdateRequestDto requestDto) throws PermissionDeniedException, IllegalAccessException { Member currentMember = memberService.getCurrentMember(); ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); validateReportUpdatePermission(activityGroupId, currentMember, activityGroup); ActivityGroupReport report = getReportByIdOrThrow(reportId); - report.update(reportRequestDto); + report.update(requestDto); + validationService.checkValid(report); return activityGroupReportRepository.save(report).getId(); } public Long deleteReport(Long reportId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); - ActivityGroupReport report = validateReportDeletionPermission(reportId, member); + Member currentMember = memberService.getCurrentMember(); + ActivityGroupReport report = validateReportDeletionPermission(reportId, currentMember); activityGroupReportRepository.delete(report); return report.getId(); } - public ActivityGroupReport getReportByIdOrThrow(Long reportId){ + @Transactional(readOnly = true) + public PagedResponseDto getDeletedActivityGroupReports(Pageable pageable) { + Page activityGroupReports = activityGroupReportRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(activityGroupReports.map(ActivityGroupReportResponseDto::toDto)); + } + + public ActivityGroupReport getReportByIdOrThrow(Long reportId) { return activityGroupReportRepository.findById(reportId) .orElseThrow(() -> new NotFoundException("활동 보고서를 찾을 수 없습니다.")); } - private ActivityGroupReport validateReportCreationPermission(ActivityGroupReportRequestDto reportRequestDto, ActivityGroup activityGroup) { - Long turn = reportRequestDto.getTurn(); - if (activityGroupReportRepository.existsByActivityGroupAndTurn(activityGroup, turn)) { + private ActivityGroupReport validateReportCreationPermission(ActivityGroupReportRequestDto requestDto, ActivityGroup activityGroup) { + if (activityGroupReportRepository.existsByActivityGroupAndTurn(activityGroup, requestDto.getTurn())) { throw new DuplicateReportException("이미 해당 차시의 보고서가 존재합니다."); } - return ActivityGroupReport.create(turn, activityGroup, reportRequestDto); + return ActivityGroupReportRequestDto.toEntity(requestDto, activityGroup); } private void validateReportUpdatePermission(Long activityGroupId, Member currentMember, ActivityGroup activityGroup) throws PermissionDeniedException, IllegalAccessException { diff --git a/src/main/java/page/clab/api/domain/activityGroup/application/AttendanceService.java b/src/main/java/page/clab/api/domain/activityGroup/application/AttendanceService.java index 5b8c5f446..1f9f1db85 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/application/AttendanceService.java +++ b/src/main/java/page/clab/api/domain/activityGroup/application/AttendanceService.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.dao.AbsentRepository; import page.clab.api.domain.activityGroup.dao.AttendanceRepository; import page.clab.api.domain.activityGroup.domain.Absent; @@ -60,9 +61,10 @@ public class AttendanceService { private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + @Transactional public String generateAttendanceQRCode(Long activityGroupId) throws IOException, WriterException, PermissionDeniedException, IllegalAccessException { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = validateAttendanceQRCodeGeneration(activityGroupId, member); + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = validateAttendanceQRCodeGeneration(activityGroupId, currentMember); String nowDateTime = LocalDateTime.now().format(dateTimeFormatter); String secretKey = googleAuthenticator.createCredentials().getKey(); @@ -70,7 +72,7 @@ public String generateAttendanceQRCode(Long activityGroupId) throws IOException, redisQRKeyRepository.save(redisQRKey); String url = generateQRCodeURL(activityGroupId, secretKey); - Attendance attendance = Attendance.create(member, activityGroup, LocalDate.parse(nowDateTime.split(" ")[0], dateFormatter)); + Attendance attendance = Attendance.create(currentMember, activityGroup, LocalDate.parse(nowDateTime.split(" ")[0], dateFormatter)); attendanceRepository.save(attendance); byte[] QRCodeImage = QRCodeUtil.encodeQRCode(url); @@ -78,61 +80,66 @@ public String generateAttendanceQRCode(Long activityGroupId) throws IOException, return fileService.saveQRCodeImage(QRCodeImage, path, 1, nowDateTime); } - public Long checkMemberAttendance(AttendanceRequestDto attendanceRequestDto) throws IllegalAccessException { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = validateMemberForAttendance(member, attendanceRequestDto.getActivityGroupId()); - validateQRCodeData(attendanceRequestDto.getQRCodeSecretKey()); - Attendance attendance = Attendance.create(member, activityGroup, LocalDate.now()); + @Transactional + public Long checkMemberAttendance(AttendanceRequestDto requestDto) throws IllegalAccessException { + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = validateMemberForAttendance(currentMember, requestDto.getActivityGroupId()); + validateQRCodeData(requestDto.getQRCodeSecretKey()); + Attendance attendance = Attendance.create(currentMember, activityGroup, LocalDate.now()); return attendanceRepository.save(attendance).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getMyAttendances(Long activityGroupId, Pageable pageable) throws IllegalAccessException { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = validateGroupAndMemberForAttendance(activityGroupId, member); - Page attendances = getAttendanceByMember(pageable, member, activityGroup); - return new PagedResponseDto<>(attendances.map(AttendanceResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = validateGroupAndMemberForAttendance(activityGroupId, currentMember); + Page attendances = getAttendanceByMember(activityGroup, currentMember, pageable); + return new PagedResponseDto<>(attendances.map(AttendanceResponseDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getGroupAttendances(Long activityGroupId, Pageable pageable) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupWithValidPermissions(activityGroupId, member); - Page attendances = getAttendanceByActivityGroup(pageable, activityGroup); - return new PagedResponseDto<>(attendances.map(AttendanceResponseDto::of)); - } - - public Long writeAbsentExcuse(AbsentRequestDto absentRequestDto) throws IllegalAccessException, DuplicateAbsentExcuseException { - Member absentee = memberService.getMemberByIdOrThrow(absentRequestDto.getAbsenteeId()); - ActivityGroup activityGroup = getValidActivityGroup(absentRequestDto.getActivityGroupId()); - validateAbsentExcuseConditions(absentee, activityGroup, absentRequestDto.getAbsentDate()); - Absent absent = Absent.create(absentee, activityGroup, absentRequestDto); + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = getActivityGroupWithValidPermissions(activityGroupId, currentMember); + Page attendances = getAttendanceByActivityGroup(activityGroup, pageable); + return new PagedResponseDto<>(attendances.map(AttendanceResponseDto::toDto)); + } + + @Transactional + public Long writeAbsentExcuse(AbsentRequestDto requestDto) throws IllegalAccessException, DuplicateAbsentExcuseException { + Member absentee = memberService.getMemberByIdOrThrow(requestDto.getAbsenteeId()); + ActivityGroup activityGroup = getValidActivityGroup(requestDto.getActivityGroupId()); + validateAbsentExcuseConditions(absentee, activityGroup, requestDto.getAbsentDate()); + Absent absent = AbsentRequestDto.toEntity(requestDto, absentee, activityGroup); return absentRepository.save(absent).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getActivityGroupAbsentExcuses(Long activityGroupId, Pageable pageable) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupWithPermissionCheck(activityGroupId, member); + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = getActivityGroupWithPermissionCheck(activityGroupId, currentMember); Page absents = absentRepository.findAllByActivityGroup(activityGroup, pageable); - return new PagedResponseDto<>(absents.map(AbsentResponseDto::of)); + return new PagedResponseDto<>(absents.map(AbsentResponseDto::toDto)); } - private Page getAttendanceByMember(Pageable pageable, Member member, ActivityGroup activityGroup){ + private Page getAttendanceByMember(ActivityGroup activityGroup, Member member, Pageable pageable) { return attendanceRepository.findAllByMemberAndActivityGroupOrderByCreatedAt(member, activityGroup, pageable); } - private Page getAttendanceByActivityGroup(Pageable pageable, ActivityGroup activityGroup){ + private Page getAttendanceByActivityGroup(ActivityGroup activityGroup, Pageable pageable) { return attendanceRepository.findAllByActivityGroupOrderByActivityDateAscMemberAsc(activityGroup, pageable); } - public boolean hasAttendanceHistory(ActivityGroup activityGroup, Member member, LocalDate activityDate){ + public boolean hasAttendanceHistory(ActivityGroup activityGroup, Member member, LocalDate activityDate) { Attendance attendanceHistory = attendanceRepository.findByActivityGroupAndMemberAndActivityDate(activityGroup, member, activityDate); return attendanceHistory != null; } - public boolean isActivityExistedAt(ActivityGroup activityGroup, LocalDate date){ + public boolean isActivityExistedAt(ActivityGroup activityGroup, LocalDate date) { return attendanceRepository.existsByActivityGroupAndActivityDate(activityGroup, date); } - public boolean hasAbsentExcuseHistory(ActivityGroup activityGroup, Member absentee, LocalDate absentDate){ + public boolean hasAbsentExcuseHistory(ActivityGroup activityGroup, Member absentee, LocalDate absentDate) { Absent absentHistory = absentRepository.findByActivityGroupAndAbsenteeAndAbsentDate(activityGroup, absentee, absentDate); return absentHistory != null; } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupBoardRepository.java b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupBoardRepository.java index 5b0ca23b0..9476ebffa 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupBoardRepository.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupBoardRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; @@ -21,4 +22,7 @@ public interface ActivityGroupBoardRepository extends JpaRepository findAllChildrenByParentId(Long activityGroupBoardId); + @Query(value = "SELECT a.* FROM activity_group_board a WHERE a.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupDetailsRepositoryImpl.java b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupDetailsRepositoryImpl.java index a6de90ec6..647679c60 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupDetailsRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupDetailsRepositoryImpl.java @@ -1,5 +1,6 @@ package page.clab.api.domain.activityGroup.dao; +import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -22,25 +23,34 @@ public class ActivityGroupDetailsRepositoryImpl implements ActivityGroupDetailsR @Override public ActivityGroupDetails fetchActivityGroupDetails(Long activityGroupId) { - QActivityGroup activityGroup = QActivityGroup.activityGroup; - QGroupMember groupMember = QGroupMember.groupMember; - QActivityGroupBoard activityGroupBoard = QActivityGroupBoard.activityGroupBoard; + QActivityGroup qActivityGroup = QActivityGroup.activityGroup; + QGroupMember qGroupMember = QGroupMember.groupMember; + QActivityGroupBoard qActivityGroupBoard = QActivityGroupBoard.activityGroupBoard; - List groupMembers = queryFactory.selectFrom(groupMember) - .where(groupMember.activityGroup.id.eq(activityGroupId)) + BooleanBuilder groupMemberCondition = new BooleanBuilder(); + if (activityGroupId != null) groupMemberCondition.and(qGroupMember.activityGroup.id.eq(activityGroupId)); + + List groupMembers = queryFactory.selectFrom(qGroupMember) + .where(groupMemberCondition) .fetch(); - List boards = queryFactory.selectFrom(activityGroupBoard) - .where(activityGroupBoard.activityGroup.id.eq(activityGroupId), - activityGroupBoard.category.in(ActivityGroupBoardCategory.NOTICE, ActivityGroupBoardCategory.WEEKLY_ACTIVITY, ActivityGroupBoardCategory.ASSIGNMENT)) - .orderBy(activityGroupBoard.createdAt.desc()) + BooleanBuilder boardCondition = new BooleanBuilder(); + if (activityGroupId != null) boardCondition.and(qActivityGroupBoard.activityGroup.id.eq(activityGroupId)); + if (activityGroupId != null) boardCondition.and(qActivityGroupBoard.category.in(ActivityGroupBoardCategory.NOTICE, ActivityGroupBoardCategory.WEEKLY_ACTIVITY, ActivityGroupBoardCategory.ASSIGNMENT)); + + List boards = queryFactory.selectFrom(qActivityGroupBoard) + .where(boardCondition) + .orderBy(qActivityGroupBoard.createdAt.desc()) .fetch(); - ActivityGroup foundActivityGroup = queryFactory.selectFrom(activityGroup) - .where(activityGroup.id.eq(activityGroupId)) + BooleanBuilder activityGroupCondition = new BooleanBuilder(); + if (activityGroupId != null) activityGroupCondition.and(qActivityGroup.id.eq(activityGroupId)); + + ActivityGroup foundActivityGroup = queryFactory.selectFrom(qActivityGroup) + .where(activityGroupCondition) .fetchOne(); - return new ActivityGroupDetails(foundActivityGroup, groupMembers, boards); + return ActivityGroupDetails.create(foundActivityGroup, groupMembers, boards); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupReportRepository.java b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupReportRepository.java index 2bdac0bee..3b2ac3eb0 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupReportRepository.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupReportRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupReport; @@ -16,4 +17,7 @@ public interface ActivityGroupReportRepository extends JpaRepository findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepository.java b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepository.java index 9db96c827..bcf619698 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepository.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.activityGroup.domain.ActivityGroup; @@ -15,4 +16,7 @@ public interface ActivityGroupRepository extends JpaRepository findAllByCategoryOrderByCreatedAtDesc(ActivityGroupCategory category, Pageable pageable); + @Query(value = "SELECT a.* FROM activity_group a WHERE a.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepositoryCustomImpl.java b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepositoryCustomImpl.java index 1d8778097..1b6450e05 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepositoryCustomImpl.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dao/ActivityGroupRepositoryCustomImpl.java @@ -1,5 +1,6 @@ package page.clab.api.domain.activityGroup.dao; +import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -18,8 +19,12 @@ public class ActivityGroupRepositoryCustomImpl implements ActivityGroupRepositor @Override public List findActivityGroupsByStatus(ActivityGroupStatus status) { QActivityGroup qActivityGroup = QActivityGroup.activityGroup; + BooleanBuilder builder = new BooleanBuilder(); + + if (status != null) builder.and(qActivityGroup.status.eq(status)); + return queryFactory.selectFrom(qActivityGroup) - .where(qActivityGroup.status.eq(status)) + .where(builder) .fetch(); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dao/GroupMemberRepositoryImpl.java b/src/main/java/page/clab/api/domain/activityGroup/dao/GroupMemberRepositoryImpl.java index 2f67f01e2..e80f04960 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dao/GroupMemberRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dao/GroupMemberRepositoryImpl.java @@ -1,5 +1,6 @@ package page.clab.api.domain.activityGroup.dao; +import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -17,18 +18,26 @@ public class GroupMemberRepositoryImpl implements GroupMemberRepositoryCustom { @Override public long countAcceptedMembersByActivityGroupId(Long activityGroupId) { QGroupMember qGroupMember = QGroupMember.groupMember; + BooleanBuilder builder = new BooleanBuilder(); + + if (activityGroupId != null) builder.and(qGroupMember.activityGroup.id.eq(activityGroupId)); + builder.and(qGroupMember.status.eq(GroupMemberStatus.ACCEPTED)); + return queryFactory.selectFrom(qGroupMember) - .where(qGroupMember.activityGroup.id.eq(activityGroupId) - .and(qGroupMember.status.eq(GroupMemberStatus.ACCEPTED))) + .where(builder) .fetchCount(); } @Override public GroupMember findLeaderByActivityGroupId(Long activityGroupId) { QGroupMember qGroupMember = QGroupMember.groupMember; + BooleanBuilder builder = new BooleanBuilder(); + + if (activityGroupId != null) builder.and(qGroupMember.activityGroup.id.eq(activityGroupId)); + if (activityGroupId != null) builder.and(qGroupMember.role.eq(ActivityGroupRole.LEADER)); + return queryFactory.selectFrom(qGroupMember) - .where(qGroupMember.activityGroup.id.eq(activityGroupId) - .and(qGroupMember.role.eq(ActivityGroupRole.LEADER))) + .where(builder) .fetchOne(); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/Absent.java b/src/main/java/page/clab/api/domain/activityGroup/domain/Absent.java index ba80848d1..ecb54d056 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/Absent.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/Absent.java @@ -1,20 +1,21 @@ package page.clab.api.domain.activityGroup.domain; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import lombok.ToString; -import page.clab.api.domain.activityGroup.dto.request.AbsentRequestDto; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDate; @@ -22,10 +23,10 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -@ToString -public class Absent { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(callSuper = false) +public class Absent extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -39,20 +40,10 @@ public class Absent { @JoinColumn(name = "activity_group_id", nullable = false) private ActivityGroup activityGroup; - @NotNull + @Column(nullable = false) private LocalDate absentDate; - @NotNull + @Column(nullable = false) private String reason; - public static Absent create(Member absentee, ActivityGroup activityGroup, AbsentRequestDto absentRequestDto) { - return Absent.builder() - .id(null) - .absentee(absentee) - .activityGroup(activityGroup) - .absentDate(absentRequestDto.getAbsentDate()) - .reason(absentRequestDto.getReason()) - .build(); - } - } diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroup.java b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroup.java index 65bd918ba..a439a3c05 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroup.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroup.java @@ -8,30 +8,32 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.validator.constraints.Range; import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.activityGroup.dto.request.ActivityGroupRequestDto; import page.clab.api.domain.activityGroup.dto.request.ActivityGroupUpdateRequestDto; import page.clab.api.domain.activityGroup.exception.ActivityGroupNotProgressingException; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.Optional; @Entity(name = "activity_group") @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class ActivityGroup { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE activity_group SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class ActivityGroup extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -72,28 +74,17 @@ public class ActivityGroup { @URL(message = "{url.activityGroup.githubUrl}") private String githubUrl; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static ActivityGroup create(ActivityGroupRequestDto activityGroupRequestDto) { - ActivityGroup activityGroup = ModelMapperUtil.getModelMapper().map(activityGroupRequestDto, ActivityGroup.class); - activityGroup.setStatus(ActivityGroupStatus.WAITING); - activityGroup.setProgress(0L); - return activityGroup; - } - - public void update(ActivityGroupUpdateRequestDto dto) { - Optional.ofNullable(dto.getCategory()).ifPresent(this::setCategory); - Optional.ofNullable(dto.getSubject()).ifPresent(this::setSubject); - Optional.ofNullable(dto.getName()).ifPresent(this::setName); - Optional.ofNullable(dto.getContent()).ifPresent(this::setContent); - Optional.ofNullable(dto.getImageUrl()).ifPresent(this::setImageUrl); - Optional.ofNullable(dto.getCurriculum()).ifPresent(this::setCurriculum); - Optional.ofNullable(dto.getStartDate()).ifPresent(this::setStartDate); - Optional.ofNullable(dto.getEndDate()).ifPresent(this::setEndDate); - Optional.ofNullable(dto.getTechStack()).ifPresent(this::setTechStack); - Optional.ofNullable(dto.getGithubUrl()).ifPresent(this::setGithubUrl); + public void update(ActivityGroupUpdateRequestDto requestDto) { + Optional.ofNullable(requestDto.getCategory()).ifPresent(this::setCategory); + Optional.ofNullable(requestDto.getSubject()).ifPresent(this::setSubject); + Optional.ofNullable(requestDto.getName()).ifPresent(this::setName); + Optional.ofNullable(requestDto.getContent()).ifPresent(this::setContent); + Optional.ofNullable(requestDto.getImageUrl()).ifPresent(this::setImageUrl); + Optional.ofNullable(requestDto.getCurriculum()).ifPresent(this::setCurriculum); + Optional.ofNullable(requestDto.getStartDate()).ifPresent(this::setStartDate); + Optional.ofNullable(requestDto.getEndDate()).ifPresent(this::setEndDate); + Optional.ofNullable(requestDto.getTechStack()).ifPresent(this::setTechStack); + Optional.ofNullable(requestDto.getGithubUrl()).ifPresent(this::setGithubUrl); } public boolean isWaiting() { @@ -112,6 +103,10 @@ public void updateStatus(ActivityGroupStatus status) { this.status = status; } + public void updateProgress(Long progress) { + this.progress = progress; + } + public boolean isStudy() { return this.category.equals(ActivityGroupCategory.STUDY); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupBoard.java b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupBoard.java index 87fda816b..8f7843b22 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupBoard.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupBoard.java @@ -12,33 +12,35 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.activityGroup.dto.request.ActivityGroupBoardRequestDto; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.activityGroup.dto.request.ActivityGroupBoardUpdateRequestDto; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.common.file.application.FileService; +import page.clab.api.global.common.domain.BaseEntity; +import page.clab.api.global.common.file.application.UploadedFileService; import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class ActivityGroupBoard { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE activity_group_board SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class ActivityGroupBoard extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -72,40 +74,15 @@ public class ActivityGroupBoard { @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "activity_group_board_files") - private List uploadedFiles = new ArrayList<>(); + private List uploadedFiles; - @Column(name = "dueDate_time") private LocalDateTime dueDateTime; - @Column(name = "update_time") - private LocalDateTime updateTime; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static ActivityGroupBoard create(ActivityGroupBoardRequestDto dto, Member member, ActivityGroup activityGroup, ActivityGroupBoard parent, List uploadedFiles) { - ActivityGroupBoard activityGroupBoard = ModelMapperUtil.getModelMapper().map(dto, ActivityGroupBoard.class); - activityGroupBoard.setMember(member); - activityGroupBoard.setActivityGroup(activityGroup); - activityGroupBoard.setParent(parent); - activityGroupBoard.setUploadedFiles(uploadedFiles); - return activityGroupBoard; - } - - - public void update(ActivityGroupBoardUpdateRequestDto dto, FileService fileService) { - Optional.ofNullable(dto.getTitle()).ifPresent(this::setTitle); - Optional.ofNullable(dto.getContent()).ifPresent(this::setContent); - Optional.ofNullable(dto.getDueDateTime()).ifPresent(this::setDueDateTime); - Optional.ofNullable(dto.getFileUrls()) - .ifPresent(urls -> { - List uploadedFiles = urls.stream() - .map(fileService::getUploadedFileByUrl) - .collect(Collectors.toList()); - setUploadedFiles(uploadedFiles); - }); - this.updateTime = LocalDateTime.now(); + public void update(ActivityGroupBoardUpdateRequestDto requestDto, UploadedFileService uploadedFileService) { + Optional.ofNullable(requestDto.getTitle()).ifPresent(this::setTitle); + Optional.ofNullable(requestDto.getContent()).ifPresent(this::setContent); + Optional.ofNullable(requestDto.getDueDateTime()).ifPresent(this::setDueDateTime); + Optional.ofNullable(requestDto.getFileUrls()).ifPresent(urls -> { this.setUploadedFiles(uploadedFileService.getUploadedFilesByUrls(urls)); }); } public void addChild(ActivityGroupBoard child) { diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupReport.java b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupReport.java index 05e58f021..f98918023 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupReport.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupReport.java @@ -7,60 +7,46 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.activityGroup.dto.request.ActivityGroupReportRequestDto; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.activityGroup.dto.request.ActivityGroupReportUpdateRequestDto; +import page.clab.api.global.common.domain.BaseEntity; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class ActivityGroupReport { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE activity_group_report SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class ActivityGroupReport extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @NotNull + @Column(nullable = false) private Long turn; @ManyToOne @JoinColumn(name = "activity_group_id", nullable = false) private ActivityGroup activityGroup; - @NotNull + @Column(nullable = false) private String title; - @NotNull + @Column(nullable = false) private String content; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @Column(name = "update_time") - private LocalDateTime updateTime; - - public static ActivityGroupReport create(Long turn, ActivityGroup activityGroup, ActivityGroupReportRequestDto reportRequestDto) { - return ActivityGroupReport.builder() - .turn(turn) - .activityGroup(activityGroup) - .title(reportRequestDto.getTitle()) - .content(reportRequestDto.getContent()) - .build(); - } - public void update(ActivityGroupReportUpdateRequestDto reportRequestDto) { Optional.ofNullable(reportRequestDto.getTurn()).ifPresent(this::setTurn); Optional.ofNullable(reportRequestDto.getTitle()).ifPresent(this::setTitle); diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupRole.java b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupRole.java index 59223d204..d00cd6fd0 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupRole.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/ActivityGroupRole.java @@ -8,7 +8,8 @@ public enum ActivityGroupRole { LEADER("LEADER", "Leader"), - MEMBER("MEMBER", "Member"); + MEMBER("MEMBER", "Member"), + NONE("NONE", "None"); private String key; private String description; diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/ApplyForm.java b/src/main/java/page/clab/api/domain/activityGroup/domain/ApplyForm.java index 9ecb100dd..f8640c40f 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/ApplyForm.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/ApplyForm.java @@ -7,25 +7,22 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.activityGroup.dto.request.ApplyFormRequestDto; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; - -import java.time.LocalDateTime; +import page.clab.api.global.common.domain.BaseEntity; @Getter @Setter @Entity @Builder -@AllArgsConstructor -@NoArgsConstructor -public class ApplyForm { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApplyForm extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -42,14 +39,4 @@ public class ApplyForm { @Column(nullable = false) private String applyReason; - @CreationTimestamp - private LocalDateTime createdAt; - - public static ApplyForm create(ApplyFormRequestDto applyFormRequestDto, ActivityGroup activityGroup, Member member) { - ApplyForm applyForm = ModelMapperUtil.getModelMapper().map(applyFormRequestDto, ApplyForm.class); - applyForm.setActivityGroup(activityGroup); - applyForm.setMember(member); - return applyForm; - } - } diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/Attendance.java b/src/main/java/page/clab/api/domain/activityGroup/domain/Attendance.java index b6173ae7b..ea1e67aca 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/Attendance.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/Attendance.java @@ -7,24 +7,24 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDate; -import java.time.LocalDateTime; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Attendance { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Attendance extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -38,13 +38,9 @@ public class Attendance { @JoinColumn(name = "group_id") private ActivityGroup activityGroup; - @Column(name = "activity_date", nullable = false) + @Column(nullable = false) private LocalDate activityDate; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - public static Attendance create(Member member, ActivityGroup activityGroup, LocalDate activityDate) { return Attendance.builder() .member(member) diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMember.java b/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMember.java index 25ce41f08..60b53828d 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMember.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMember.java @@ -8,6 +8,7 @@ import jakarta.persistence.IdClass; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -15,15 +16,16 @@ import lombok.Setter; import page.clab.api.domain.activityGroup.exception.LeaderStatusChangeNotAllowedException; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @IdClass(GroupMemberId.class) -public class GroupMember { +public class GroupMember extends BaseEntity { @Id @ManyToOne @@ -88,7 +90,7 @@ public void updateStatus(GroupMemberStatus status) { if (this.isAccepted()) { this.updateRole(ActivityGroupRole.MEMBER); } else { - this.updateRole(null); + this.updateRole(ActivityGroupRole.NONE); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMemberId.java b/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMemberId.java index 905869872..abe8138d6 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMemberId.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/GroupMemberId.java @@ -1,17 +1,15 @@ package page.clab.api.domain.activityGroup.domain; import jakarta.persistence.Embeddable; -import java.io.Serializable; +import lombok.AccessLevel; import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor +import java.io.Serializable; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Embeddable public class GroupMemberId implements Serializable { diff --git a/src/main/java/page/clab/api/domain/activityGroup/domain/GroupSchedule.java b/src/main/java/page/clab/api/domain/activityGroup/domain/GroupSchedule.java index 148f69be2..0bd15e8df 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/domain/GroupSchedule.java +++ b/src/main/java/page/clab/api/domain/activityGroup/domain/GroupSchedule.java @@ -7,21 +7,23 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import java.time.LocalDateTime; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import page.clab.api.domain.activityGroup.dto.param.GroupScheduleDto; +import page.clab.api.global.common.domain.BaseEntity; + +import java.time.LocalDateTime; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class GroupSchedule { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class GroupSchedule extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -37,12 +39,4 @@ public class GroupSchedule { @Column(nullable = false) private String content; - public static GroupSchedule of(ActivityGroup activityGroup, GroupScheduleDto groupScheduleDto) { - return GroupSchedule.builder() - .activityGroup(activityGroup) - .schedule(groupScheduleDto.getSchedule()) - .content(groupScheduleDto.getContent()) - .build(); - } - } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/param/ActivityGroupDetails.java b/src/main/java/page/clab/api/domain/activityGroup/dto/param/ActivityGroupDetails.java index 20eeff1fd..ea14b6c93 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/param/ActivityGroupDetails.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/param/ActivityGroupDetails.java @@ -1,9 +1,6 @@ package page.clab.api.domain.activityGroup.dto.param; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; @@ -13,9 +10,6 @@ @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupDetails { private ActivityGroup activityGroup; @@ -24,4 +18,14 @@ public class ActivityGroupDetails { private List activityGroupBoards; + private ActivityGroupDetails(ActivityGroup activityGroup, List groupMembers, List activityGroupBoards) { + this.activityGroup = activityGroup; + this.groupMembers = groupMembers; + this.activityGroupBoards = activityGroupBoards; + } + + public static ActivityGroupDetails create(ActivityGroup activityGroup, List groupMembers, List activityGroupBoards) { + return new ActivityGroupDetails(activityGroup, groupMembers, activityGroupBoards); + } + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/param/GroupScheduleDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/param/GroupScheduleDto.java index b38dcfffa..d95c143e8 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/param/GroupScheduleDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/param/GroupScheduleDto.java @@ -1,18 +1,15 @@ package page.clab.api.domain.activityGroup.dto.param; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.GroupSchedule; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class GroupScheduleDto { @@ -20,7 +17,19 @@ public class GroupScheduleDto { private String content; - public static GroupScheduleDto of(GroupSchedule groupSchedule) { - return ModelMapperUtil.getModelMapper().map(groupSchedule, GroupScheduleDto.class); + public static GroupScheduleDto toDto(GroupSchedule groupSchedule) { + return GroupScheduleDto.builder() + .schedule(groupSchedule.getSchedule()) + .content(groupSchedule.getContent()) + .build(); } + + public static GroupSchedule toEntity(GroupScheduleDto groupScheduleDto, ActivityGroup activityGroup) { + return GroupSchedule.builder() + .activityGroup(activityGroup) + .schedule(groupScheduleDto.getSchedule()) + .content(groupScheduleDto.getContent()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/AbsentRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/AbsentRequestDto.java index e692ab715..116514b20 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/AbsentRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/AbsentRequestDto.java @@ -2,20 +2,16 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.activityGroup.domain.Absent; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; +import page.clab.api.domain.member.domain.Member; + +import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class AbsentRequestDto { @NotNull(message = "{notNull.absent.absenteeId}") @@ -34,8 +30,14 @@ public class AbsentRequestDto { @Schema(description = "불참 날짜", example = "2023-11-12", required = true) private LocalDate absentDate; - public static AbsentRequestDto of(Absent absent){ - return ModelMapperUtil.getModelMapper().map(absent, AbsentRequestDto.class); + public static Absent toEntity(AbsentRequestDto requestDto, Member absentee, ActivityGroup activityGroup) { + return Absent.builder() + .absentee(absentee) + .activityGroup(activityGroup) + .absentDate(requestDto.getAbsentDate()) + .reason(requestDto.getReason()) + .build(); + } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardRequestDto.java index 6355060df..d25be53b0 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardRequestDto.java @@ -2,29 +2,25 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; +import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoardCategory; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.file.domain.UploadedFile; import java.time.LocalDateTime; import java.util.List; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupBoardRequestDto { @NotNull(message = "{notNull.activityGroupBoard.category}") @Schema(description = "카테고리", example = "NOTICE") private ActivityGroupBoardCategory category; - @Size(min = 1, max = 100, message = "{size.activityGroupBoard.title}") @Schema(description = "제목", example = "C언어 스터디 과제 제출 관련 공지") private String title; @@ -37,4 +33,17 @@ public class ActivityGroupBoardRequestDto { @Schema(description = "마감일자", example = "2024-11-28 18:00:00.000") private LocalDateTime dueDateTime; + public static ActivityGroupBoard toEntity(ActivityGroupBoardRequestDto requestDto, Member member, ActivityGroup activityGroup, ActivityGroupBoard parentBoard, List uploadedFiles) { + return ActivityGroupBoard.builder() + .activityGroup(activityGroup) + .member(member) + .category(requestDto.getCategory()) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .parent(parentBoard) + .uploadedFiles(uploadedFiles) + .dueDateTime(requestDto.getDueDateTime()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardUpdateRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardUpdateRequestDto.java index 3d0bb2cc0..20444dc84 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupBoardUpdateRequestDto.java @@ -1,11 +1,7 @@ package page.clab.api.domain.activityGroup.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; @@ -13,12 +9,8 @@ @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupBoardUpdateRequestDto { - @Size(min = 1, max = 100, message = "{size.activityGroupBoard.title}") @Schema(description = "제목", example = "C언어 스터디 과제 제출 관련 공지") private String title; diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportRequestDto.java index 17f0e2eed..287754a90 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportRequestDto.java @@ -2,17 +2,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; +import page.clab.api.domain.activityGroup.domain.ActivityGroupReport; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupReportRequestDto { @NotNull(message = "notNull.report.turn") @@ -31,4 +27,13 @@ public class ActivityGroupReportRequestDto { @Schema(description = "내용", example = "변수, 자료형에 대해 공부", required = true) private String content; + public static ActivityGroupReport toEntity(ActivityGroupReportRequestDto requestDto, ActivityGroup activityGroup) { + return ActivityGroupReport.builder() + .turn(requestDto.getTurn()) + .activityGroup(activityGroup) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportUpdateRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportUpdateRequestDto.java index f9942f696..d44169098 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupReportUpdateRequestDto.java @@ -1,17 +1,11 @@ package page.clab.api.domain.activityGroup.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupReportUpdateRequestDto { @Schema(description = "차시", example = "1") diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupRequestDto.java index 4dc4fba4c..e04117ad1 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupRequestDto.java @@ -2,23 +2,16 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupCategory; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.activityGroup.domain.ActivityGroupStatus; + +import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupRequestDto { @NotNull(message = "{notnull.activityGroup.category}") @@ -29,12 +22,10 @@ public class ActivityGroupRequestDto { private String subject; @NotNull(message = "{notnull.activityGroup.name}") - @Size(min = 1, max = 30, message = "{size.activityGroup.name}") @Schema(description = "활동명", example = "2024-1 신입생 대상 C언어 스터디") private String name; @NotNull(message = "{notnull.activityGroup.content}") - @Size(min = 1, max = 1000, message = "{size.activityGroup.content}") @Schema(description = "활동 설명", example = "2024-1 신입생 대상 C언어 스터디") private String content; @@ -53,12 +44,24 @@ public class ActivityGroupRequestDto { @Schema(description = "기술 스택", example = "Unreal Engine, C#") private String techStack; - @URL(message = "{url.activityGroup.githubUrl}") @Schema(description = "Github URL", example = "https://github.com/KGU-C-Lab") private String githubUrl; - public static ActivityGroupRequestDto of(ActivityGroup activityGroup) { - return ModelMapperUtil.getModelMapper().map(activityGroup, ActivityGroupRequestDto.class); + public static ActivityGroup toEntity(ActivityGroupRequestDto requestDto) { + return ActivityGroup.builder() + .category(requestDto.getCategory()) + .subject(requestDto.getSubject()) + .name(requestDto.getName()) + .content(requestDto.getContent()) + .status(ActivityGroupStatus.WAITING) + .progress(0L) + .imageUrl(requestDto.getImageUrl()) + .curriculum(requestDto.getCurriculum()) + .startDate(requestDto.getStartDate()) + .endDate(requestDto.getEndDate()) + .techStack(requestDto.getTechStack()) + .githubUrl(requestDto.getGithubUrl()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupUpdateRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupUpdateRequestDto.java index 1c67becd3..32b123aca 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ActivityGroupUpdateRequestDto.java @@ -1,24 +1,14 @@ package page.clab.api.domain.activityGroup.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupCategory; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityGroupUpdateRequestDto { private ActivityGroupCategory category; @@ -26,11 +16,9 @@ public class ActivityGroupUpdateRequestDto { @Schema(description = "활동 대상", example = "1학년 이상") private String subject; - @Size(min = 1, max = 30, message = "{size.activityGroup.name}") @Schema(description = "활동명", example = "2024-1 신입생 대상 C언어 스터디") private String name; - @Size(min = 1, max = 1000, message = "{size.activityGroup.content}") @Schema(description = "활동 설명", example = "2024-1 신입생 대상 C언어 스터디") private String content; @@ -49,12 +37,7 @@ public class ActivityGroupUpdateRequestDto { @Schema(description = "기술 스택", example = "Unreal Engine, C#") private String techStack; - @URL(message = "{url.activityGroup.githubUrl}") @Schema(description = "Github URL", example = "https://github.com/KGU-C-Lab") private String githubUrl; - public static ActivityGroupUpdateRequestDto of(ActivityGroup activityGroup) { - return ModelMapperUtil.getModelMapper().map(activityGroup, ActivityGroupUpdateRequestDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ApplyFormRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ApplyFormRequestDto.java index b562f1be4..3f13c85e8 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/ApplyFormRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/ApplyFormRequestDto.java @@ -2,21 +2,26 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; +import page.clab.api.domain.activityGroup.domain.ApplyForm; +import page.clab.api.domain.member.domain.Member; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ApplyFormRequestDto { @NotNull(message = "{notnull.applyForm.applyReason}") @Schema(description = "지원 동기", example = "백엔드에 관심이 있어서") private String applyReason; + public static ApplyForm toEntity(ApplyFormRequestDto requestDto, ActivityGroup activityGroup, Member member) { + return ApplyForm.builder() + .activityGroup(activityGroup) + .member(member) + .applyReason(requestDto.getApplyReason()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/request/AttendanceRequestDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/request/AttendanceRequestDto.java index 6313d5be9..dafce907e 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/request/AttendanceRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/request/AttendanceRequestDto.java @@ -1,16 +1,10 @@ package page.clab.api.domain.activityGroup.dto.request; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class AttendanceRequestDto { private Long activityGroupId; diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/AbsentResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/AbsentResponseDto.java index 4bbd7b662..ee0397f30 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/AbsentResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/AbsentResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.activityGroup.dto.response; -import java.time.LocalDate; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.Absent; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class AbsentResponseDto { @@ -28,8 +22,15 @@ public class AbsentResponseDto { private LocalDate absentDate; - public static AbsentResponseDto of(Absent absent) { - return ModelMapperUtil.getModelMapper().map(absent, AbsentResponseDto.class); + public static AbsentResponseDto toDto(Absent absent) { + return AbsentResponseDto.builder() + .absenteeId(absent.getAbsentee().getId()) + .absenteeName(absent.getAbsentee().getName()) + .activityGroupId(absent.getActivityGroup().getId()) + .activityGroupName(absent.getActivityGroup().getName()) + .reason(absent.getReason()) + .absentDate(absent.getAbsentDate()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardChildResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardChildResponseDto.java index 925d9dde5..7d58a6dba 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardChildResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardChildResponseDto.java @@ -1,22 +1,15 @@ package page.clab.api.domain.activityGroup.dto.response; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoardCategory; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; +import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupBoardChildResponseDto { @@ -30,16 +23,26 @@ public class ActivityGroupBoardChildResponseDto { private LocalDateTime dueDateTime; - private LocalDateTime updateTime; + private LocalDateTime updatedAt; private LocalDateTime createdAt; - private List files = new ArrayList<>(); + private List files; private List children; - public static ActivityGroupBoardChildResponseDto of(ActivityGroupBoard activityGroupBoard) { - return ModelMapperUtil.getModelMapper().map(activityGroupBoard, ActivityGroupBoardChildResponseDto.class); + public static ActivityGroupBoardChildResponseDto toDto(ActivityGroupBoard board) { + return ActivityGroupBoardChildResponseDto.builder() + .id(board.getId()) + .category(board.getCategory()) + .title(board.getTitle()) + .content(board.getContent()) + .dueDateTime(board.getDueDateTime()) + .updatedAt(board.getUpdatedAt()) + .createdAt(board.getCreatedAt()) + .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) + .children(board.getChildren().stream().map(ActivityGroupBoardChildResponseDto::toDto).toList()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardResponseDto.java index 393466968..64ed81bf2 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardResponseDto.java @@ -1,23 +1,15 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoardCategory; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupBoardResponseDto { @@ -31,18 +23,26 @@ public class ActivityGroupBoardResponseDto { private String content; - private List files = new ArrayList<>(); + private List files; private LocalDateTime dueDateTime; private LocalDateTime createdAt; - private LocalDateTime updateTime; - - public static ActivityGroupBoardResponseDto of(ActivityGroupBoard activityGroupBoard) { - ActivityGroupBoardResponseDto activityGroupBoardResponseDto = ModelMapperUtil.getModelMapper().map(activityGroupBoard, ActivityGroupBoardResponseDto.class); - activityGroupBoardResponseDto.setParentId(activityGroupBoard.getParent() != null ? activityGroupBoard.getParent().getId() : null); - return activityGroupBoardResponseDto; + private LocalDateTime updatedAt; + + public static ActivityGroupBoardResponseDto toDto(ActivityGroupBoard board) { + return ActivityGroupBoardResponseDto.builder() + .id(board.getId()) + .parentId(board.getParent() != null ? board.getParent().getId() : null) + .category(board.getCategory()) + .title(board.getTitle()) + .content(board.getContent()) + .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) + .dueDateTime(board.getDueDateTime()) + .createdAt(board.getCreatedAt()) + .updatedAt(board.getUpdatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardUpdateResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardUpdateResponseDto.java index 857ded0c8..b948538d7 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardUpdateResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupBoardUpdateResponseDto.java @@ -1,16 +1,10 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupBoardUpdateResponseDto { @@ -18,7 +12,7 @@ public class ActivityGroupBoardUpdateResponseDto { private Long parentId; - public static ActivityGroupBoardUpdateResponseDto create(ActivityGroupBoard board) { + public static ActivityGroupBoardUpdateResponseDto toDto(ActivityGroupBoard board) { return ActivityGroupBoardUpdateResponseDto.builder() .id(board.getId()) .parentId(board.getParent() != null ? board.getParent().getId() : null) diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java index 09099534e..18906ca64 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.GroupMember; import page.clab.api.domain.activityGroup.domain.GroupMemberStatus; @Getter -@Setter @Builder -@NoArgsConstructor -@AllArgsConstructor public class ActivityGroupMemberWithApplyReasonResponseDto { private String memberId; diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupProjectResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupProjectResponseDto.java index 7e1127882..3291a0487 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupProjectResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupProjectResponseDto.java @@ -1,26 +1,19 @@ package page.clab.api.domain.activityGroup.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.domain.activityGroup.domain.ActivityGroupCategory; import page.clab.api.domain.activityGroup.domain.ActivityGroupStatus; import page.clab.api.domain.activityGroup.domain.GroupMember; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupProjectResponseDto { @@ -56,11 +49,23 @@ public class ActivityGroupProjectResponseDto { private LocalDateTime createdAt; public static ActivityGroupProjectResponseDto create(ActivityGroup activityGroup, List groupMembers, List boards, boolean isOwner) { - ActivityGroupProjectResponseDto activityGroupProjectResponseDto = ModelMapperUtil.getModelMapper().map(activityGroup, ActivityGroupProjectResponseDto.class); - activityGroupProjectResponseDto.setGroupMembers(groupMembers.stream().map(GroupMemberResponseDto::of).toList()); - activityGroupProjectResponseDto.setActivityGroupBoards(boards.stream().map(ActivityGroupBoardResponseDto::of).toList()); - activityGroupProjectResponseDto.setOwner(isOwner); - return activityGroupProjectResponseDto; + return ActivityGroupProjectResponseDto.builder() + .id(activityGroup.getId()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .name(activityGroup.getName()) + .content(activityGroup.getContent()) + .status(activityGroup.getStatus()) + .imageUrl(activityGroup.getImageUrl()) + .groupMembers(groupMembers.stream().map(GroupMemberResponseDto::toDto).toList()) + .startDate(activityGroup.getStartDate()) + .endDate(activityGroup.getEndDate()) + .techStack(activityGroup.getTechStack()) + .githubUrl(activityGroup.getGithubUrl()) + .activityGroupBoards(boards.stream().map(ActivityGroupBoardResponseDto::toDto).toList()) + .isOwner(isOwner) + .createdAt(activityGroup.getCreatedAt()) + .build(); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupReportResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupReportResponseDto.java index fc44c52b0..290a6a64d 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupReportResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupReportResponseDto.java @@ -1,17 +1,12 @@ package page.clab.api.domain.activityGroup.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroupReport; +import java.time.LocalDateTime; + @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupReportResponseDto { @@ -29,15 +24,15 @@ public class ActivityGroupReportResponseDto { private LocalDateTime updatedAt; - public static ActivityGroupReportResponseDto of(ActivityGroupReport activityGroupReport){ + public static ActivityGroupReportResponseDto toDto(ActivityGroupReport report) { return ActivityGroupReportResponseDto.builder() - .activityGroupId(activityGroupReport.getActivityGroup().getId()) - .activityGroupName(activityGroupReport.getActivityGroup().getName()) - .turn(activityGroupReport.getTurn()) - .title(activityGroupReport.getTitle()) - .content(activityGroupReport.getContent()) - .createdAt(activityGroupReport.getCreatedAt()) - .updatedAt(activityGroupReport.getUpdateTime()) + .activityGroupId(report.getActivityGroup().getId()) + .activityGroupName(report.getActivityGroup().getName()) + .turn(report.getTurn()) + .title(report.getTitle()) + .content(report.getContent()) + .createdAt(report.getCreatedAt()) + .updatedAt(report.getUpdatedAt()) .build(); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupResponseDto.java index a6ee5b74f..22344cb78 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.activityGroup.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupCategory; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupResponseDto { @@ -29,7 +23,15 @@ public class ActivityGroupResponseDto { private LocalDateTime createdAt; - public static ActivityGroupResponseDto of(ActivityGroup activityGroup) { - return ModelMapperUtil.getModelMapper().map(activityGroup, ActivityGroupResponseDto.class); + public static ActivityGroupResponseDto toDto(ActivityGroup activityGroup) { + return ActivityGroupResponseDto.builder() + .id(activityGroup.getId()) + .name(activityGroup.getName()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .imageUrl(activityGroup.getImageUrl()) + .createdAt(activityGroup.getCreatedAt()) + .build(); } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStatusResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStatusResponseDto.java index 591bd1455..615bafbe8 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStatusResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStatusResponseDto.java @@ -1,21 +1,14 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupCategory; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupStatusResponseDto { @@ -41,15 +34,20 @@ public class ActivityGroupStatusResponseDto { private LocalDateTime createdAt; - public static ActivityGroupStatusResponseDto create(ActivityGroup activityGroup, Member leader, Long participantCount, Long weeklyActivityCount) { - ActivityGroupStatusResponseDto activityGroupResponseDto = ModelMapperUtil.getModelMapper().map(activityGroup, ActivityGroupStatusResponseDto.class); - if (leader != null) { - activityGroupResponseDto.setLeaderId(leader.getId()); - activityGroupResponseDto.setLeaderName(leader.getName()); - } - activityGroupResponseDto.setParticipantCount(participantCount); - activityGroupResponseDto.setWeeklyActivityCount(weeklyActivityCount); - return activityGroupResponseDto; + public static ActivityGroupStatusResponseDto toDto(ActivityGroup activityGroup, Member leader, Long participantCount, Long weeklyActivityCount) { + return ActivityGroupStatusResponseDto.builder() + .id(activityGroup.getId()) + .name(activityGroup.getName()) + .content(activityGroup.getContent()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .imageUrl(activityGroup.getImageUrl()) + .leaderId(leader != null ? leader.getId() : null) + .leaderName(leader != null ? leader.getName() : null) + .participantCount(participantCount) + .weeklyActivityCount(weeklyActivityCount) + .createdAt(activityGroup.getCreatedAt()) + .build(); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStudyResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStudyResponseDto.java index d40b98851..07fe45e0b 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStudyResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ActivityGroupStudyResponseDto.java @@ -1,25 +1,18 @@ package page.clab.api.domain.activityGroup.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.domain.activityGroup.domain.ActivityGroupCategory; import page.clab.api.domain.activityGroup.domain.ActivityGroupStatus; import page.clab.api.domain.activityGroup.domain.GroupMember; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityGroupStudyResponseDto { @@ -49,12 +42,20 @@ public class ActivityGroupStudyResponseDto { private LocalDateTime createdAt; public static ActivityGroupStudyResponseDto create(ActivityGroup activityGroup, List groupMembers, List boards, boolean isOwner) { - - ActivityGroupStudyResponseDto activityGroupStudyResponseDto = ModelMapperUtil.getModelMapper().map(activityGroup, ActivityGroupStudyResponseDto.class); - activityGroupStudyResponseDto.setGroupMembers(groupMembers.stream().map(GroupMemberResponseDto::of).toList()); - activityGroupStudyResponseDto.setActivityGroupBoards(boards.stream().map(ActivityGroupBoardResponseDto::of).toList()); - activityGroupStudyResponseDto.setOwner(isOwner); - return activityGroupStudyResponseDto; + return ActivityGroupStudyResponseDto.builder() + .id(activityGroup.getId()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .name(activityGroup.getName()) + .content(activityGroup.getContent()) + .status(activityGroup.getStatus()) + .imageUrl(activityGroup.getImageUrl()) + .groupMembers(groupMembers.stream().map(GroupMemberResponseDto::toDto).toList()) + .curriculum(activityGroup.getCurriculum()) + .activityGroupBoards(boards.stream().map(ActivityGroupBoardResponseDto::toDto).toList()) + .isOwner(isOwner) + .createdAt(activityGroup.getCreatedAt()) + .build(); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ApplyFormResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/ApplyFormResponseDto.java deleted file mode 100644 index c124237cb..000000000 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/ApplyFormResponseDto.java +++ /dev/null @@ -1,44 +0,0 @@ -package page.clab.api.domain.activityGroup.dto.response; - -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import page.clab.api.domain.activityGroup.domain.ApplyForm; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class ApplyFormResponseDto { - - private String applierName; - - private String applierId; - - private String applierDepartment; - - private String applierYear; - - private String applierContact; - - private String applyReason; - - private LocalDateTime createdAt; - - public static ApplyFormResponseDto of(ApplyForm applyForm) { - return ApplyFormResponseDto.builder() - .applierName(applyForm.getMember().getName()) - .applierId(applyForm.getMember().getId()) - .applierDepartment(applyForm.getMember().getDepartment()) - .applierYear(applyForm.getMember().getGrade().toString()) - .applierContact(applyForm.getMember().getContact()) - .applyReason(applyForm.getApplyReason()) - .createdAt(applyForm.getCreatedAt()) - .build(); - } - -} diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java index 7f97a1916..eea2cbba0 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java @@ -1,21 +1,14 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class AssignmentSubmissionWithFeedbackResponseDto { @@ -25,22 +18,22 @@ public class AssignmentSubmissionWithFeedbackResponseDto { private String content; - private List files = new ArrayList<>(); + private List files; private LocalDateTime createdAt; - private LocalDateTime updateTime; + private LocalDateTime updatedAt; - private List feedbacks = new ArrayList<>(); + private List feedbacks; - public static AssignmentSubmissionWithFeedbackResponseDto of(ActivityGroupBoard activityGroupBoard, List feedbackDtos) { + public static AssignmentSubmissionWithFeedbackResponseDto toDto(ActivityGroupBoard board, List feedbackDtos) { return AssignmentSubmissionWithFeedbackResponseDto.builder() - .id(activityGroupBoard.getId()) - .parentId(activityGroupBoard.getParent() != null ? activityGroupBoard.getParent().getId() : null) - .content(activityGroupBoard.getContent()) - .files(activityGroupBoard.getUploadedFiles().stream().map(UploadedFileResponseDto::of).toList()) - .createdAt(activityGroupBoard.getCreatedAt()) - .updateTime(activityGroupBoard.getUpdateTime()) + .id(board.getId()) + .parentId(board.getParent() != null ? board.getParent().getId() : null) + .content(board.getContent()) + .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) + .createdAt(board.getCreatedAt()) + .updatedAt(board.getUpdatedAt()) .feedbacks(feedbackDtos) .build(); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/AttendanceResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/AttendanceResponseDto.java index c667d470b..4ed4f431a 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/AttendanceResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/AttendanceResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.activityGroup.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.Attendance; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class AttendanceResponseDto { @@ -22,13 +16,12 @@ public class AttendanceResponseDto { private LocalDateTime attendanceDateTime; - public static AttendanceResponseDto of(Attendance attendance){ - AttendanceResponseDto attendanceResponseDto = ModelMapperUtil.getModelMapper().map(attendance, AttendanceResponseDto.class); - attendanceResponseDto.setActivityGroupId(attendance.getActivityGroup().getId()); - attendanceResponseDto.setMemberId(attendance.getMember().getId()); - attendanceResponseDto.setAttendanceDateTime(attendance.getCreatedAt()); - - return attendanceResponseDto; + public static AttendanceResponseDto toDto(Attendance attendance) { + return AttendanceResponseDto.builder() + .activityGroupId(attendance.getActivityGroup().getId()) + .memberId(attendance.getMember().getId()) + .attendanceDateTime(attendance.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/FeedbackResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/FeedbackResponseDto.java index b2df956c3..e1e5ff309 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/FeedbackResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/FeedbackResponseDto.java @@ -1,21 +1,14 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.ActivityGroupBoard; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class FeedbackResponseDto { @@ -23,19 +16,19 @@ public class FeedbackResponseDto { private String content; - private List files = new ArrayList<>(); + private List files; private LocalDateTime createdAt; - private LocalDateTime updateTime; + private LocalDateTime updatedAt; - public static FeedbackResponseDto of(ActivityGroupBoard activityGroupBoard) { + public static FeedbackResponseDto toDto(ActivityGroupBoard board) { return FeedbackResponseDto.builder() - .id(activityGroupBoard.getId()) - .content(activityGroupBoard.getContent()) - .files(activityGroupBoard.getUploadedFiles().stream().map(UploadedFileResponseDto::of).toList()) - .createdAt(activityGroupBoard.getCreatedAt()) - .updateTime(activityGroupBoard.getUpdateTime()) + .id(board.getId()) + .content(board.getContent()) + .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) + .createdAt(board.getCreatedAt()) + .updatedAt(board.getUpdatedAt()) .build(); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/dto/response/GroupMemberResponseDto.java b/src/main/java/page/clab/api/domain/activityGroup/dto/response/GroupMemberResponseDto.java index 59d2b86ed..54fae3772 100644 --- a/src/main/java/page/clab/api/domain/activityGroup/dto/response/GroupMemberResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityGroup/dto/response/GroupMemberResponseDto.java @@ -1,20 +1,13 @@ package page.clab.api.domain.activityGroup.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityGroup.domain.GroupMember; import page.clab.api.domain.activityGroup.domain.GroupMemberStatus; -import page.clab.api.global.util.ModelMapperUtil; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class GroupMemberResponseDto { @@ -26,16 +19,18 @@ public class GroupMemberResponseDto { private GroupMemberStatus status; - public static GroupMemberResponseDto of(GroupMember groupMember) { - GroupMemberResponseDto groupMemberResponseDto = ModelMapperUtil.getModelMapper().map(groupMember, GroupMemberResponseDto.class); - groupMemberResponseDto.setMemberId(groupMember.getMember().getId()); - groupMemberResponseDto.setMemberName(groupMember.getMember().getName()); - return groupMemberResponseDto; + public static GroupMemberResponseDto toDto(GroupMember groupMember) { + return GroupMemberResponseDto.builder() + .memberId(groupMember.getMember().getId()) + .memberName(groupMember.getMember().getName()) + .role(groupMember.getRole().getKey()) + .status(groupMember.getStatus()) + .build(); } - public static List of(List groupMembers) { + public static List toDto(List groupMembers) { return groupMembers.stream() - .map(GroupMemberResponseDto::of) + .map(GroupMemberResponseDto::toDto) .toList(); } diff --git a/src/main/java/page/clab/api/domain/activityGroup/exception/NotSubmitCategoryBoardException.java b/src/main/java/page/clab/api/domain/activityGroup/exception/NotSubmitCategoryBoardException.java deleted file mode 100644 index 224af7bc8..000000000 --- a/src/main/java/page/clab/api/domain/activityGroup/exception/NotSubmitCategoryBoardException.java +++ /dev/null @@ -1,9 +0,0 @@ -package page.clab.api.domain.activityGroup.exception; - -public class NotSubmitCategoryBoardException extends RuntimeException { - - public NotSubmitCategoryBoardException(String message) { - super(message); - } - -} diff --git a/src/main/java/page/clab/api/domain/activityPhoto/api/ActivityPhotoController.java b/src/main/java/page/clab/api/domain/activityPhoto/api/ActivityPhotoController.java index 0dda297a5..98bc5e3d6 100644 --- a/src/main/java/page/clab/api/domain/activityPhoto/api/ActivityPhotoController.java +++ b/src/main/java/page/clab/api/domain/activityPhoto/api/ActivityPhotoController.java @@ -21,10 +21,10 @@ import page.clab.api.domain.activityPhoto.dto.request.ActivityPhotoRequestDto; import page.clab.api.domain.activityPhoto.dto.response.ActivityPhotoResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; @RestController -@RequestMapping("/activity-photos") +@RequestMapping("/api/v1/activity-photos") @RequiredArgsConstructor @Tag(name = "ActivityPhoto", description = "활동 사진") @Slf4j @@ -35,52 +35,44 @@ public class ActivityPhotoController { @Operation(summary = "[A] 활동 사진 등록", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createActivityPhoto( - @Valid @RequestBody ActivityPhotoRequestDto activityPhotoRequestDto + public ApiResponse createActivityPhoto( + @Valid @RequestBody ActivityPhotoRequestDto requestDto ) { - Long id = activityPhotoService.createActivityPhoto(activityPhotoRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = activityPhotoService.createActivityPhoto(requestDto); + return ApiResponse.success(id); } @Operation(summary = "활동 사진 목록 조회", description = "ROLE_ANONYMOUS 이상의 권한이 필요함
" + "공개 여부를 입력하지 않으면 전체 조회됨") @GetMapping("") - public ResponseModel getActivityPhotosByConditions( + public ApiResponse> getActivityPhotosByConditions( @RequestParam(name = "isPublic", required = false) Boolean isPublic, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto activityPhotos = activityPhotoService.getActivityPhotosByConditions(isPublic, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(activityPhotos); - return responseModel; + return ApiResponse.success(activityPhotos); } @Operation(summary = "활동 사진 고정/해제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{activityPhotoId}") - public ResponseModel togglePublicStatus( + public ApiResponse togglePublicStatus( @PathVariable(name = "activityPhotoId") Long activityPhotoId ) { Long id = activityPhotoService.togglePublicStatus(activityPhotoId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[A] 활동 사진 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{activityPhotoId}") - public ResponseModel deleteActivityPhoto( + public ApiResponse deleteActivityPhoto( @PathVariable(name = "activityPhotoId") Long activityPhotoId ) { Long id = activityPhotoService.deleteActivityPhoto(activityPhotoId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } } diff --git a/src/main/java/page/clab/api/domain/activityPhoto/application/ActivityPhotoService.java b/src/main/java/page/clab/api/domain/activityPhoto/application/ActivityPhotoService.java index b116ec268..dcccec830 100644 --- a/src/main/java/page/clab/api/domain/activityPhoto/application/ActivityPhotoService.java +++ b/src/main/java/page/clab/api/domain/activityPhoto/application/ActivityPhotoService.java @@ -1,22 +1,20 @@ package page.clab.api.domain.activityPhoto.application; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityPhoto.dao.ActivityPhotoRepository; import page.clab.api.domain.activityPhoto.domain.ActivityPhoto; import page.clab.api.domain.activityPhoto.dto.request.ActivityPhotoRequestDto; import page.clab.api.domain.activityPhoto.dto.response.ActivityPhotoResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.file.application.FileService; +import page.clab.api.global.common.file.application.UploadedFileService; import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.exception.NotFoundException; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -24,19 +22,22 @@ public class ActivityPhotoService { private final ActivityPhotoRepository activityPhotoRepository; - private final FileService fileService; + private final UploadedFileService uploadedFileService; - public Long createActivityPhoto(ActivityPhotoRequestDto dto) { - List uploadedFiles = prepareUploadedFiles(dto.getFileUrlList()); - ActivityPhoto activityPhoto = ActivityPhoto.create(dto, uploadedFiles); + @Transactional + public Long createActivityPhoto(ActivityPhotoRequestDto requestDto) { + List uploadedFiles = uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrlList()); + ActivityPhoto activityPhoto = ActivityPhotoRequestDto.toEntity(requestDto, uploadedFiles); return activityPhotoRepository.save(activityPhoto).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getActivityPhotosByConditions(Boolean isPublic, Pageable pageable) { Page activityPhotos = activityPhotoRepository.findByConditions(isPublic, pageable); - return new PagedResponseDto<>(activityPhotos.map(ActivityPhotoResponseDto::of)); + return new PagedResponseDto<>(activityPhotos.map(ActivityPhotoResponseDto::toDto)); } + @Transactional public Long togglePublicStatus(Long activityPhotoId) { ActivityPhoto activityPhoto = getActivityPhotoByIdOrThrow(activityPhotoId); activityPhoto.togglePublicStatus(); @@ -54,12 +55,4 @@ public ActivityPhoto getActivityPhotoByIdOrThrow(Long activityPhotoId) { .orElseThrow(() -> new NotFoundException("존재하지 않는 활동 사진입니다.")); } - @NotNull - private List prepareUploadedFiles(List fileUrls) { - if (fileUrls == null) return new ArrayList<>(); - return fileUrls.stream() - .map(fileService::getUploadedFileByUrl) - .collect(Collectors.toList()); - } - } diff --git a/src/main/java/page/clab/api/domain/activityPhoto/domain/ActivityPhoto.java b/src/main/java/page/clab/api/domain/activityPhoto/domain/ActivityPhoto.java index ff5c8ecef..218c948f2 100644 --- a/src/main/java/page/clab/api/domain/activityPhoto/domain/ActivityPhoto.java +++ b/src/main/java/page/clab/api/domain/activityPhoto/domain/ActivityPhoto.java @@ -8,28 +8,25 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.activityPhoto.dto.request.ActivityPhotoRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.common.file.domain.UploadedFile; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class ActivityPhoto { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ActivityPhoto extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -41,7 +38,7 @@ public class ActivityPhoto { @Column(nullable = false) @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "activity_photo_files") - private List uploadedFiles = new ArrayList<>(); + private List uploadedFiles; @Column(nullable = false) private LocalDate date; @@ -49,16 +46,6 @@ public class ActivityPhoto { @Column(nullable = false) private Boolean isPublic; - @CreationTimestamp - private LocalDateTime createdAt; - - public static ActivityPhoto create(ActivityPhotoRequestDto activityPhotoRequestDto, List uploadedFiles) { - ActivityPhoto activityPhoto = ModelMapperUtil.getModelMapper().map(activityPhotoRequestDto, ActivityPhoto.class); - activityPhoto.uploadedFiles = uploadedFiles; - activityPhoto.isPublic = false; - return activityPhoto; - } - public void togglePublicStatus() { this.isPublic = !this.isPublic; } diff --git a/src/main/java/page/clab/api/domain/activityPhoto/dto/request/ActivityPhotoRequestDto.java b/src/main/java/page/clab/api/domain/activityPhoto/dto/request/ActivityPhotoRequestDto.java index 1a4336955..9c65b049a 100644 --- a/src/main/java/page/clab/api/domain/activityPhoto/dto/request/ActivityPhotoRequestDto.java +++ b/src/main/java/page/clab/api/domain/activityPhoto/dto/request/ActivityPhotoRequestDto.java @@ -2,20 +2,16 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityPhoto.domain.ActivityPhoto; +import page.clab.api.global.common.file.domain.UploadedFile; import java.time.LocalDate; import java.util.List; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ActivityPhotoRequestDto { @NotNull(message = "{notNull.activityPhoto.title}") @@ -30,4 +26,13 @@ public class ActivityPhotoRequestDto { @Schema(description = "활동 날짜", example = "2021-01-01", required = true) private LocalDate date; + public static ActivityPhoto toEntity(ActivityPhotoRequestDto requestDto, List uploadedFiles) { + return ActivityPhoto.builder() + .title(requestDto.getTitle()) + .uploadedFiles(uploadedFiles) + .date(requestDto.getDate()) + .isPublic(false) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/activityPhoto/dto/response/ActivityPhotoResponseDto.java b/src/main/java/page/clab/api/domain/activityPhoto/dto/response/ActivityPhotoResponseDto.java index ff37e4d25..77ff8f00c 100644 --- a/src/main/java/page/clab/api/domain/activityPhoto/dto/response/ActivityPhotoResponseDto.java +++ b/src/main/java/page/clab/api/domain/activityPhoto/dto/response/ActivityPhotoResponseDto.java @@ -1,21 +1,14 @@ package page.clab.api.domain.activityPhoto.dto.response; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.activityPhoto.domain.ActivityPhoto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; +import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ActivityPhotoResponseDto { @@ -23,14 +16,20 @@ public class ActivityPhotoResponseDto { private String title; - private List files = new ArrayList<>(); + private List files; private LocalDate date; private Boolean isPublic; - public static ActivityPhotoResponseDto of(ActivityPhoto activityPhoto) { - return ModelMapperUtil.getModelMapper().map(activityPhoto, ActivityPhotoResponseDto.class); + public static ActivityPhotoResponseDto toDto(ActivityPhoto activityPhoto) { + return ActivityPhotoResponseDto.builder() + .id(activityPhoto.getId()) + .title(activityPhoto.getTitle()) + .files(UploadedFileResponseDto.toDto(activityPhoto.getUploadedFiles())) + .date(activityPhoto.getDate()) + .isPublic(activityPhoto.getIsPublic()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/application/api/ApplicationController.java b/src/main/java/page/clab/api/domain/application/api/ApplicationController.java index 0c199974f..c5ff1a9de 100644 --- a/src/main/java/page/clab/api/domain/application/api/ApplicationController.java +++ b/src/main/java/page/clab/api/domain/application/api/ApplicationController.java @@ -22,11 +22,12 @@ import page.clab.api.domain.application.dto.request.ApplicationRequestDto; import page.clab.api.domain.application.dto.response.ApplicationPassResponseDto; import page.clab.api.domain.application.dto.response.ApplicationResponseDto; +import page.clab.api.domain.board.dto.response.BoardListResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; @RestController -@RequestMapping("/applications") +@RequestMapping("/api/v1/applications") @RequiredArgsConstructor @Tag(name = "Application", description = "동아리 지원") @Slf4j @@ -36,14 +37,12 @@ public class ApplicationController { @Operation(summary = "동아리 지원", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @PostMapping("") - public ResponseModel createApplication( + public ApiResponse createApplication( HttpServletRequest request, - @Valid @RequestBody ApplicationRequestDto applicationRequestDto + @Valid @RequestBody ApplicationRequestDto requestDto ) { - String id = applicationService.createApplication(request, applicationRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + String id = applicationService.createApplication(request, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 지원자 목록 조회(모집 일정 ID, 지원자 ID, 합격 여부 기준)", description = "ROLE_ADMIN 이상의 권한이 필요함
" + @@ -51,7 +50,7 @@ public ResponseModel createApplication( "모집 일정 ID, 지원자 ID, 합격 여부 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/conditions") - public ResponseModel getApplicationsByConditions( + public ApiResponse> getApplicationsByConditions( @RequestParam(name = "recruitmentId", required = false) Long recruitmentId, @RequestParam(name = "studentId", required = false) String studentId, @RequestParam(name = "isPass", required = false) Boolean isPass, @@ -60,48 +59,52 @@ public ResponseModel getApplicationsByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto applications = applicationService.getApplicationsByConditions(recruitmentId, studentId, isPass, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(applications); - return responseModel; + return ApiResponse.success(applications); } @Operation(summary = "[S] 지원 합격/취소", description = "ROLE_SUPER 이상의 권한이 필요함
" + "승인/취소 상태가 반전됨") @Secured({"ROLE_SUPER"}) @PatchMapping("/{recruitmentId}/{studentId}") - public ResponseModel toggleApprovalStatus( + public ApiResponse toggleApprovalStatus( @PathVariable(name = "recruitmentId") Long recruitmentId, @PathVariable(name = "studentId") String studentId ) { String id = applicationService.toggleApprovalStatus(recruitmentId, studentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "합격 여부 조회", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @GetMapping("/{recruitmentId}/{studentId}") - public ResponseModel getApplicationPass( + public ApiResponse getApplicationPass( @PathVariable(name = "recruitmentId") Long recruitmentId, @PathVariable(name = "studentId") String studentId ) { - ApplicationPassResponseDto applicationPassResponseDto = applicationService.getApplicationPass(recruitmentId, studentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(applicationPassResponseDto); - return responseModel; + ApplicationPassResponseDto pass = applicationService.getApplicationPass(recruitmentId, studentId); + return ApiResponse.success(pass); } @Operation(summary = "[S] 지원서 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/{recruitmentId}/{studentId}") - public ResponseModel deleteApplication( + public ApiResponse deleteApplication( @PathVariable(name = "recruitmentId") Long recruitmentId, @PathVariable(name = "studentId") String studentId ) { String id = applicationService.deleteApplication(recruitmentId, studentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 지원서 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedBoards( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto applications = applicationService.getDeletedApplications(pageable); + return ApiResponse.success(applications); } } diff --git a/src/main/java/page/clab/api/domain/application/application/ApplicationService.java b/src/main/java/page/clab/api/domain/application/application/ApplicationService.java index 9c694b073..c3b5e989c 100644 --- a/src/main/java/page/clab/api/domain/application/application/ApplicationService.java +++ b/src/main/java/page/clab/api/domain/application/application/ApplicationService.java @@ -1,12 +1,12 @@ package page.clab.api.domain.application.application; import jakarta.servlet.http.HttpServletRequest; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.application.dao.ApplicationRepository; import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.application.domain.ApplicationId; @@ -19,6 +19,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.common.slack.application.SlackService; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -29,37 +30,43 @@ public class ApplicationService { private final NotificationService notificationService; + private final ValidationService validationService; + private final SlackService slackService; private final ApplicationRepository applicationRepository; @Transactional - public String createApplication(HttpServletRequest request, ApplicationRequestDto applicationRequestDto) { - Recruitment recruitment = recruitmentService.getRecruitmentByIdOrThrow(applicationRequestDto.getRecruitmentId()); - Application application = Application.create(applicationRequestDto); - notificationService.sendNotificationToAdmins( - applicationRequestDto.getStudentId() + " " + - applicationRequestDto.getName() + "님이 동아리에 지원하였습니다." - ); - slackService.sendApplicationNotification(request, applicationRequestDto); + public String createApplication(HttpServletRequest request, ApplicationRequestDto requestDto) { + Recruitment recruitment = recruitmentService.getRecruitmentByIdOrThrow(requestDto.getRecruitmentId()); + Application application = ApplicationRequestDto.toEntity(requestDto); + validationService.checkValid(application); + + notificationService.sendNotificationToAdmins(requestDto.getStudentId() + " " + + requestDto.getName() + "님이 동아리에 지원하였습니다."); + slackService.sendApplicationNotification(request, requestDto); return applicationRepository.save(application).getStudentId(); } + @Transactional(readOnly = true) public PagedResponseDto getApplicationsByConditions(Long recruitmentId, String studentId, Boolean isPass, Pageable pageable) { Page applications = applicationRepository.findByConditions(recruitmentId, studentId, isPass, pageable); - return new PagedResponseDto<>(applications.map(ApplicationResponseDto::of)); + return new PagedResponseDto<>(applications.map(ApplicationResponseDto::toDto)); } + @Transactional public String toggleApprovalStatus(Long recruitmentId, String studentId) { Application application = getApplicationByIdOrThrow(studentId, recruitmentId); application.toggleApprovalStatus(); + validationService.checkValid(application); return applicationRepository.save(application).getStudentId(); } + @Transactional(readOnly = true) public ApplicationPassResponseDto getApplicationPass(Long recruitmentId, String studentId) { - ApplicationId id = new ApplicationId(studentId, recruitmentId); + ApplicationId id = ApplicationId.create(studentId, recruitmentId); return applicationRepository.findById(id) - .map(ApplicationPassResponseDto::of) + .map(ApplicationPassResponseDto::toDto) .orElseGet(() -> ApplicationPassResponseDto.builder() .isPass(false) .build()); @@ -71,8 +78,13 @@ public String deleteApplication(Long recruitmentId, String studentId) { return application.getStudentId(); } + public PagedResponseDto getDeletedApplications(Pageable pageable) { + Page applications = applicationRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(applications.map(ApplicationResponseDto::toDto)); + } + private Application getApplicationByIdOrThrow(String studentId, Long recruitmentId) { - ApplicationId id = new ApplicationId(studentId, recruitmentId); + ApplicationId id = ApplicationId.create(studentId, recruitmentId); return applicationRepository.findById(id) .orElseThrow(() -> new NotFoundException("해당 지원자가 없습니다.")); } diff --git a/src/main/java/page/clab/api/domain/application/dao/ApplicationRepository.java b/src/main/java/page/clab/api/domain/application/dao/ApplicationRepository.java index e3e31e29b..464260c32 100644 --- a/src/main/java/page/clab/api/domain/application/dao/ApplicationRepository.java +++ b/src/main/java/page/clab/api/domain/application/dao/ApplicationRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.application.domain.ApplicationId; @@ -18,4 +19,7 @@ public interface ApplicationRepository extends JpaRepository findByRecruitmentIdAndStudentId(Long recruitmentId, String studentId); + @Query(value = "SELECT a.* FROM application a WHERE a.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/application/domain/Application.java b/src/main/java/page/clab/api/domain/application/domain/Application.java index 7068ed661..5726a6bb9 100644 --- a/src/main/java/page/clab/api/domain/application/domain/Application.java +++ b/src/main/java/page/clab/api/domain/application/domain/Application.java @@ -9,30 +9,34 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.application.dto.request.ApplicationRequestDto; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.member.domain.Role; +import page.clab.api.domain.member.domain.StudentStatus; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDate; -import java.time.LocalDateTime; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @IdClass(ApplicationId.class) -public class Application { +@SQLDelete(sql = "UPDATE application SET is_deleted = true WHERE recruitment_id = ? AND student_id = ?") +@SQLRestriction("is_deleted = false") +public class Application extends BaseEntity { @Id @Size(min = 9, max = 9, message = "{size.application.studentId}") @@ -85,31 +89,28 @@ public class Application { @Enumerated(EnumType.STRING) private ApplicationType applicationType; - @NotNull + @Column(nullable = false) private Boolean isPass; - @CreationTimestamp - private LocalDateTime updateTime; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static Application create(ApplicationRequestDto applicationRequestDto) { - Application application = ModelMapperUtil.getModelMapper().map(applicationRequestDto, Application.class); - application.setContact(removeHyphensFromContact(application.getContact())); - application.setIsPass(false); - application.setUpdateTime(LocalDateTime.now()); - return application; - } - - public static String removeHyphensFromContact(String contact) { - return contact.replaceAll("-", ""); + public static Member toMember(Application application) { + return Member.builder() + .id(application.getStudentId()) + .name(application.getName()) + .contact(application.getContact()) + .email(application.getEmail()) + .department(application.getDepartment()) + .grade(application.getGrade()) + .birth(application.getBirth()) + .address(application.getAddress()) + .interests(application.getInterests()) + .githubUrl(application.getGithubUrl()) + .studentStatus(StudentStatus.CURRENT) + .role(Role.USER) + .build(); } public void toggleApprovalStatus() { this.isPass = !this.isPass; - this.setUpdateTime(LocalDateTime.now()); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/application/domain/ApplicationId.java b/src/main/java/page/clab/api/domain/application/domain/ApplicationId.java index 3a8cd5e70..300c95000 100644 --- a/src/main/java/page/clab/api/domain/application/domain/ApplicationId.java +++ b/src/main/java/page/clab/api/domain/application/domain/ApplicationId.java @@ -1,18 +1,27 @@ package page.clab.api.domain.application.domain; +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.io.Serializable; -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Embeddable public class ApplicationId implements Serializable { + @EqualsAndHashCode.Include private String studentId; + @EqualsAndHashCode.Include private Long recruitmentId; + public static ApplicationId create(String studentId, Long recruitmentId) { + return new ApplicationId(studentId, recruitmentId); + } + } diff --git a/src/main/java/page/clab/api/domain/application/domain/ApplicationType.java b/src/main/java/page/clab/api/domain/application/domain/ApplicationType.java index 59c38475e..6e5cde7ef 100644 --- a/src/main/java/page/clab/api/domain/application/domain/ApplicationType.java +++ b/src/main/java/page/clab/api/domain/application/domain/ApplicationType.java @@ -8,7 +8,7 @@ public enum ApplicationType { NORMAL("NORMAL", "일반 회원"), - OFFICER("OFFICER", "운영진"), + OPERATION("OPERATION", "운영진"), CORE_TEAM("CORE_TEAM", "코어팀"); private String key; diff --git a/src/main/java/page/clab/api/domain/application/dto/request/ApplicationRequestDto.java b/src/main/java/page/clab/api/domain/application/dto/request/ApplicationRequestDto.java index d82cdbfb4..fc05c1505 100644 --- a/src/main/java/page/clab/api/domain/application/dto/request/ApplicationRequestDto.java +++ b/src/main/java/page/clab/api/domain/application/dto/request/ApplicationRequestDto.java @@ -1,34 +1,20 @@ package page.clab.api.domain.application.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.application.domain.ApplicationType; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.Contact; import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ApplicationRequestDto { @NotNull(message = "{notNull.application.studentId}") - @Size(min = 9, max = 9, message = "{size.application.studentId}") - @Pattern(regexp = "^[0-9]+$", message = "{pattern.application.studentId}") @Schema(description = "학번", example = "202312000", required = true) private String studentId; @@ -37,29 +23,22 @@ public class ApplicationRequestDto { private Long recruitmentId; @NotNull(message = "{notNull.application.name}") - @Size(min = 1, max = 10, message = "{size.application.name}") @Schema(description = "이름", example = "홍길동", required = true) private String name; @NotNull(message = "{notNull.application.contact}") - @Size(min = 9, max = 13, message = "{size.application.contact}") @Schema(description = "연락처", example = "01012345678", required = true) private String contact; @NotNull(message = "{notNull.application.email}") - @Email(message = "{email.application.email}") - @Size(min = 1, message = "{size.application.email}") @Schema(description = "이메일", example = "clab.coreteam@gamil.com", required = true) private String email; @NotNull(message = "{notNull.application.department}") - @Size(min = 1, message = "{size.application.department}") @Schema(description = "학과", example = "AI컴퓨터공학부", required = true) private String department; @NotNull(message = "{notNull.application.grade}") - @Min(value = 1, message = "{min.application.grade}") - @Max(value = 4, message = "{max.application.grade}") @Schema(description = "학년", example = "1", required = true) private Long grade; @@ -68,7 +47,6 @@ public class ApplicationRequestDto { private LocalDate birth; @NotNull(message = "{notNull.application.address}") - @Size(min = 1, message = "{size.application.address}") @Schema(description = "주소", example = "경기도 수원시 영통구 광교산로 154-42", required = true) private String address; @@ -77,11 +55,9 @@ public class ApplicationRequestDto { private String interests; @NotNull(message = "{notNull.application.otherActivities}") - @Size(max = 1000, message = "{size.application.otherActivities}") @Schema(description = "IT 관련 자격증 및 활동", example = "경기대학교 컴퓨터공학과 학생회", required = true) private String otherActivities; - @URL(message = "{url.application.githubUrl}") @Schema(description = "GitHub", example = "https://github.com/KGU-C-Lab") private String githubUrl; @@ -89,8 +65,23 @@ public class ApplicationRequestDto { @Schema(description = "구분", example = "NORMAL", required = true) private ApplicationType applicationType; - public static ApplicationRequestDto of(Application application) { - return ModelMapperUtil.getModelMapper().map(application, ApplicationRequestDto.class); + public static Application toEntity(ApplicationRequestDto requestDto) { + return Application.builder() + .studentId(requestDto.getStudentId()) + .recruitmentId(requestDto.getRecruitmentId()) + .name(requestDto.getName()) + .contact(Contact.of(requestDto.getContact()).getValue()) + .email(requestDto.getEmail()) + .department(requestDto.getDepartment()) + .grade(requestDto.getGrade()) + .birth(requestDto.getBirth()) + .address(requestDto.getAddress()) + .interests(requestDto.getInterests()) + .otherActivities(requestDto.getOtherActivities()) + .githubUrl(requestDto.getGithubUrl()) + .applicationType(requestDto.getApplicationType()) + .isPass(false) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/application/dto/response/ApplicationPassResponseDto.java b/src/main/java/page/clab/api/domain/application/dto/response/ApplicationPassResponseDto.java index 19c2a0d24..9094dc064 100644 --- a/src/main/java/page/clab/api/domain/application/dto/response/ApplicationPassResponseDto.java +++ b/src/main/java/page/clab/api/domain/application/dto/response/ApplicationPassResponseDto.java @@ -1,18 +1,11 @@ package page.clab.api.domain.application.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.application.domain.ApplicationType; -import page.clab.api.global.util.ModelMapperUtil; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ApplicationPassResponseDto { @@ -24,8 +17,13 @@ public class ApplicationPassResponseDto { private Boolean isPass; - public static ApplicationPassResponseDto of(Application application) { - return ModelMapperUtil.getModelMapper().map(application, ApplicationPassResponseDto.class); + public static ApplicationPassResponseDto toDto(Application application) { + return ApplicationPassResponseDto.builder() + .recruitmentId(application.getRecruitmentId()) + .name(application.getName()) + .applicationType(application.getApplicationType()) + .isPass(application.getIsPass()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/application/dto/response/ApplicationResponseDto.java b/src/main/java/page/clab/api/domain/application/dto/response/ApplicationResponseDto.java index bced7bddd..57f72a8f3 100644 --- a/src/main/java/page/clab/api/domain/application/dto/response/ApplicationResponseDto.java +++ b/src/main/java/page/clab/api/domain/application/dto/response/ApplicationResponseDto.java @@ -1,21 +1,14 @@ package page.clab.api.domain.application.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.application.domain.ApplicationType; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ApplicationResponseDto { @@ -47,12 +40,29 @@ public class ApplicationResponseDto { private Boolean isPass; - private LocalDateTime updateTime; + private LocalDateTime updatedAt; private LocalDateTime createdAt; - public static ApplicationResponseDto of(Application application) { - return ModelMapperUtil.getModelMapper().map(application, ApplicationResponseDto.class); + public static ApplicationResponseDto toDto(Application application) { + return ApplicationResponseDto.builder() + .studentId(application.getStudentId()) + .recruitmentId(application.getRecruitmentId()) + .name(application.getName()) + .contact(application.getContact()) + .email(application.getEmail()) + .department(application.getDepartment()) + .grade(application.getGrade()) + .birth(application.getBirth()) + .address(application.getAddress()) + .interests(application.getInterests()) + .otherActivities(application.getOtherActivities()) + .githubUrl(application.getGithubUrl()) + .applicationType(application.getApplicationType()) + .isPass(application.getIsPass()) + .updatedAt(application.getUpdatedAt()) + .createdAt(application.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/award/api/AwardController.java b/src/main/java/page/clab/api/domain/award/api/AwardController.java index 7ef487cc3..0f8e954cd 100644 --- a/src/main/java/page/clab/api/domain/award/api/AwardController.java +++ b/src/main/java/page/clab/api/domain/award/api/AwardController.java @@ -21,12 +21,12 @@ import page.clab.api.domain.award.dto.request.AwardRequestDto; import page.clab.api.domain.award.dto.request.AwardUpdateRequestDto; import page.clab.api.domain.award.dto.response.AwardResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/awards") +@RequestMapping("/api/v1/awards") @RequiredArgsConstructor @Tag(name = "Award", description = "수상 이력") @Slf4j @@ -37,13 +37,11 @@ public class AwardController { @Operation(summary = "[U] 수상 이력 등록", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createAward( - @Valid @RequestBody AwardRequestDto awardRequestDto + public ApiResponse createAward( + @Valid @RequestBody AwardRequestDto requestDto ) { - Long id = awardService.createAward(awardRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = awardService.createAward(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 수상 이력 조회(학번, 연도 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -51,58 +49,62 @@ public ResponseModel createAward( "학번, 연도 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getAwardsByConditions( + public ApiResponse> getAwardsByConditions( @RequestParam(name = "memberId", required = false) String memberId, @RequestParam(name = "year", required = false) Long year, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto awardResponseDtos = awardService.getAwardsByConditions(memberId, year, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(awardResponseDtos); - return responseModel; + PagedResponseDto awards = awardService.getAwardsByConditions(memberId, year, pageable); + return ApiResponse.success(awards); } @Operation(summary = "[U] 나의 수상 이력 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my") - public ResponseModel getMyAwards( + public ApiResponse> getMyAwards( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto awardResponseDtos = awardService.getMyAwards(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(awardResponseDtos); - return responseModel; + PagedResponseDto myAwards = awardService.getMyAwards(pageable); + return ApiResponse.success(myAwards); } @Operation(summary = "[U] 수상 이력 수정", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{awardId}") - public ResponseModel updateAward( + public ApiResponse updateAward( @PathVariable(name = "awardId") Long awardId, - @Valid @RequestBody AwardUpdateRequestDto awardUpdateRequestDto + @Valid @RequestBody AwardUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = awardService.updateAward(awardId, awardUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = awardService.updateAward(awardId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 수상 이력 삭제", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{awardId}") - public ResponseModel deleteAward( + public ApiResponse deleteAward( @PathVariable(name = "awardId") Long awardId ) throws PermissionDeniedException { Long id = awardService.deleteAward(awardId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 수상이력 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedAwards( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto awards = awardService.getDeletedAwards(pageable); + return ApiResponse.success(awards); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/award/application/AwardService.java b/src/main/java/page/clab/api/domain/award/application/AwardService.java index 49fce7002..3ade96d03 100644 --- a/src/main/java/page/clab/api/domain/award/application/AwardService.java +++ b/src/main/java/page/clab/api/domain/award/application/AwardService.java @@ -1,10 +1,10 @@ package page.clab.api.domain.award.application; -import jakarta.transaction.Transactional; 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 page.clab.api.domain.award.dao.AwardRepository; import page.clab.api.domain.award.domain.Award; import page.clab.api.domain.award.dto.request.AwardRequestDto; @@ -15,6 +15,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -22,42 +23,55 @@ public class AwardService { private final MemberService memberService; + private final ValidationService validationService; + private final AwardRepository awardRepository; - public Long createAward(AwardRequestDto awardRequestDto) { - Member member = memberService.getCurrentMember(); - Award award = Award.create(awardRequestDto, member); + @Transactional + public Long createAward(AwardRequestDto requestDto) { + Member currentMember = memberService.getCurrentMember(); + Award award = AwardRequestDto.toEntity(requestDto, currentMember); + validationService.checkValid(award); return awardRepository.save(award).getId(); } - @Transactional + @Transactional(readOnly = true) public PagedResponseDto getAwardsByConditions(String memberId, Long year, Pageable pageable) { Page awards = awardRepository.findByConditions(memberId, year, pageable); - return new PagedResponseDto<>(awards.map(AwardResponseDto::of)); + return new PagedResponseDto<>(awards.map(AwardResponseDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getMyAwards(Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page awards = getAwardByMember(pageable, member); - return new PagedResponseDto<>(awards.map(AwardResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + Page awards = getAwardByMember(pageable, currentMember); + return new PagedResponseDto<>(awards.map(AwardResponseDto::toDto)); } - public Long updateAward(Long awardId, AwardUpdateRequestDto awardUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + @Transactional + public Long updateAward(Long awardId, AwardUpdateRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); Award award = getAwardByIdOrThrow(awardId); - award.validateAccessPermission(member); - award.update(awardUpdateRequestDto); + award.validateAccessPermission(currentMember); + award.update(requestDto); + validationService.checkValid(award); return awardRepository.save(award).getId(); } public Long deleteAward(Long awardId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Award award = getAwardByIdOrThrow(awardId); - award.validateAccessPermission(member); + award.validateAccessPermission(currentMember); awardRepository.delete(award); return award.getId(); } + @Transactional(readOnly = true) + public PagedResponseDto getDeletedAwards(Pageable pageable) { + Page awards = awardRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(awards.map(AwardResponseDto::toDto)); + } + private Award getAwardByIdOrThrow(Long awardId) { return awardRepository.findById(awardId) .orElseThrow(() -> new NotFoundException("해당 수상 이력이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/award/dao/AwardRepository.java b/src/main/java/page/clab/api/domain/award/dao/AwardRepository.java index 5a82420ee..bdcc318b3 100644 --- a/src/main/java/page/clab/api/domain/award/dao/AwardRepository.java +++ b/src/main/java/page/clab/api/domain/award/dao/AwardRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.award.domain.Award; @@ -10,6 +11,10 @@ @Repository public interface AwardRepository extends JpaRepository, AwardRepositoryCustom, QuerydslPredicateExecutor { + Page findAllByMemberOrderByAwardDateDesc(Member member, Pageable pageable); + @Query(value = "SELECT a.* FROM award a WHERE a.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/award/domain/Award.java b/src/main/java/page/clab/api/domain/award/domain/Award.java index aaf46ea6b..d9f7646e5 100644 --- a/src/main/java/page/clab/api/domain/award/domain/Award.java +++ b/src/main/java/page/clab/api/domain/award/domain/Award.java @@ -8,16 +8,18 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import page.clab.api.domain.award.dto.request.AwardRequestDto; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.award.dto.request.AwardUpdateRequestDto; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; import java.util.Optional; @@ -26,9 +28,11 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Award { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE award SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Award extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -53,17 +57,11 @@ public class Award { @JoinColumn(name = "member_id") private Member member; - public static Award create(AwardRequestDto awardRequestDto, Member member) { - Award award = ModelMapperUtil.getModelMapper().map(awardRequestDto, Award.class); - award.setMember(member); - return award; - } - - public void update(AwardUpdateRequestDto awardUpdateRequestDto) { - Optional.ofNullable(awardUpdateRequestDto.getCompetitionName()).ifPresent(this::setCompetitionName); - Optional.ofNullable(awardUpdateRequestDto.getOrganizer()).ifPresent(this::setOrganizer); - Optional.ofNullable(awardUpdateRequestDto.getAwardName()).ifPresent(this::setAwardName); - Optional.ofNullable(awardUpdateRequestDto.getAwardDate()).ifPresent(this::setAwardDate); + public void update(AwardUpdateRequestDto requestDto) { + Optional.ofNullable(requestDto.getCompetitionName()).ifPresent(this::setCompetitionName); + Optional.ofNullable(requestDto.getOrganizer()).ifPresent(this::setOrganizer); + Optional.ofNullable(requestDto.getAwardName()).ifPresent(this::setAwardName); + Optional.ofNullable(requestDto.getAwardDate()).ifPresent(this::setAwardDate); } public boolean isOwner(Member member) { diff --git a/src/main/java/page/clab/api/domain/award/dto/request/AwardRequestDto.java b/src/main/java/page/clab/api/domain/award/dto/request/AwardRequestDto.java index 33bbb3fb2..80ce4fe91 100644 --- a/src/main/java/page/clab/api/domain/award/dto/request/AwardRequestDto.java +++ b/src/main/java/page/clab/api/domain/award/dto/request/AwardRequestDto.java @@ -2,33 +2,26 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.award.domain.Award; +import page.clab.api.domain.member.domain.Member; + +import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class AwardRequestDto { @NotNull(message = "{notNull.award.competitionName}") - @Size(min = 1, max = 255, message = "{size.award.competitionName}") @Schema(description = "대회명", example = "제10회 소프트웨어 개발보안 시큐어코딩 해커톤", required = true) private String competitionName; @NotNull(message = "{notNull.award.organizer}") - @Size(min = 1, max = 255, message = "{size.award.organizer}") @Schema(description = "주최기관", example = "한국정보보호학회", required = true) private String organizer; @NotNull(message = "{notNull.award.awardName}") - @Size(min = 1, max = 255, message = "{size.award.awardName}") @Schema(description = "수상명", example = "우수상", required = true) private String awardName; @@ -36,4 +29,14 @@ public class AwardRequestDto { @Schema(description = "수상일", example = "2023-08-18", required = true) private LocalDate awardDate; + public static Award toEntity(AwardRequestDto requestDto, Member member) { + return Award.builder() + .competitionName(requestDto.getCompetitionName()) + .organizer(requestDto.getOrganizer()) + .awardName(requestDto.getAwardName()) + .awardDate(requestDto.getAwardDate()) + .member(member) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/award/dto/request/AwardUpdateRequestDto.java b/src/main/java/page/clab/api/domain/award/dto/request/AwardUpdateRequestDto.java index d982b6f2e..eeb650d48 100644 --- a/src/main/java/page/clab/api/domain/award/dto/request/AwardUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/award/dto/request/AwardUpdateRequestDto.java @@ -1,31 +1,21 @@ package page.clab.api.domain.award.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class AwardUpdateRequestDto { - @Size(min = 1, max = 255, message = "{size.award.competitionName}") @Schema(description = "대회명", example = "제10회 소프트웨어 개발보안 시큐어코딩 해커톤") private String competitionName; - @Size(min = 1, max = 255, message = "{size.award.organizer}") @Schema(description = "주최기관", example = "한국정보보호학회") private String organizer; - @Size(min = 1, max = 255, message = "{size.award.awardName}") @Schema(description = "수상명", example = "우수상") private String awardName; diff --git a/src/main/java/page/clab/api/domain/award/dto/response/AwardResponseDto.java b/src/main/java/page/clab/api/domain/award/dto/response/AwardResponseDto.java index 18c190acb..585866481 100644 --- a/src/main/java/page/clab/api/domain/award/dto/response/AwardResponseDto.java +++ b/src/main/java/page/clab/api/domain/award/dto/response/AwardResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.award.dto.response; -import java.time.LocalDate; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.award.domain.Award; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class AwardResponseDto { @@ -26,8 +20,14 @@ public class AwardResponseDto { private LocalDate awardDate; - public static AwardResponseDto of(Award award) { - return ModelMapperUtil.getModelMapper().map(award, AwardResponseDto.class); + public static AwardResponseDto toDto(Award award) { + return AwardResponseDto.builder() + .id(award.getId()) + .competitionName(award.getCompetitionName()) + .organizer(award.getOrganizer()) + .awardName(award.getAwardName()) + .awardDate(award.getAwardDate()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/blacklistIp/api/BlacklistIpController.java b/src/main/java/page/clab/api/domain/blacklistIp/api/BlacklistIpController.java index 4b1b755bf..5908cb395 100644 --- a/src/main/java/page/clab/api/domain/blacklistIp/api/BlacklistIpController.java +++ b/src/main/java/page/clab/api/domain/blacklistIp/api/BlacklistIpController.java @@ -20,10 +20,12 @@ import page.clab.api.domain.blacklistIp.domain.BlacklistIp; import page.clab.api.domain.blacklistIp.dto.request.BlacklistIpRequestDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; + +import java.util.List; @RestController -@RequestMapping("/blacklists") +@RequestMapping("/api/v1/blacklists") @RequiredArgsConstructor @Tag(name = "Blacklist IP", description = "블랙리스트 IP") @Slf4j @@ -34,52 +36,45 @@ public class BlacklistIpController { @Operation(summary = "[S] 블랙리스트 IP 추가", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("") - public ResponseModel addBlacklistedIp( + public ApiResponse addBlacklistedIp( HttpServletRequest request, - @Valid @RequestBody BlacklistIpRequestDto blacklistIpRequestDto + @Valid @RequestBody BlacklistIpRequestDto requestDto ) { - String addedIp = blacklistIpService.addBlacklistedIp(request, blacklistIpRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(addedIp); - return responseModel; + String addedIp = blacklistIpService.addBlacklistedIp(request, requestDto); + return ApiResponse.success(addedIp); } @Operation(summary = "[A] 블랙리스트 IP 목록 조회", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getBlacklistedIps( + public ApiResponse> getBlacklistedIps( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto blacklistedIps = blacklistIpService.getBlacklistedIps(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(blacklistedIps); - return responseModel; + return ApiResponse.success(blacklistedIps); } @Operation(summary = "[S] 블랙리스트 IP 제거", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("") - public ResponseModel removeBlacklistedIp( + public ApiResponse removeBlacklistedIp( HttpServletRequest request, @RequestParam(name = "ipAddress") String ipAddress ) { String deletedIp = blacklistIpService.deleteBlacklistedIp(request, ipAddress); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(deletedIp); - return responseModel; + return ApiResponse.success(deletedIp); } @Operation(summary = "[S] 블랙리스트 IP 초기화", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/clear") - public ResponseModel clearBlacklist( + public ApiResponse> clearBlacklist( HttpServletRequest request ) { - blacklistIpService.clearBlacklist(request); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + List blacklistIps = blacklistIpService.clearBlacklist(request); + return ApiResponse.success(blacklistIps); } } diff --git a/src/main/java/page/clab/api/domain/blacklistIp/application/BlacklistIpService.java b/src/main/java/page/clab/api/domain/blacklistIp/application/BlacklistIpService.java index 12f5f48cd..8d786899b 100644 --- a/src/main/java/page/clab/api/domain/blacklistIp/application/BlacklistIpService.java +++ b/src/main/java/page/clab/api/domain/blacklistIp/application/BlacklistIpService.java @@ -1,12 +1,12 @@ package page.clab.api.domain.blacklistIp.application; import jakarta.servlet.http.HttpServletRequest; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.blacklistIp.dao.BlacklistIpRepository; import page.clab.api.domain.blacklistIp.domain.BlacklistIp; import page.clab.api.domain.blacklistIp.dto.request.BlacklistIpRequestDto; @@ -15,6 +15,8 @@ import page.clab.api.global.common.slack.domain.SecurityAlertType; import page.clab.api.global.exception.NotFoundException; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j @@ -24,18 +26,20 @@ public class BlacklistIpService { private final BlacklistIpRepository blacklistIpRepository; - public String addBlacklistedIp(HttpServletRequest request, BlacklistIpRequestDto dto) { - String ipAddress = dto.getIpAddress(); + @Transactional + public String addBlacklistedIp(HttpServletRequest request, BlacklistIpRequestDto requestDto) { + String ipAddress = requestDto.getIpAddress(); return blacklistIpRepository.findByIpAddress(ipAddress) .map(BlacklistIp::getIpAddress) .orElseGet(() -> { - BlacklistIp blacklistIp = BlacklistIp.create(ipAddress, dto.getReason()); + BlacklistIp blacklistIp = BlacklistIpRequestDto.toEntity(requestDto); blacklistIpRepository.save(blacklistIp); slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Added IP: " + ipAddress); return ipAddress; }); } + @Transactional(readOnly = true) public PagedResponseDto getBlacklistedIps(Pageable pageable) { Page blacklistedIps = blacklistIpRepository.findAllByOrderByCreatedAtDesc(pageable); return new PagedResponseDto<>(blacklistedIps); @@ -45,13 +49,18 @@ public PagedResponseDto getBlacklistedIps(Pageable pageable) { public String deleteBlacklistedIp(HttpServletRequest request, String ipAddress) { BlacklistIp blacklistIp = getBlacklistIpByIpAddressOrThrow(ipAddress); blacklistIpRepository.delete(blacklistIp); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Deleted IP: " + ipAddress); + slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: " + ipAddress); return blacklistIp.getIpAddress(); } - public void clearBlacklist(HttpServletRequest request) { + public List clearBlacklist(HttpServletRequest request) { + List blacklistedIps = blacklistIpRepository.findAll() + .stream() + .map(BlacklistIp::getIpAddress) + .toList(); blacklistIpRepository.deleteAll(); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Deleted IP: ALL"); + slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: ALL"); + return blacklistedIps; } private BlacklistIp getBlacklistIpByIpAddressOrThrow(String ipAddress) { diff --git a/src/main/java/page/clab/api/domain/blacklistIp/domain/BlacklistIp.java b/src/main/java/page/clab/api/domain/blacklistIp/domain/BlacklistIp.java index eeb4df4c7..a218f0048 100644 --- a/src/main/java/page/clab/api/domain/blacklistIp/domain/BlacklistIp.java +++ b/src/main/java/page/clab/api/domain/blacklistIp/domain/BlacklistIp.java @@ -5,20 +5,21 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; +import lombok.Setter; +import page.clab.api.global.common.domain.BaseEntity; @Entity -@Data +@Getter +@Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class BlacklistIp { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class BlacklistIp extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,17 +30,11 @@ public class BlacklistIp { private String reason; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - private BlacklistIp(String ipAddress, String reason) { - this.ipAddress = ipAddress; - this.reason = reason; - } - public static BlacklistIp create(String ipAddress, String reason) { - return new BlacklistIp(ipAddress, reason); + return BlacklistIp.builder() + .ipAddress(ipAddress) + .reason(reason) + .build(); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/blacklistIp/dto/request/BlacklistIpRequestDto.java b/src/main/java/page/clab/api/domain/blacklistIp/dto/request/BlacklistIpRequestDto.java index a705feedf..5396d855d 100644 --- a/src/main/java/page/clab/api/domain/blacklistIp/dto/request/BlacklistIpRequestDto.java +++ b/src/main/java/page/clab/api/domain/blacklistIp/dto/request/BlacklistIpRequestDto.java @@ -2,17 +2,12 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.blacklistIp.domain.BlacklistIp; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BlacklistIpRequestDto { @NotNull(message = "{notNull.blacklistIp.ipAddress}") @@ -22,4 +17,11 @@ public class BlacklistIpRequestDto { @Schema(description = "블랙리스트 사유", example = "스팸") private String reason; + public static BlacklistIp toEntity(BlacklistIpRequestDto requestDto) { + return BlacklistIp.builder() + .ipAddress(requestDto.getIpAddress()) + .reason(requestDto.getReason()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/blog/api/BlogController.java b/src/main/java/page/clab/api/domain/blog/api/BlogController.java index 2cacb7178..9b9ca209e 100644 --- a/src/main/java/page/clab/api/domain/blog/api/BlogController.java +++ b/src/main/java/page/clab/api/domain/blog/api/BlogController.java @@ -22,12 +22,12 @@ import page.clab.api.domain.blog.dto.request.BlogUpdateRequestDto; import page.clab.api.domain.blog.dto.response.BlogDetailsResponseDto; import page.clab.api.domain.blog.dto.response.BlogResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/blogs") +@RequestMapping("/api/v1/blogs") @RequiredArgsConstructor @Tag(name = "Blog", description = "블로그 포스트") @Slf4j @@ -38,13 +38,11 @@ public class BlogController { @Operation(summary = "[A] 블로그 포스트 생성", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createBlog( - @Valid @RequestBody BlogRequestDto blogRequestDto + public ApiResponse createBlog( + @Valid @RequestBody BlogRequestDto requestDto ) { - Long id = blogService.createBlog(blogRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = blogService.createBlog(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 블로그 포스트 조회(제목, 작성자명 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -52,7 +50,7 @@ public ResponseModel createBlog( "제목, 작성자명 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getBlogsByConditions( + public ApiResponse> getBlogsByConditions( @RequestParam(name = "title", required = false) String title, @RequestParam(name = "memberName", required = false) String memberName, @RequestParam(name = "page", defaultValue = "0") int page, @@ -60,46 +58,50 @@ public ResponseModel getBlogsByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto blogs = blogService.getBlogsByConditions(title, memberName, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(blogs); - return responseModel; + return ApiResponse.success(blogs); } @Operation(summary = "[U] 블로그 포스트 상세 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/{blogId}") - public ResponseModel getBlogDetails( + public ApiResponse getBlogDetails( @PathVariable(name = "blogId") Long blogId ) { BlogDetailsResponseDto blog = blogService.getBlogDetails(blogId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(blog); - return responseModel; + return ApiResponse.success(blog); } @Operation(summary = "[A] 블로그 포스트 수정", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{blogId}") - public ResponseModel updateBlog( + public ApiResponse updateBlog( @PathVariable(name = "blogId") Long blogId, - @Valid @RequestBody BlogUpdateRequestDto blogUpdateRequestDto + @Valid @RequestBody BlogUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = blogService.updateBlog(blogId, blogUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = blogService.updateBlog(blogId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 블로그 포스트 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{blogId}") - public ResponseModel deleteBlog( + public ApiResponse deleteBlog( @PathVariable(name = "blogId") Long blogId ) throws PermissionDeniedException { Long id = blogService.deleteBlog(blogId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 블로그 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedBlogs( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto blogs = blogService.getDeletedBlogs(pageable); + return ApiResponse.success(blogs); } } diff --git a/src/main/java/page/clab/api/domain/blog/application/BlogService.java b/src/main/java/page/clab/api/domain/blog/application/BlogService.java index 8ab3cf930..0abc22dac 100644 --- a/src/main/java/page/clab/api/domain/blog/application/BlogService.java +++ b/src/main/java/page/clab/api/domain/blog/application/BlogService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.blog.dao.BlogRepository; import page.clab.api.domain.blog.domain.Blog; import page.clab.api.domain.blog.dto.request.BlogRequestDto; @@ -15,6 +16,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -22,38 +24,54 @@ public class BlogService { private final MemberService memberService; + private final ValidationService validationService; + private final BlogRepository blogRepository; - public Long createBlog(BlogRequestDto blogRequestDto) { - Member member = memberService.getCurrentMember(); - Blog blog = Blog.create(blogRequestDto, member); + @Transactional + public Long createBlog(BlogRequestDto requestDto) { + Member currentMember = memberService.getCurrentMember(); + Blog blog = BlogRequestDto.toEntity(requestDto, currentMember); + validationService.checkValid(blog); return blogRepository.save(blog).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getBlogsByConditions(String title, String memberName, Pageable pageable) { Page blogs = blogRepository.findByConditions(title, memberName, pageable); - return new PagedResponseDto<>(blogs.map(BlogResponseDto::of)); + return new PagedResponseDto<>(blogs.map(BlogResponseDto::toDto)); } + @Transactional(readOnly = true) public BlogDetailsResponseDto getBlogDetails(Long blogId) { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Blog blog = getBlogByIdOrThrow(blogId); - boolean isOwner = blog.isOwner(member); - return BlogDetailsResponseDto.create(blog, isOwner); + boolean isOwner = blog.isOwner(currentMember); + return BlogDetailsResponseDto.toDto(blog, isOwner); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedBlogs(Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + Page blogs = blogRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(blogs + .map(blog -> BlogDetailsResponseDto.toDto(blog, blog.isOwner(currentMember)))); } - public Long updateBlog(Long blogId, BlogUpdateRequestDto blogUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + @Transactional + public Long updateBlog(Long blogId, BlogUpdateRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); Blog blog = getBlogByIdOrThrow(blogId); - blog.validateAccessPermission(member); - blog.update(blogUpdateRequestDto); + blog.validateAccessPermission(currentMember); + blog.update(requestDto); + validationService.checkValid(blog); return blogRepository.save(blog).getId(); } public Long deleteBlog(Long blogId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Blog blog = getBlogByIdOrThrow(blogId); - blog.validateAccessPermission(member); + blog.validateAccessPermission(currentMember); blogRepository.delete(blog); return blog.getId(); } diff --git a/src/main/java/page/clab/api/domain/blog/dao/BlogRepository.java b/src/main/java/page/clab/api/domain/blog/dao/BlogRepository.java index b49e33e3a..57c5f8720 100644 --- a/src/main/java/page/clab/api/domain/blog/dao/BlogRepository.java +++ b/src/main/java/page/clab/api/domain/blog/dao/BlogRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.blog.domain.Blog; @@ -12,4 +13,7 @@ public interface BlogRepository extends JpaRepository, BlogRepositor Page findAllByOrderByCreatedAtDesc(Pageable pageable); + @Query(value = "SELECT b.* FROM blog b WHERE b.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/blog/domain/Blog.java b/src/main/java/page/clab/api/domain/blog/domain/Blog.java index c134ec16a..02f1cea59 100644 --- a/src/main/java/page/clab/api/domain/blog/domain/Blog.java +++ b/src/main/java/page/clab/api/domain/blog/domain/Blog.java @@ -8,28 +8,30 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.blog.dto.request.BlogRequestDto; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.blog.dto.request.BlogUpdateRequestDto; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Blog { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE blog SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Blog extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -40,32 +42,23 @@ public class Blog { private Member member; @Column(nullable = false) + @Size(min = 1, max = 100, message = "{size.blog.title}") private String title; @Column(nullable = false) private String subTitle; - @Column(nullable = false, length = 10000) + @Column(nullable = false) @Size(min = 1, max = 10000, message = "{size.blog.content}") private String content; private String imageUrl; - @CreationTimestamp - @Column(updatable = false) - private LocalDateTime createdAt; - - public static Blog create(BlogRequestDto blogRequestDto, Member member) { - Blog blog = ModelMapperUtil.getModelMapper().map(blogRequestDto, Blog.class); - blog.setMember(member); - return blog; - } - - public void update(BlogUpdateRequestDto blogUpdateRequestDto) { - Optional.ofNullable(blogUpdateRequestDto.getTitle()).ifPresent(this::setTitle); - Optional.ofNullable(blogUpdateRequestDto.getSubTitle()).ifPresent(this::setSubTitle); - Optional.ofNullable(blogUpdateRequestDto.getContent()).ifPresent(this::setContent); - Optional.ofNullable(blogUpdateRequestDto.getImageUrl()).ifPresent(this::setImageUrl); + public void update(BlogUpdateRequestDto requestDto) { + Optional.ofNullable(requestDto.getTitle()).ifPresent(this::setTitle); + Optional.ofNullable(requestDto.getSubTitle()).ifPresent(this::setSubTitle); + Optional.ofNullable(requestDto.getContent()).ifPresent(this::setContent); + Optional.ofNullable(requestDto.getImageUrl()).ifPresent(this::setImageUrl); } public boolean isOwner(Member member) { diff --git a/src/main/java/page/clab/api/domain/blog/dto/request/BlogRequestDto.java b/src/main/java/page/clab/api/domain/blog/dto/request/BlogRequestDto.java index ce111d2a7..a724f0134 100644 --- a/src/main/java/page/clab/api/domain/blog/dto/request/BlogRequestDto.java +++ b/src/main/java/page/clab/api/domain/blog/dto/request/BlogRequestDto.java @@ -2,36 +2,38 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.blog.domain.Blog; +import page.clab.api.domain.member.domain.Member; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BlogRequestDto { @NotNull(message = "{notNull.blog.title}") - @Size(min = 1, max = 255, message = "{size.blog.title}") @Schema(description = "제목", example = "Swagger Docs의 접근 권한을 제어하기 위한 여정", required = true) private String title; @NotNull(message = "{notNull.blog.subTitle}") - @Size(min = 1, max = 255, message = "{size.blog.subTitle}") @Schema(description = "부제목", example = "Basic Auth와 JWT를 같이 사용하기 위한 Spring Security 디버깅", required = true) private String subTitle; @NotNull(message = "{notNull.blog.content}") - @Size(min = 1, max = 10000, message = "{size.blog.content}") @Schema(description = "내용", example = "NestJs는 스웨거 설정에 있던데 스프링은........", required = true) private String content; @Schema(description = "이미지 URL", example = "https://www.clab.page/assets/logoWhite-fc1ef9a0.webp") private String imageUrl; + public static Blog toEntity(BlogRequestDto requestDto, Member member) { + return Blog.builder() + .member(member) + .title(requestDto.getTitle()) + .subTitle(requestDto.getSubTitle()) + .content(requestDto.getContent()) + .imageUrl(requestDto.getImageUrl()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/blog/dto/request/BlogUpdateRequestDto.java b/src/main/java/page/clab/api/domain/blog/dto/request/BlogUpdateRequestDto.java index 34f960902..20df92a43 100644 --- a/src/main/java/page/clab/api/domain/blog/dto/request/BlogUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/blog/dto/request/BlogUpdateRequestDto.java @@ -1,29 +1,19 @@ package page.clab.api.domain.blog.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BlogUpdateRequestDto { - @Size(min = 1, max = 255, message = "{size.blog.title}") @Schema(description = "제목", example = "Swagger Docs의 접근 권한을 제어하기 위한 여정") private String title; - @Size(min = 1, max = 255, message = "{size.blog.subTitle}") @Schema(description = "부제목", example = "Basic Auth와 JWT를 같이 사용하기 위한 Spring Security 디버깅") private String subTitle; - @Size(min = 1, max = 10000, message = "{size.blog.content}") @Schema(description = "내용", example = "NestJs는 스웨거 설정에 있던데 스프링은........") private String content; diff --git a/src/main/java/page/clab/api/domain/blog/dto/response/BlogDetailsResponseDto.java b/src/main/java/page/clab/api/domain/blog/dto/response/BlogDetailsResponseDto.java index 027fce1b2..8853f454b 100644 --- a/src/main/java/page/clab/api/domain/blog/dto/response/BlogDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/blog/dto/response/BlogDetailsResponseDto.java @@ -1,20 +1,13 @@ package page.clab.api.domain.blog.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.blog.domain.Blog; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class BlogDetailsResponseDto { @@ -37,12 +30,18 @@ public class BlogDetailsResponseDto { private LocalDateTime createdAt; - public static BlogDetailsResponseDto create(Blog blog, boolean isOwner) { - BlogDetailsResponseDto blogResponseDto = ModelMapperUtil.getModelMapper().map(blog, BlogDetailsResponseDto.class); - blogResponseDto.setMemberId(blog.getMember().getId()); - blogResponseDto.setName(blog.getMember().getName()); - blogResponseDto.setIsOwner(isOwner); - return blogResponseDto; + public static BlogDetailsResponseDto toDto(Blog blog, boolean isOwner) { + return BlogDetailsResponseDto.builder() + .id(blog.getId()) + .memberId(blog.getMember().getId()) + .name(blog.getMember().getName()) + .title(blog.getTitle()) + .subTitle(blog.getSubTitle()) + .content(blog.getContent()) + .imageUrl(blog.getImageUrl()) + .isOwner(isOwner) + .createdAt(blog.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/blog/dto/response/BlogResponseDto.java b/src/main/java/page/clab/api/domain/blog/dto/response/BlogResponseDto.java index 8fe7cd204..2aba1b090 100644 --- a/src/main/java/page/clab/api/domain/blog/dto/response/BlogResponseDto.java +++ b/src/main/java/page/clab/api/domain/blog/dto/response/BlogResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.blog.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.blog.domain.Blog; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class BlogResponseDto { @@ -26,8 +20,14 @@ public class BlogResponseDto { private LocalDateTime createdAt; - public static BlogResponseDto of(Blog blog) { - return ModelMapperUtil.getModelMapper().map(blog, BlogResponseDto.class); + public static BlogResponseDto toDto(Blog blog) { + return BlogResponseDto.builder() + .id(blog.getId()) + .title(blog.getTitle()) + .subTitle(blog.getSubTitle()) + .imageUrl(blog.getImageUrl()) + .createdAt(blog.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/board/api/BoardController.java b/src/main/java/page/clab/api/domain/board/api/BoardController.java index 74a0b5ad2..1e633370f 100644 --- a/src/main/java/page/clab/api/domain/board/api/BoardController.java +++ b/src/main/java/page/clab/api/domain/board/api/BoardController.java @@ -18,17 +18,19 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import page.clab.api.domain.board.application.BoardService; +import page.clab.api.domain.board.domain.BoardCategory; import page.clab.api.domain.board.dto.request.BoardRequestDto; import page.clab.api.domain.board.dto.request.BoardUpdateRequestDto; import page.clab.api.domain.board.dto.response.BoardCategoryResponseDto; import page.clab.api.domain.board.dto.response.BoardDetailsResponseDto; import page.clab.api.domain.board.dto.response.BoardListResponseDto; +import page.clab.api.domain.board.dto.response.BoardMyResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/boards") +@RequestMapping("/api/v1/boards") @RequiredArgsConstructor @Tag(name = "Board", description = "커뮤니티 게시판") @Slf4j @@ -39,105 +41,101 @@ public class BoardController { @Operation(summary = "[U] 커뮤니티 게시글 생성", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createBoard( - @Valid @RequestBody BoardRequestDto boardRequestDto - ) { - Long id = boardService.createBoard(boardRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + public ApiResponse createBoard( + @Valid @RequestBody BoardRequestDto requestDto + ) throws PermissionDeniedException { + Long id = boardService.createBoard(requestDto); + return ApiResponse.success(id); } @GetMapping("") @Operation(summary = "[U] 커뮤니티 게시글 목록 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel getBoards( + public ApiResponse> getBoards( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto boards = boardService.getBoards(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(boards); - return responseModel; + return ApiResponse.success(boards); } @GetMapping("/{boardId}") @Operation(summary = "[U] 커뮤니티 게시글 상세 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel getBoardDetails( + public ApiResponse getBoardDetails( @PathVariable(name = "boardId") Long boardId ) { BoardDetailsResponseDto board = boardService.getBoardDetails(boardId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(board); - return responseModel; + return ApiResponse.success(board); } @GetMapping("/my-boards") @Operation(summary = "[U] 내가 쓴 커뮤니티 게시글 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel getMyBoards( + public ApiResponse> getMyBoards( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto board = boardService.getMyBoards(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(board); - return responseModel; + PagedResponseDto board = boardService.getMyBoards(pageable); + return ApiResponse.success(board); } - @GetMapping("/list") + @GetMapping("/category") @Operation(summary = "[U] 커뮤니티 게시글 카테고리별 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel getBoardsByCategory( - @RequestParam(name = "category") String category, + public ApiResponse> getBoardsByCategory( + @RequestParam(name = "category") BoardCategory category, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto boards = boardService.getBoardsByCategory(category, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(boards); - return responseModel; + return ApiResponse.success(boards); } @PatchMapping("/{boardId}") @Operation(summary = "[U] 커뮤니티 게시글 수정", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel updateBoard( + public ApiResponse updateBoard( @PathVariable(name = "boardId") Long boardId, - @Valid @RequestBody BoardUpdateRequestDto boardUpdateRequestDto + @Valid @RequestBody BoardUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = boardService.updateBoard(boardId, boardUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = boardService.updateBoard(boardId, requestDto); + return ApiResponse.success(id); } @DeleteMapping("/{boardId}") @Operation(summary = "[U] 커뮤니티 게시글 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel deleteBoard( + public ApiResponse deleteBoard( @PathVariable(name = "boardId") Long boardId ) throws PermissionDeniedException { Long id = boardService.deleteBoard(boardId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @PostMapping("/likes/{boardId}") @Operation(summary = "[U] 커뮤니티 게시글 좋아요 누르기/취소하기", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel toggleLikeStatus( + public ApiResponse toggleLikeStatus( @PathVariable(name = "boardId") Long boardId ) { Long id = boardService.toggleLikeStatus(boardId); - ResponseModel responseModel= ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 커뮤니티 게시글 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedBoards( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto boards = boardService.getDeletedBoards(pageable); + return ApiResponse.success(boards); } } diff --git a/src/main/java/page/clab/api/domain/board/application/BoardService.java b/src/main/java/page/clab/api/domain/board/application/BoardService.java index b3d73d505..143e111a0 100644 --- a/src/main/java/page/clab/api/domain/board/application/BoardService.java +++ b/src/main/java/page/clab/api/domain/board/application/BoardService.java @@ -1,33 +1,35 @@ package page.clab.api.domain.board.application; -import jakarta.transaction.Transactional; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.board.dao.BoardLikeRepository; import page.clab.api.domain.board.dao.BoardRepository; import page.clab.api.domain.board.domain.Board; +import page.clab.api.domain.board.domain.BoardCategory; import page.clab.api.domain.board.domain.BoardLike; import page.clab.api.domain.board.dto.request.BoardRequestDto; import page.clab.api.domain.board.dto.request.BoardUpdateRequestDto; import page.clab.api.domain.board.dto.response.BoardCategoryResponseDto; import page.clab.api.domain.board.dto.response.BoardDetailsResponseDto; import page.clab.api.domain.board.dto.response.BoardListResponseDto; +import page.clab.api.domain.board.dto.response.BoardMyResponseDto; +import page.clab.api.domain.comment.dao.CommentRepository; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.file.application.FileService; +import page.clab.api.global.common.file.application.UploadedFileService; import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; -import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -37,57 +39,64 @@ public class BoardService { private final NotificationService notificationService; + private final UploadedFileService uploadedFileService; + + private final ValidationService validationService; + private final BoardRepository boardRepository; private final BoardLikeRepository boardLikeRepository; - private final FileService fileService; + private final CommentRepository commentRepository; @Transactional - public Long createBoard(BoardRequestDto dto) { - Member member = memberService.getCurrentMember(); - List uploadedFiles = prepareUploadedFiles(dto.getFileUrlList()); - Board board = Board.create(dto, member, uploadedFiles); - + public Long createBoard(BoardRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); + List uploadedFiles = uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrlList()); + Board board = BoardRequestDto.toEntity(requestDto, currentMember, uploadedFiles); + board.validateAccessPermissionForCreation(currentMember); + validationService.checkValid(board); if (board.shouldNotifyForNewBoard()) { - notificationService.sendNotificationToMember( - member, - "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다." - ); + notificationService.sendNotificationToMember(currentMember, "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다."); } return boardRepository.save(board).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getBoards(Pageable pageable) { Page boards = boardRepository.findAllByOrderByCreatedAtDesc(pageable); - return new PagedResponseDto<>(boards.map(BoardListResponseDto::of)); + return new PagedResponseDto<>(boards.map(this::mapToBoardListResponseDto)); } + @Transactional(readOnly = true) public BoardDetailsResponseDto getBoardDetails(Long boardId) { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Board board = getBoardByIdOrThrow(boardId); - boolean hasLikeByMe = checkLikeStatus(board, member); - boolean isOwner = board.isOwner(member); - return BoardDetailsResponseDto.create(board, hasLikeByMe, isOwner); + boolean hasLikeByMe = checkLikeStatus(board, currentMember); + boolean isOwner = board.isOwner(currentMember); + return BoardDetailsResponseDto.toDto(board, hasLikeByMe, isOwner); } - public PagedResponseDto getMyBoards(Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page boards = getBoardByMember(pageable, member); - return new PagedResponseDto<>(boards.map(BoardCategoryResponseDto::of)); + @Transactional(readOnly = true) + public PagedResponseDto getMyBoards(Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + Page boards = getBoardByMember(pageable, currentMember); + return new PagedResponseDto<>(boards.map(BoardMyResponseDto::toDto)); } - public PagedResponseDto getBoardsByCategory(String category, Pageable pageable) { - Page boards; - boards = getBoardByCategory(category, pageable); - return new PagedResponseDto<>(boards.map(BoardCategoryResponseDto::of)); + @Transactional(readOnly = true) + public PagedResponseDto getBoardsByCategory(BoardCategory category, Pageable pageable) { + Page boards = getBoardByCategory(category, pageable); + return new PagedResponseDto<>(boards.map(BoardCategoryResponseDto::toDto)); } - public Long updateBoard(Long boardId, BoardUpdateRequestDto boardUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + @Transactional + public Long updateBoard(Long boardId, BoardUpdateRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); Board board = getBoardByIdOrThrow(boardId); - board.checkPermission(member); - board.update(boardUpdateRequestDto); + board.validateAccessPermission(currentMember); + board.update(requestDto); + validationService.checkValid(board); return boardRepository.save(board).getId(); } @@ -101,45 +110,46 @@ public Long toggleLikeStatus(Long boardId) { boardLikeRepository.delete(boardLikeOpt.get()); } else { board.incrementLikes(); - BoardLike newBoardLike = new BoardLike(currentMember.getId(), board.getId()); + BoardLike newBoardLike = BoardLike.create(currentMember.getId(), board.getId()); + validationService.checkValid(newBoardLike); boardLikeRepository.save(newBoardLike); } return board.getLikes(); } + @Transactional(readOnly = true) + public PagedResponseDto getDeletedBoards(Pageable pageable) { + Page boards = boardRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(boards.map(this::mapToBoardListResponseDto)); + } + public Long deleteBoard(Long boardId) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); Board board = getBoardByIdOrThrow(boardId); - board.checkPermission(currentMember); + board.validateAccessPermission(currentMember); boardRepository.delete(board); return board.getId(); } + @NotNull + private BoardListResponseDto mapToBoardListResponseDto(Board board) { + Long commentCount = commentRepository.countByBoard(board); + return BoardListResponseDto.toDto(board, commentCount); + } + public Board getBoardByIdOrThrow(Long boardId) { return boardRepository.findById(boardId) .orElseThrow(() -> new NotFoundException("해당 게시글이 존재하지 않습니다.")); } - public boolean isBoardExistById(Long boardId) { - return boardRepository.existsById(boardId); - } - private Page getBoardByMember(Pageable pageable, Member member) { return boardRepository.findAllByMemberOrderByCreatedAtDesc(member, pageable); } - private Page getBoardByCategory(String category, Pageable pageable) { + private Page getBoardByCategory(BoardCategory category, Pageable pageable) { return boardRepository.findAllByCategoryOrderByCreatedAtDesc(category, pageable); } - @NotNull - private List prepareUploadedFiles(List fileUrls) { - if (fileUrls == null) return new ArrayList<>(); - return fileUrls.stream() - .map(fileService::getUploadedFileByUrl) - .collect(Collectors.toList()); - } - private boolean checkLikeStatus(Board board, Member member) { return boardLikeRepository.existsByBoardIdAndMemberId(board.getId(), member.getId()); } diff --git a/src/main/java/page/clab/api/domain/board/dao/BoardRepository.java b/src/main/java/page/clab/api/domain/board/dao/BoardRepository.java index 567715464..2bf12a880 100644 --- a/src/main/java/page/clab/api/domain/board/dao/BoardRepository.java +++ b/src/main/java/page/clab/api/domain/board/dao/BoardRepository.java @@ -3,8 +3,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import page.clab.api.domain.board.domain.Board; +import page.clab.api.domain.board.domain.BoardCategory; import page.clab.api.domain.member.domain.Member; @Repository @@ -14,6 +16,9 @@ public interface BoardRepository extends JpaRepository { Page findAllByMemberOrderByCreatedAtDesc(Member member, Pageable pageable); - Page findAllByCategoryOrderByCreatedAtDesc(String category, Pageable pageable); + Page findAllByCategoryOrderByCreatedAtDesc(BoardCategory category, Pageable pageable); + + @Query(value = "SELECT b.* FROM board b WHERE b.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/board/domain/Board.java b/src/main/java/page/clab/api/domain/board/domain/Board.java index ce0a96f71..99847c588 100644 --- a/src/main/java/page/clab/api/domain/board/domain/Board.java +++ b/src/main/java/page/clab/api/domain/board/domain/Board.java @@ -2,6 +2,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -10,23 +12,21 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.board.dto.request.BoardRequestDto; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.board.dto.request.BoardUpdateRequestDto; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.member.domain.Role; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; -import page.clab.api.global.util.RandomNicknameUtil; -import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -34,9 +34,11 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Board { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE board SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Board extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -47,55 +49,49 @@ public class Board { private Member member; @Column(nullable = false) - private String nickName; + private String nickname; @Column(nullable = false) - @Size(min = 1, max = 50, message = "{size.board.category}") - private String category; + @Enumerated(EnumType.STRING) + private BoardCategory category; @Column(nullable = false) @Size(min = 1, max = 100, message = "{size.board.title}") private String title; - @Column(nullable = false, length = 10000) + @Column(nullable = false) @Size(min = 1, max = 10000, message = "{size.board.content}") private String content; @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "board_files") - private List uploadedFiles = new ArrayList<>(); - - @Column(name = "update_time") - private LocalDateTime updateTime; + private List uploadedFiles; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; + private String imageUrl; + @Column(nullable = false) private boolean wantAnonymous; private Long likes; - public static Board create(BoardRequestDto dto, Member member, List uploadedFiles) { - Board board = ModelMapperUtil.getModelMapper().map(dto, Board.class); - board.setMember(member); - board.setUploadedFiles(uploadedFiles); - board.setNickName(RandomNicknameUtil.makeRandomNickname()); - board.setWantAnonymous(dto.isWantAnonymous()); - board.setLikes(0L); - return board; - } - public void update(BoardUpdateRequestDto boardUpdateRequestDto) { Optional.ofNullable(boardUpdateRequestDto.getCategory()).ifPresent(this::setCategory); Optional.ofNullable(boardUpdateRequestDto.getTitle()).ifPresent(this::setTitle); Optional.ofNullable(boardUpdateRequestDto.getContent()).ifPresent(this::setContent); + Optional.ofNullable(boardUpdateRequestDto.getImageUrl()).ifPresent(this::setImageUrl); Optional.of(boardUpdateRequestDto.isWantAnonymous()).ifPresent(this::setWantAnonymous); - updateTime = LocalDateTime.now(); + } + + public boolean isNotice() { + return this.category.equals(BoardCategory.NOTICE); + } + + public boolean isGraduated() { + return this.category.equals(BoardCategory.GRADUATED); } public boolean shouldNotifyForNewBoard() { - return !this.member.getRole().equals(Role.USER) && this.category.equals("공지사항"); + return !this.member.getRole().equals(Role.USER) && this.category.equals(BoardCategory.NOTICE); } public void incrementLikes() { @@ -112,10 +108,19 @@ public boolean isOwner(Member member) { return this.member.isSameMember(member); } - public void checkPermission(Member member) throws PermissionDeniedException { + public void validateAccessPermission(Member member) throws PermissionDeniedException { if (!isOwner(member) && !member.isAdminRole()) { throw new PermissionDeniedException("해당 게시글을 수정할 권한이 없습니다."); } } + public void validateAccessPermissionForCreation(Member currentMember) throws PermissionDeniedException { + if (this.isNotice() && !currentMember.isAdminRole()) { + throw new PermissionDeniedException("공지사항은 관리자만 작성할 수 있습니다."); + } + if (this.isGraduated() && !currentMember.isGraduated()) { + throw new PermissionDeniedException("졸업생 게시판은 졸업생만 작성할 수 있습니다."); + } + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/board/domain/BoardCategory.java b/src/main/java/page/clab/api/domain/board/domain/BoardCategory.java new file mode 100644 index 000000000..e95900f18 --- /dev/null +++ b/src/main/java/page/clab/api/domain/board/domain/BoardCategory.java @@ -0,0 +1,19 @@ +package page.clab.api.domain.board.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BoardCategory { + + NOTICE("notice", "공지사항"), + FREE("free", "자유 게시판"), + QNA("qna", "질문 게시판"), + GRADUATED("graduated", "졸업생 게시판"), + ORGANIZATION("organization", "동아리 소식"); + + private String key; + private String description; + +} diff --git a/src/main/java/page/clab/api/domain/board/domain/BoardLike.java b/src/main/java/page/clab/api/domain/board/domain/BoardLike.java index 43948ec3b..6422a8a1c 100644 --- a/src/main/java/page/clab/api/domain/board/domain/BoardLike.java +++ b/src/main/java/page/clab/api/domain/board/domain/BoardLike.java @@ -6,20 +6,22 @@ import jakarta.persistence.Id; import jakarta.persistence.Index; import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.global.common.domain.BaseEntity; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Table(name = "BoardLike", indexes = @Index(name = "boardLike_index", columnList = "memberId, boardId")) -public class BoardLike { +public class BoardLike extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,8 +31,11 @@ public class BoardLike { private Long boardId; - public BoardLike(String memberId, Long boardId) { - this.memberId = memberId; - this.boardId = boardId; + public static BoardLike create(String memberId, Long boardId) { + return BoardLike.builder() + .memberId(memberId) + .boardId(boardId) + .build(); } + } diff --git a/src/main/java/page/clab/api/domain/board/dto/request/BoardLikeRequestDto.java b/src/main/java/page/clab/api/domain/board/dto/request/BoardLikeRequestDto.java index a0eac70b7..160658912 100644 --- a/src/main/java/page/clab/api/domain/board/dto/request/BoardLikeRequestDto.java +++ b/src/main/java/page/clab/api/domain/board/dto/request/BoardLikeRequestDto.java @@ -2,17 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BoardLikeRequestDto { @NotNull(message = "{notNull.boardLike.boardId}") diff --git a/src/main/java/page/clab/api/domain/board/dto/request/BoardRequestDto.java b/src/main/java/page/clab/api/domain/board/dto/request/BoardRequestDto.java index 7f654e8e6..eb011dee7 100644 --- a/src/main/java/page/clab/api/domain/board/dto/request/BoardRequestDto.java +++ b/src/main/java/page/clab/api/domain/board/dto/request/BoardRequestDto.java @@ -1,51 +1,55 @@ package page.clab.api.domain.board.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.board.domain.Board; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.board.domain.BoardCategory; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.util.RandomNicknameUtil; import java.util.List; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BoardRequestDto { @NotNull(message = "{notNull.board.category}") - @Size(min = 1, max = 50, message = "{size.board.category}") - @Schema(description = "카테고리", example = "공지사항", required = true) - private String category; + @Schema(description = "카테고리", example = "NOTICE", required = true) + private BoardCategory category; @NotNull(message = "{notNull.board.title}") - @Size(min = 1, max = 100, message = "{size.board.title}") @Schema(description = "제목", example = "2023년 2학기 모집 안내", required = true) private String title; @NotNull(message = "{notNull.board.content}") - @Size(min = 1, max = 10000, message = "{size.board.content}") @Schema(description = "내용", example = "2023년 2학기 모집 안내", required = true) private String content; @Schema(description = "첨부파일 경로 리스트", example = "[\"/resources/files/boards/339609571877700_4305d83e-090a-480b-a470-b5e96164d113.png\", \"/resources/files/boards/4305d83e-090a-480b-a470-b5e96164d114.png\"]") private List fileUrlList; + @Schema(description = "썸네일 이미지 URL", example = "/resources/files/boards/339609571877700_4305d83e-090a-480b-a470-b5e96164d113.png") + private String imageUrl; + @NotNull(message = "{notNull.board.wantAnonymous}") - @Column(name = "want_anonymous", nullable = false) @Schema(description = "익명 사용 여부", example = "false", required = true) private boolean wantAnonymous; - public static BoardRequestDto of(Board board) { - return ModelMapperUtil.getModelMapper().map(board, BoardRequestDto.class); + public static Board toEntity(BoardRequestDto requestDto, Member member, List uploadedFiles) { + return Board.builder() + .member(member) + .nickname(RandomNicknameUtil.makeRandomNickname()) + .category(requestDto.getCategory()) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .uploadedFiles(uploadedFiles) + .imageUrl(requestDto.getImageUrl()) + .wantAnonymous(requestDto.isWantAnonymous()) + .likes(0L) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/board/dto/request/BoardUpdateRequestDto.java b/src/main/java/page/clab/api/domain/board/dto/request/BoardUpdateRequestDto.java index 92e05a840..feb037fbb 100644 --- a/src/main/java/page/clab/api/domain/board/dto/request/BoardUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/board/dto/request/BoardUpdateRequestDto.java @@ -1,41 +1,29 @@ package page.clab.api.domain.board.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; +import jakarta.validation.constraints.NotNull; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import page.clab.api.domain.board.domain.Board; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.board.domain.BoardCategory; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BoardUpdateRequestDto { - @Size(min = 1, max = 50, message = "{size.board.category}") - @Schema(description = "카테고리", example = "공지사항") - private String category; + @Schema(description = "카테고리", example = "NOTICE") + private BoardCategory category; - @Size(min = 1, max = 100, message = "{size.board.title}") @Schema(description = "제목", example = "2023년 2학기 모집 안내") private String title; - @Size(min = 1, max = 10000, message = "{size.board.content}") @Schema(description = "내용", example = "2023년 2학기 모집 안내") private String content; - @Column(name = "want_anonymous", nullable = false) + @Schema(description = "썸네일 이미지 URL", example = "/resources/files/boards/339609571877700_4305d83e-090a-480b-a470-b5e96164d113.png") + private String imageUrl; + + @NotNull(message = "{notNull.board.wantAnonymous}") @Schema(description = "익명 사용 여부", example = "false") private boolean wantAnonymous; - public static BoardUpdateRequestDto of(Board board) { - return ModelMapperUtil.getModelMapper().map(board, BoardUpdateRequestDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/board/dto/response/BoardCategoryResponseDto.java b/src/main/java/page/clab/api/domain/board/dto/response/BoardCategoryResponseDto.java index aa103ae6a..a040476f4 100644 --- a/src/main/java/page/clab/api/domain/board/dto/response/BoardCategoryResponseDto.java +++ b/src/main/java/page/clab/api/domain/board/dto/response/BoardCategoryResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.board.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.board.domain.Board; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.board.domain.BoardCategory; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class BoardCategoryResponseDto { @@ -21,23 +15,26 @@ public class BoardCategoryResponseDto { private String category; - private String writer; + private String writerId; - private String title; + private String writerName; - private LocalDateTime createdAt; + private String title; - public static BoardCategoryResponseDto of(Board board) { - BoardCategoryResponseDto boardCategoryResponseDto = ModelMapperUtil.getModelMapper().map(board, BoardCategoryResponseDto.class); + private String imageUrl; - if(board.isWantAnonymous()){ - boardCategoryResponseDto.setWriter(board.getNickName()); - } - else{ - boardCategoryResponseDto.setWriter(board.getMember().getName()); - } + private LocalDateTime createdAt; - return boardCategoryResponseDto; + public static BoardCategoryResponseDto toDto(Board board) { + return BoardCategoryResponseDto.builder() + .id(board.getId()) + .category(board.getCategory().getKey()) + .writerId(board.isWantAnonymous() ? null : board.getMember().getId()) + .writerName(board.isWantAnonymous() ? board.getNickname() : board.getMember().getName()) + .title(board.getTitle()) + .imageUrl(board.getImageUrl()) + .createdAt(board.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/board/dto/response/BoardDetailsResponseDto.java b/src/main/java/page/clab/api/domain/board/dto/response/BoardDetailsResponseDto.java index d18258422..002dc86c9 100644 --- a/src/main/java/page/clab/api/domain/board/dto/response/BoardDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/board/dto/response/BoardDetailsResponseDto.java @@ -1,39 +1,35 @@ package page.clab.api.domain.board.dto.response; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.board.domain.Board; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class BoardDetailsResponseDto { private Long id; - private String writer; + private String writerId; + + private String writerName; private Long writerRoleLevel; - private String memberImageUrl; + private String writerImageUrl; private String title; private String content; - private List files = new ArrayList<>(); + private List files; + + private String imageUrl; private Long likes; @@ -44,18 +40,22 @@ public class BoardDetailsResponseDto { private LocalDateTime createdAt; - public static BoardDetailsResponseDto create(Board board, boolean hasLikeByMe, boolean isOwner) { - BoardDetailsResponseDto boardResponseDto = ModelMapperUtil.getModelMapper().map(board, BoardDetailsResponseDto.class); - if (board.isWantAnonymous()) { - boardResponseDto.setWriter(board.getNickName()); - boardResponseDto.setMemberImageUrl(null); - } else { - boardResponseDto.setWriter(board.getMember().getName()); - boardResponseDto.setMemberImageUrl(board.getMember().getImageUrl()); - } - boardResponseDto.setHasLikeByMe(hasLikeByMe); - boardResponseDto.setIsOwner(isOwner); - return boardResponseDto; + public static BoardDetailsResponseDto toDto(Board board, boolean hasLikeByMe, boolean isOwner) { + return BoardDetailsResponseDto.builder() + .id(board.getId()) + .writerId(board.isWantAnonymous() ? null : board.getMember().getId()) + .writerName(board.isWantAnonymous() ? board.getNickname() : board.getMember().getName()) + .writerRoleLevel(board.isWantAnonymous() ? null : board.getMember().getRole().toRoleLevel()) + .writerImageUrl(board.isWantAnonymous() ? null : board.getMember().getImageUrl()) + .title(board.getTitle()) + .content(board.getContent()) + .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) + .imageUrl(board.getImageUrl()) + .likes(board.getLikes()) + .hasLikeByMe(hasLikeByMe) + .isOwner(isOwner) + .createdAt(board.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/board/dto/response/BoardListResponseDto.java b/src/main/java/page/clab/api/domain/board/dto/response/BoardListResponseDto.java index 95eec61ce..820f24cc0 100644 --- a/src/main/java/page/clab/api/domain/board/dto/response/BoardListResponseDto.java +++ b/src/main/java/page/clab/api/domain/board/dto/response/BoardListResponseDto.java @@ -1,29 +1,46 @@ package page.clab.api.domain.board.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.board.domain.Board; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.board.domain.BoardCategory; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class BoardListResponseDto { private Long id; + private String writerId; + + private String writerName; + private String category; private String title; - public static BoardListResponseDto of(Board board) { - BoardListResponseDto boardResponseDto = ModelMapperUtil.getModelMapper().map(board, BoardListResponseDto.class); - return boardResponseDto; + private String content; + + private Long commentCount; + + private String imageUrl; + + private LocalDateTime createdAt; + + public static BoardListResponseDto toDto(Board board, Long commentCount) { + return BoardListResponseDto.builder() + .id(board.getId()) + .writerId(board.isWantAnonymous() ? null : board.getMember().getId()) + .writerName(board.isWantAnonymous() ? board.getNickname() : board.getMember().getName()) + .category(board.getCategory().getKey()) + .title(board.getTitle()) + .content(board.getContent()) + .commentCount(commentCount) + .imageUrl(board.getImageUrl()) + .createdAt(board.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/board/dto/response/BoardMyResponseDto.java b/src/main/java/page/clab/api/domain/board/dto/response/BoardMyResponseDto.java new file mode 100644 index 000000000..4842a1a6a --- /dev/null +++ b/src/main/java/page/clab/api/domain/board/dto/response/BoardMyResponseDto.java @@ -0,0 +1,36 @@ +package page.clab.api.domain.board.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.board.domain.Board; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class BoardMyResponseDto { + + private Long id; + + private String category; + + private String writerName; + + private String title; + + private String imageUrl; + + private LocalDateTime createdAt; + + public static BoardMyResponseDto toDto(Board board) { + return BoardMyResponseDto.builder() + .id(board.getId()) + .category(board.getCategory().getKey()) + .writerName(board.isWantAnonymous() ? board.getNickname() : board.getMember().getName()) + .title(board.getTitle()) + .imageUrl(board.getImageUrl()) + .createdAt(board.getCreatedAt()) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/book/api/BookController.java b/src/main/java/page/clab/api/domain/book/api/BookController.java index 82c152076..2279b3c66 100644 --- a/src/main/java/page/clab/api/domain/book/api/BookController.java +++ b/src/main/java/page/clab/api/domain/book/api/BookController.java @@ -20,12 +20,13 @@ import page.clab.api.domain.book.application.BookService; import page.clab.api.domain.book.dto.request.BookRequestDto; import page.clab.api.domain.book.dto.request.BookUpdateRequestDto; +import page.clab.api.domain.book.dto.response.BookDetailsResponseDto; import page.clab.api.domain.book.dto.response.BookResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; @RestController -@RequestMapping("/books") +@RequestMapping("/api/v1/books") @RequiredArgsConstructor @Tag(name = "Book", description = "도서") @Slf4j @@ -36,13 +37,11 @@ public class BookController { @Operation(summary = "[A] 도서 등록", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createBook( - @Valid @RequestBody BookRequestDto bookRequestDto + public ApiResponse createBook( + @Valid @RequestBody BookRequestDto requestDto ) { - Long id = bookService.createBook(bookRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = bookService.createBook(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 도서 목록 조회(제목, 카테고리, 출판사, 대여자 ID, 대여자 이름 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -50,7 +49,7 @@ public ResponseModel createBook( "제목, 카테고리, 출판사, 대여자 ID, 대여자 이름 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getBooksByConditions( + public ApiResponse> getBooksByConditions( @RequestParam(name = "title", required = false) String title, @RequestParam(name = "category", required = false) String category, @RequestParam(name = "publisher", required = false) String publisher, @@ -61,46 +60,50 @@ public ResponseModel getBooksByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto books = bookService.getBooksByConditions(title, category, publisher, borrowerId, borrowerName, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(books); - return responseModel; + return ApiResponse.success(books); } @Operation(summary = "[U] 도서 상세 정보", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/{bookId}") - public ResponseModel getBook( + public ApiResponse getBook( @PathVariable(name = "bookId") Long bookId ) { - BookResponseDto book = bookService.getBookDetails(bookId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(book); - return responseModel; + BookDetailsResponseDto book = bookService.getBookDetails(bookId); + return ApiResponse.success(book); } @Operation(summary = "[A] 도서 정보 수정", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("") - public ResponseModel updateBookInfo( + public ApiResponse updateBookInfo( @RequestParam(name = "bookId") Long bookId, - @Valid @RequestBody BookUpdateRequestDto bookUpdateRequestDto + @Valid @RequestBody BookUpdateRequestDto requestDto ) { - Long id = bookService.updateBookInfo(bookId, bookUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = bookService.updateBookInfo(bookId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 도서 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{bookId}") - public ResponseModel deleteBook( + public ApiResponse deleteBook( @PathVariable(name = "bookId") Long bookId ) { Long id = bookService.deleteBook(bookId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 도서 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedBooks( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto books = bookService.getDeletedBooks(pageable); + return ApiResponse.success(books); } } diff --git a/src/main/java/page/clab/api/domain/book/api/BookLoanRecordController.java b/src/main/java/page/clab/api/domain/book/api/BookLoanRecordController.java index 138239929..6c4c4993f 100644 --- a/src/main/java/page/clab/api/domain/book/api/BookLoanRecordController.java +++ b/src/main/java/page/clab/api/domain/book/api/BookLoanRecordController.java @@ -9,20 +9,24 @@ import org.springframework.data.domain.Pageable; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.GetMapping; +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.RestController; import page.clab.api.domain.book.application.BookLoanRecordService; +import page.clab.api.domain.book.domain.BookLoanStatus; import page.clab.api.domain.book.dto.request.BookLoanRecordRequestDto; +import page.clab.api.domain.book.dto.response.BookLoanRecordOverdueResponseDto; import page.clab.api.domain.book.dto.response.BookLoanRecordResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.exception.CustomOptimisticLockingFailureException; @RestController -@RequestMapping("/book-loan-records") +@RequestMapping("/api/v1/book-loan-records") @RequiredArgsConstructor @Tag(name = "BookLoanRecord", description = "도서 대출") @Slf4j @@ -30,59 +34,83 @@ public class BookLoanRecordController { private final BookLoanRecordService bookLoanRecordService; - @Operation(summary = "[U] 도서 대출", description = "ROLE_USER 이상의 권한이 필요함") + @Operation(summary = "[U] 도서 대출 요청", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel borrowBook( - @Valid @RequestBody BookLoanRecordRequestDto bookLoanRecordRequestDto + public ApiResponse requestBookLoan( + @Valid @RequestBody BookLoanRecordRequestDto requestDto ) throws CustomOptimisticLockingFailureException { - Long id = bookLoanRecordService.borrowBook(bookLoanRecordRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = bookLoanRecordService.requestBookLoan(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 도서 반납", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/return") - public ResponseModel returnBook( - @Valid @RequestBody BookLoanRecordRequestDto bookLoanRecordRequestDto + public ApiResponse returnBook( + @Valid @RequestBody BookLoanRecordRequestDto requestDto ) { - Long id = bookLoanRecordService.returnBook(bookLoanRecordRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = bookLoanRecordService.returnBook(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 도서 대출 연장", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/extend") - public ResponseModel extendBookLoan( - @Valid @RequestBody BookLoanRecordRequestDto bookLoanRecordRequestDto + public ApiResponse extendBookLoan( + @Valid @RequestBody BookLoanRecordRequestDto requestDto ) { - Long id = bookLoanRecordService.extendBookLoan(bookLoanRecordRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = bookLoanRecordService.extendBookLoan(requestDto); + return ApiResponse.success(id); } - @Operation(summary = "[U] 도서 대출 내역 조회(도서 ID, 대출자 ID, 대출 가능 여부 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + + @Operation(summary = "[A] 도서 대출 승인", description = "ROLE_ADMIN 이상의 권한이 필요함") + @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) + @PatchMapping("/approve/{bookLoanRecordId}") + public ApiResponse approveBookLoan( + @PathVariable(name = "bookLoanRecordId") Long bookLoanRecordId + ) { + Long id = bookLoanRecordService.approveBookLoan(bookLoanRecordId); + return ApiResponse.success(id); + } + + @Operation(summary = "[A] 도서 대출 거절", description = "ROLE_ADMIN 이상의 권한이 필요함") + @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) + @PatchMapping("/reject/{bookLoanRecordId}") + public ApiResponse rejectBookLoan( + @PathVariable(name = "bookLoanRecordId") Long bookLoanRecordId + ) { + Long id = bookLoanRecordService.rejectBookLoan(bookLoanRecordId); + return ApiResponse.success(id); + } + + @Operation(summary = "[U] 도서 대출 내역 조회(도서 ID, 대출자 ID, 대출 상태 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + "3개의 파라미터를 자유롭게 조합하여 필터링 가능
" + "도서 ID, 대출자 ID, 대출 가능 여부 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/conditions") - public ResponseModel getBookLoanRecordsByConditions( + public ApiResponse> getBookLoanRecordsByConditions( @RequestParam(name = "bookId", required = false) Long bookId, @RequestParam(name = "borrowerId", required = false) String borrowerId, - @RequestParam(name = "isReturned", required = false) Boolean isReturned, + @RequestParam(name = "status", required = false) BookLoanStatus status, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto bookLoanRecords = bookLoanRecordService.getBookLoanRecordsByConditions(bookId, borrowerId, status, pageable); + return ApiResponse.success(bookLoanRecords); + } + + @Operation(summary = "[A] 도서 연체자 조회", description = "ROLE_ADMIN 이상의 권한이 필요함") + @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) + @GetMapping("/overdue") + public ApiResponse> getOverdueBookLoanRecords( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto bookLoanRecords = bookLoanRecordService.getBookLoanRecordsByConditions(bookId, borrowerId, isReturned, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(bookLoanRecords); - return responseModel; + PagedResponseDto overdueRecords = bookLoanRecordService.getOverdueBookLoanRecords(pageable); + return ApiResponse.success(overdueRecords); } } diff --git a/src/main/java/page/clab/api/domain/book/application/BookLoanRecordService.java b/src/main/java/page/clab/api/domain/book/application/BookLoanRecordService.java index a0ae582b9..e4a62d7fa 100644 --- a/src/main/java/page/clab/api/domain/book/application/BookLoanRecordService.java +++ b/src/main/java/page/clab/api/domain/book/application/BookLoanRecordService.java @@ -1,23 +1,28 @@ package page.clab.api.domain.book.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.book.dao.BookLoanRecordRepository; import page.clab.api.domain.book.dao.BookRepository; import page.clab.api.domain.book.domain.Book; import page.clab.api.domain.book.domain.BookLoanRecord; +import page.clab.api.domain.book.domain.BookLoanStatus; import page.clab.api.domain.book.dto.request.BookLoanRecordRequestDto; +import page.clab.api.domain.book.dto.response.BookLoanRecordOverdueResponseDto; import page.clab.api.domain.book.dto.response.BookLoanRecordResponseDto; +import page.clab.api.domain.book.exception.BookAlreadyAppliedForLoanException; +import page.clab.api.domain.book.exception.MaxBorrowLimitExceededException; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.CustomOptimisticLockingFailureException; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -27,74 +32,121 @@ public class BookLoanRecordService { private final MemberService memberService; - private final BookRepository bookRepository; - private final NotificationService notificationService; + private final ValidationService validationService; + + private final BookRepository bookRepository; + private final BookLoanRecordRepository bookLoanRecordRepository; @Transactional - public Long borrowBook(BookLoanRecordRequestDto dto) throws CustomOptimisticLockingFailureException { + public Long requestBookLoan(BookLoanRecordRequestDto requestDto) throws CustomOptimisticLockingFailureException { try { Member borrower = memberService.getCurrentMember(); borrower.checkLoanSuspension(); - Book book = bookService.getBookByIdOrThrow(dto.getBookId()); - book.borrowTo(borrower); - bookRepository.save(book); + validateBorrowLimit(borrower); + + Book book = bookService.getBookByIdOrThrow(requestDto.getBookId()); + checkIfLoanAlreadyApplied(book, borrower); BookLoanRecord bookLoanRecord = BookLoanRecord.create(book, borrower); - notificationService.sendNotificationToMember( - borrower.getId(), - "[" + book.getTitle() + "] 도서 대출이 완료되었습니다." - ); + validationService.checkValid(bookLoanRecord); + + notificationService.sendNotificationToMember(borrower.getId(), "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다."); return bookLoanRecordRepository.save(bookLoanRecord).getId(); } catch (ObjectOptimisticLockingFailureException e) { - throw new CustomOptimisticLockingFailureException("도서 대출에 실패했습니다. 다시 시도해주세요."); + throw new CustomOptimisticLockingFailureException("도서 대출 신청에 실패했습니다. 다시 시도해주세요."); } } @Transactional - public Long returnBook(BookLoanRecordRequestDto dto) { + public Long returnBook(BookLoanRecordRequestDto requestDto) { Member currentMember = memberService.getCurrentMember(); - Book book = bookService.getBookByIdOrThrow(dto.getBookId()); + Book book = bookService.getBookByIdOrThrow(requestDto.getBookId()); book.returnBook(currentMember); bookRepository.save(book); BookLoanRecord bookLoanRecord = getBookLoanRecordByBookAndReturnedAtIsNullOrThrow(book); bookLoanRecord.markAsReturned(); + validationService.checkValid(bookLoanRecord); - notificationService.sendNotificationToMember( - currentMember.getId(), - "[" + book.getTitle() + "] 도서 반납이 완료되었습니다." - ); + notificationService.sendNotificationToMember(currentMember.getId(), "[" + book.getTitle() + "] 도서 반납이 완료되었습니다."); return bookLoanRecordRepository.save(bookLoanRecord).getId(); } @Transactional - public Long extendBookLoan(BookLoanRecordRequestDto dto) { + public Long extendBookLoan(BookLoanRecordRequestDto requestDto) { Member currentMember = memberService.getCurrentMember(); - Book book = bookService.getBookByIdOrThrow(dto.getBookId()); + Book book = bookService.getBookByIdOrThrow(requestDto.getBookId()); book.validateCurrentBorrower(currentMember); BookLoanRecord bookLoanRecord = getBookLoanRecordByBookAndReturnedAtIsNullOrThrow(book); bookLoanRecord.extendLoan(); + validationService.checkValid(bookLoanRecord); + + notificationService.sendNotificationToMember(currentMember.getId(), "[" + book.getTitle() + "] 도서 대출 연장이 완료되었습니다."); + + return bookLoanRecordRepository.save(bookLoanRecord).getId(); + } + + @Transactional + public Long approveBookLoan(Long bookLoanRecordId) { + Member currentMember = memberService.getCurrentMember(); + BookLoanRecord bookLoanRecord = getBookLoanRecordByIdOrThrow(bookLoanRecordId); + Book book = bookService.getBookByIdOrThrow(bookLoanRecord.getBook().getId()); + + book.validateBookIsNotBorrowed(); + validateBorrowLimit(currentMember); + bookLoanRecord.approve(); - notificationService.sendNotificationToMember( - currentMember.getId(), - "[" + book.getTitle() + "] 도서 대출 연장이 완료되었습니다." - ); + validationService.checkValid(bookLoanRecord); return bookLoanRecordRepository.save(bookLoanRecord).getId(); } - public PagedResponseDto getBookLoanRecordsByConditions(Long bookId, String borrowerId, Boolean isReturned, Pageable pageable) { - Page bookLoanRecords = bookLoanRecordRepository.findByConditions(bookId, borrowerId, isReturned, pageable); + @Transactional + public Long rejectBookLoan(Long bookLoanRecordId) { + BookLoanRecord bookLoanRecord = getBookLoanRecordByIdOrThrow(bookLoanRecordId); + bookLoanRecord.reject(); + validationService.checkValid(bookLoanRecord); + return bookLoanRecordRepository.save(bookLoanRecord).getId(); + } + + public BookLoanRecord getBookLoanRecordByIdOrThrow(Long bookLoanRecordId) { + return bookLoanRecordRepository.findById(bookLoanRecordId) + .orElseThrow(() -> new NotFoundException("해당 도서 대출 기록이 없습니다.")); + } + + @Transactional(readOnly = true) + public PagedResponseDto getBookLoanRecordsByConditions(Long bookId, String borrowerId, BookLoanStatus status, Pageable pageable) { + Page bookLoanRecords = bookLoanRecordRepository.findByConditions(bookId, borrowerId, status, pageable); return new PagedResponseDto<>(bookLoanRecords); } + public PagedResponseDto getOverdueBookLoanRecords(Pageable pageable) { + Page overdueBookLoanRecords = bookLoanRecordRepository.findOverdueBookLoanRecords(pageable); + return new PagedResponseDto<>(overdueBookLoanRecords); + } + public BookLoanRecord getBookLoanRecordByBookAndReturnedAtIsNullOrThrow(Book book) { return bookLoanRecordRepository.findByBookAndReturnedAtIsNull(book) .orElseThrow(() -> new NotFoundException("해당 도서 대출 기록이 없습니다.")); } + private void validateBorrowLimit(Member borrower) { + int borrowedBookCount = bookService.getNumberOfBooksBorrowedByMember(borrower); + int maxBorrowableBookCount = 3; + if (borrowedBookCount >= maxBorrowableBookCount) { + throw new MaxBorrowLimitExceededException("대출 가능한 도서의 수를 초과했습니다."); + } + } + + private void checkIfLoanAlreadyApplied(Book book, Member borrower) { + bookLoanRecordRepository.findByBookAndBorrowerAndStatus(book, borrower, BookLoanStatus.PENDING) + .ifPresent(bookLoanRecord -> { + throw new BookAlreadyAppliedForLoanException("이미 대출 신청한 도서입니다."); + }); + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/book/application/BookService.java b/src/main/java/page/clab/api/domain/book/application/BookService.java index ff279cad3..224104e2c 100644 --- a/src/main/java/page/clab/api/domain/book/application/BookService.java +++ b/src/main/java/page/clab/api/domain/book/application/BookService.java @@ -2,20 +2,23 @@ import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.book.dao.BookLoanRecordRepository; import page.clab.api.domain.book.dao.BookRepository; import page.clab.api.domain.book.domain.Book; import page.clab.api.domain.book.domain.BookLoanRecord; import page.clab.api.domain.book.dto.request.BookRequestDto; import page.clab.api.domain.book.dto.request.BookUpdateRequestDto; +import page.clab.api.domain.book.dto.response.BookDetailsResponseDto; import page.clab.api.domain.book.dto.response.BookResponseDto; +import page.clab.api.domain.member.domain.Member; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import java.time.LocalDateTime; -import java.util.List; @Service @RequiredArgsConstructor @@ -25,21 +28,31 @@ public class BookService { private final BookLoanRecordRepository bookLoanRecordRepository; - public Long createBook(BookRequestDto bookRequestDto) { - Book book = Book.of(bookRequestDto); + @Transactional + public Long createBook(BookRequestDto requestDto) { + Book book = BookRequestDto.toEntity(requestDto); return bookRepository.save(book).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getBooksByConditions(String title, String category, String publisher, String borrowerId, String borrowerName, Pageable pageable) { - List books = bookRepository.findByConditions(title, category, publisher, borrowerId, borrowerName); - return getBookResponseDtoPagedResponseDto(books, pageable); + Page books = bookRepository.findByConditions(title, category, publisher, borrowerId, borrowerName, pageable); + return new PagedResponseDto<>(books.map(this::mapToBookResponseDto)); } - public BookResponseDto getBookDetails(Long bookId) { + @Transactional(readOnly = true) + public BookDetailsResponseDto getBookDetails(Long bookId) { Book book = getBookByIdOrThrow(bookId); - return mapToBookResponseDto(book); + return mapToBookDetailsResponseDto(book); } + @Transactional(readOnly = true) + public PagedResponseDto getDeletedBooks(Pageable pageable) { + Page books = bookRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(books.map(this::mapToBookDetailsResponseDto)); + } + + @Transactional public Long updateBookInfo(Long bookId, BookUpdateRequestDto bookUpdateRequestDto) { Book book = getBookByIdOrThrow(bookId); book.update(bookUpdateRequestDto); @@ -48,7 +61,8 @@ public Long updateBookInfo(Long bookId, BookUpdateRequestDto bookUpdateRequestDt public Long deleteBook(Long bookId) { Book book = getBookByIdOrThrow(bookId); - bookRepository.delete(book); + book.updateIsDeleted(true); + bookRepository.save(book); return book.getId(); } @@ -62,19 +76,25 @@ public BookLoanRecord getBookLoanRecordByBookAndReturnedAtIsNull(Book book) { .orElse(null); } - @NotNull - private PagedResponseDto getBookResponseDtoPagedResponseDto(List books, Pageable pageable) { - List bookResponseDtos = books.stream() - .map(this::mapToBookResponseDto) - .toList(); - return new PagedResponseDto<>(bookResponseDtos, pageable, books.size()); + public int getNumberOfBooksBorrowedByMember(Member member) { + return bookRepository.countByBorrower(member); + } + + private LocalDateTime getDueDateForBook(Book book) { + BookLoanRecord bookLoanRecord = getBookLoanRecordByBookAndReturnedAtIsNull(book); + return bookLoanRecord != null ? bookLoanRecord.getDueDate() : null; } @NotNull private BookResponseDto mapToBookResponseDto(Book book) { - BookLoanRecord bookLoanRecord = getBookLoanRecordByBookAndReturnedAtIsNull(book); - LocalDateTime dueDate = bookLoanRecord != null ? bookLoanRecord.getDueDate() : null; - return BookResponseDto.of(book, dueDate); + LocalDateTime dueDate = getDueDateForBook(book); + return BookResponseDto.toDto(book, dueDate); + } + + @NotNull + private BookDetailsResponseDto mapToBookDetailsResponseDto(Book book) { + LocalDateTime dueDate = getDueDateForBook(book); + return BookDetailsResponseDto.toDto(book, dueDate); } } diff --git a/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepository.java b/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepository.java index d2f1180c5..a9ccc3fcc 100644 --- a/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepository.java +++ b/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepository.java @@ -3,6 +3,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import page.clab.api.domain.book.domain.Book; import page.clab.api.domain.book.domain.BookLoanRecord; +import page.clab.api.domain.book.domain.BookLoanStatus; +import page.clab.api.domain.member.domain.Member; import java.util.Optional; @@ -10,4 +12,6 @@ public interface BookLoanRecordRepository extends JpaRepository findByBookAndReturnedAtIsNull(Book book); + Optional findByBookAndBorrowerAndStatus(Book book, Member borrower, BookLoanStatus bookLoanStatus); + } diff --git a/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryCustom.java b/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryCustom.java index 1d9477798..77a33c016 100644 --- a/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryCustom.java +++ b/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryCustom.java @@ -2,10 +2,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import page.clab.api.domain.book.domain.BookLoanStatus; +import page.clab.api.domain.book.dto.response.BookLoanRecordOverdueResponseDto; import page.clab.api.domain.book.dto.response.BookLoanRecordResponseDto; public interface BookLoanRecordRepositoryCustom { - Page findByConditions(Long bookId, String borrowerId, Boolean isReturned, Pageable pageable); + Page findByConditions(Long bookId, String borrowerId, BookLoanStatus status, Pageable pageable); + + Page findOverdueBookLoanRecords(Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryImpl.java b/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryImpl.java index 0fd769002..e09df3d35 100644 --- a/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/book/dao/BookLoanRecordRepositoryImpl.java @@ -8,9 +8,12 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import page.clab.api.domain.book.domain.BookLoanStatus; import page.clab.api.domain.book.domain.QBookLoanRecord; +import page.clab.api.domain.book.dto.response.BookLoanRecordOverdueResponseDto; import page.clab.api.domain.book.dto.response.BookLoanRecordResponseDto; +import java.time.LocalDateTime; import java.util.List; @Repository @@ -20,35 +23,34 @@ public class BookLoanRecordRepositoryImpl implements BookLoanRecordRepositoryCus private final JPAQueryFactory queryFactory; @Override - public Page findByConditions(Long bookId, String borrowerId, Boolean isReturned, Pageable pageable) { + public Page findByConditions(Long bookId, String borrowerId, BookLoanStatus status, Pageable pageable) { QBookLoanRecord bookLoanRecord = QBookLoanRecord.bookLoanRecord; BooleanBuilder builder = new BooleanBuilder(); if (bookId != null) builder.and(bookLoanRecord.book.id.eq(bookId)); if (borrowerId != null && !borrowerId.trim().isEmpty()) builder.and(bookLoanRecord.borrower.id.eq(borrowerId)); - if (isReturned != null) { - if (isReturned) { - builder.and(bookLoanRecord.returnedAt.isNotNull()); - } else { - builder.and(bookLoanRecord.returnedAt.isNull()); - } - } + if (status != null) builder.and(bookLoanRecord.status.eq(status)); List results = queryFactory .select(Projections.constructor( BookLoanRecordResponseDto.class, bookLoanRecord.id, + bookLoanRecord.book.id, + bookLoanRecord.book.title, + bookLoanRecord.book.imageUrl, bookLoanRecord.borrower.id, + bookLoanRecord.borrower.name, bookLoanRecord.borrowedAt, bookLoanRecord.returnedAt, bookLoanRecord.dueDate, - bookLoanRecord.loanExtensionCount + bookLoanRecord.loanExtensionCount, + bookLoanRecord.status )) .from(bookLoanRecord) .where(builder) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) - .orderBy(bookLoanRecord.borrowedAt.desc()) + .orderBy(bookLoanRecord.borrowedAt.desc(), bookLoanRecord.createdAt.asc()) .fetch(); long total = queryFactory @@ -59,4 +61,38 @@ public Page findByConditions(Long bookId, String borr return new PageImpl<>(results, pageable, total); } + @Override + public Page findOverdueBookLoanRecords(Pageable pageable) { + QBookLoanRecord bookLoanRecord = QBookLoanRecord.bookLoanRecord; + + LocalDateTime now = LocalDateTime.now(); + + List results = queryFactory + .select(Projections.constructor( + BookLoanRecordOverdueResponseDto.class, + bookLoanRecord.book.id, + bookLoanRecord.book.title, + bookLoanRecord.borrower.id, + bookLoanRecord.borrower.name, + bookLoanRecord.borrowedAt, + bookLoanRecord.dueDate, + bookLoanRecord.status + )) + .from(bookLoanRecord) + .where(bookLoanRecord.status.eq(BookLoanStatus.APPROVED) + .and(bookLoanRecord.dueDate.lt(now))) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(bookLoanRecord.dueDate.asc()) + .fetch(); + + long total = queryFactory + .selectFrom(bookLoanRecord) + .where(bookLoanRecord.status.eq(BookLoanStatus.APPROVED) + .and(bookLoanRecord.dueDate.lt(now))) + .fetchCount(); + + return new PageImpl<>(results, pageable, total); + } + } diff --git a/src/main/java/page/clab/api/domain/book/dao/BookRepository.java b/src/main/java/page/clab/api/domain/book/dao/BookRepository.java index 1d2ef52fe..2944b2ec6 100644 --- a/src/main/java/page/clab/api/domain/book/dao/BookRepository.java +++ b/src/main/java/page/clab/api/domain/book/dao/BookRepository.java @@ -1,8 +1,12 @@ package page.clab.api.domain.book.dao; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.book.domain.Book; +import page.clab.api.domain.member.domain.Member; import java.util.List; @@ -10,4 +14,9 @@ public interface BookRepository extends JpaRepository, BookRepositor List findAllByOrderByCreatedAtDesc(); + int countByBorrower(Member member); + + @Query(value = "SELECT b.* FROM book b WHERE b.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/book/dao/BookRepositoryCustom.java b/src/main/java/page/clab/api/domain/book/dao/BookRepositoryCustom.java index 57cd3ff66..2c6ae2527 100644 --- a/src/main/java/page/clab/api/domain/book/dao/BookRepositoryCustom.java +++ b/src/main/java/page/clab/api/domain/book/dao/BookRepositoryCustom.java @@ -1,11 +1,11 @@ package page.clab.api.domain.book.dao; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import page.clab.api.domain.book.domain.Book; -import java.util.List; - public interface BookRepositoryCustom { - List findByConditions(String title, String category, String publisher, String borrowerId, String borrowerName); + Page findByConditions(String title, String category, String publisher, String borrowerId, String borrowerName, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/book/dao/BookRepositoryImpl.java b/src/main/java/page/clab/api/domain/book/dao/BookRepositoryImpl.java index 4bdf94430..c650f3f5e 100644 --- a/src/main/java/page/clab/api/domain/book/dao/BookRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/book/dao/BookRepositoryImpl.java @@ -3,6 +3,9 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQueryFactory; 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 page.clab.api.domain.book.domain.Book; import page.clab.api.domain.book.domain.QBook; @@ -17,7 +20,7 @@ public class BookRepositoryImpl implements BookRepositoryCustom { private final JPAQueryFactory queryFactory; @Override - public List findByConditions(String title, String category, String publisher, String borrowerId, String borrowerName) { + public Page findByConditions(String title, String category, String publisher, String borrowerId, String borrowerName, Pageable pageable) { QBook book = QBook.book; QMember borrower = QMember.member; BooleanBuilder builder = new BooleanBuilder(); @@ -28,11 +31,20 @@ public List findByConditions(String title, String category, String publish if (borrowerId != null) builder.and(borrower.id.eq(borrowerId)); if (borrowerName != null) builder.and(borrower.name.eq(borrowerName)); - return queryFactory.selectFrom(book) - .leftJoin(book.borrower, borrower).fetchJoin() + List books = queryFactory.selectFrom(book) + .leftJoin(book.borrower, borrower) .where(builder) .orderBy(book.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) .fetch(); + + long count = queryFactory.selectFrom(book) + .leftJoin(book.borrower, borrower) + .where(builder) + .fetchCount(); + + return new PageImpl<>(books, pageable, count); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/book/domain/Book.java b/src/main/java/page/clab/api/domain/book/domain/Book.java index 03731d7d4..03e451f53 100644 --- a/src/main/java/page/clab/api/domain/book/domain/Book.java +++ b/src/main/java/page/clab/api/domain/book/domain/Book.java @@ -1,6 +1,7 @@ package page.clab.api.domain.book.domain; import jakarta.persistence.Column; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -8,29 +9,31 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Version; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.book.dto.request.BookRequestDto; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.book.dto.request.BookUpdateRequestDto; import page.clab.api.domain.book.exception.BookAlreadyBorrowedException; import page.clab.api.domain.book.exception.InvalidBorrowerException; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; +import page.clab.api.global.util.StringJsonConverter; -import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Book { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLRestriction("is_deleted = false") +public class Book extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -50,9 +53,9 @@ public class Book { private String imageUrl; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; + @Column(columnDefinition = "TEXT") + @Convert(converter = StringJsonConverter.class) + private List reviewLinks; @ManyToOne @JoinColumn(name = "member_id") @@ -61,23 +64,23 @@ public class Book { @Version private Long version; - public static Book of(BookRequestDto bookRequestDto) { - return ModelMapperUtil.getModelMapper().map(bookRequestDto, Book.class); + public void update(BookUpdateRequestDto requestDto) { + Optional.ofNullable(requestDto.getCategory()).ifPresent(this::setCategory); + Optional.ofNullable(requestDto.getTitle()).ifPresent(this::setTitle); + Optional.ofNullable(requestDto.getAuthor()).ifPresent(this::setAuthor); + Optional.ofNullable(requestDto.getPublisher()).ifPresent(this::setPublisher); + Optional.ofNullable(requestDto.getImageUrl()).ifPresent(this::setImageUrl); + Optional.ofNullable(requestDto.getReviewLinks()).ifPresent(this::setReviewLinks); } - public void update(BookUpdateRequestDto bookUpdateRequestDto) { - Optional.ofNullable(bookUpdateRequestDto.getCategory()).ifPresent(this::setCategory); - Optional.ofNullable(bookUpdateRequestDto.getTitle()).ifPresent(this::setTitle); - Optional.ofNullable(bookUpdateRequestDto.getAuthor()).ifPresent(this::setAuthor); - Optional.ofNullable(bookUpdateRequestDto.getPublisher()).ifPresent(this::setPublisher); - Optional.ofNullable(bookUpdateRequestDto.getImageUrl()).ifPresent(this::setImageUrl); + public void updateIsDeleted(Boolean isDeleted) { + this.isDeleted = isDeleted; } - public void borrowTo(Member borrower) { + public void validateBookIsNotBorrowed() { if (this.borrower != null) { throw new BookAlreadyBorrowedException("이미 대출 중인 도서입니다."); } - this.borrower = borrower; } public void returnBook(Member currentMember) { diff --git a/src/main/java/page/clab/api/domain/book/domain/BookLoanRecord.java b/src/main/java/page/clab/api/domain/book/domain/BookLoanRecord.java index 3548e0a21..93aa70f45 100644 --- a/src/main/java/page/clab/api/domain/book/domain/BookLoanRecord.java +++ b/src/main/java/page/clab/api/domain/book/domain/BookLoanRecord.java @@ -1,20 +1,25 @@ package page.clab.api.domain.book.domain; -import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.book.exception.BookAlreadyReturnedException; +import page.clab.api.domain.book.exception.LoanNotPendingException; import page.clab.api.domain.book.exception.LoanSuspensionException; import page.clab.api.domain.book.exception.OverdueException; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -23,9 +28,9 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class BookLoanRecord { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class BookLoanRecord extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -39,7 +44,6 @@ public class BookLoanRecord { @JoinColumn(name = "member_id", nullable = false) private Member borrower; - @Column(updatable = false) private LocalDateTime borrowedAt; private LocalDateTime returnedAt; @@ -48,25 +52,28 @@ public class BookLoanRecord { private Long loanExtensionCount; + @Enumerated(EnumType.STRING) + private BookLoanStatus status; + public static BookLoanRecord create(Book book, Member borrower) { return BookLoanRecord.builder() .book(book) .borrower(borrower) - .borrowedAt(LocalDateTime.now()) - .dueDate(LocalDateTime.now().plusWeeks(1)) .loanExtensionCount(0L) + .status(BookLoanStatus.PENDING) .build(); } public void markAsReturned() { if (this.returnedAt != null) { - throw new IllegalStateException("이미 반납된 도서입니다."); + throw new BookAlreadyReturnedException("이미 반납된 도서입니다."); } this.returnedAt = LocalDateTime.now(); if (isOverdue(returnedAt)) { long overdueDays = ChronoUnit.DAYS.between(this.dueDate, this.returnedAt); this.borrower.handleOverdueAndSuspension(overdueDays); } + this.status = BookLoanStatus.RETURNED; } private boolean isOverdue(LocalDateTime returnedAt) { @@ -91,4 +98,21 @@ public void extendLoan() { this.loanExtensionCount += 1; } + public void approve() { + if (this.status != BookLoanStatus.PENDING) { + throw new LoanNotPendingException("대출 신청 상태가 아닙니다."); + } + this.book.setBorrower(this.borrower); + this.status = BookLoanStatus.APPROVED; + this.borrowedAt = LocalDateTime.now(); + this.dueDate = LocalDateTime.now().plusWeeks(1); + } + + public void reject() { + if (this.status != BookLoanStatus.PENDING) { + throw new LoanNotPendingException("대출 신청 상태가 아닙니다."); + } + this.status = BookLoanStatus.REJECTED; + } + } diff --git a/src/main/java/page/clab/api/domain/book/domain/BookLoanStatus.java b/src/main/java/page/clab/api/domain/book/domain/BookLoanStatus.java new file mode 100644 index 000000000..4c8b7e1f6 --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/domain/BookLoanStatus.java @@ -0,0 +1,18 @@ +package page.clab.api.domain.book.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BookLoanStatus { + + PENDING("PENDING", "대출 신청"), + APPROVED("APPROVED", "대출 승인"), + REJECTED("REJECTED", "대출 거절"), + RETURNED("RETURNED", "반납 완료"); + + private String key; + private String description; + +} diff --git a/src/main/java/page/clab/api/domain/book/dto/request/BookLoanRecordRequestDto.java b/src/main/java/page/clab/api/domain/book/dto/request/BookLoanRecordRequestDto.java index 25c8af941..c44dab2da 100644 --- a/src/main/java/page/clab/api/domain/book/dto/request/BookLoanRecordRequestDto.java +++ b/src/main/java/page/clab/api/domain/book/dto/request/BookLoanRecordRequestDto.java @@ -2,17 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BookLoanRecordRequestDto { @NotNull(message = "{notNull.bookLoanRecord.bookId}") diff --git a/src/main/java/page/clab/api/domain/book/dto/request/BookRequestDto.java b/src/main/java/page/clab/api/domain/book/dto/request/BookRequestDto.java index 40d39bbd6..9d17a5afe 100644 --- a/src/main/java/page/clab/api/domain/book/dto/request/BookRequestDto.java +++ b/src/main/java/page/clab/api/domain/book/dto/request/BookRequestDto.java @@ -2,17 +2,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.book.domain.Book; + +import java.util.List; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BookRequestDto { @NotNull(message = "{notNull.book.category}") @@ -34,4 +31,18 @@ public class BookRequestDto { @Schema(description = "이미지 URL", example = "https://shopping-phinf.pstatic.net/main_3243625/32436253723.20230928091945.jpg?type=w300") private String imageUrl; + @Schema(description = "리뷰 링크", example = "[\"https://www.yes24.com/Product/Goods/7516911\",\"https://www.aladin.co.kr/shop/wproduct.aspx?ISBN=8960773433&start=pnaver_02\"]") + private List reviewLinks; + + public static Book toEntity(BookRequestDto requestDto) { + return Book.builder() + .category(requestDto.getCategory()) + .title(requestDto.getTitle()) + .author(requestDto.getAuthor()) + .publisher(requestDto.getPublisher()) + .imageUrl(requestDto.getImageUrl()) + .reviewLinks(requestDto.reviewLinks) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/book/dto/request/BookUpdateRequestDto.java b/src/main/java/page/clab/api/domain/book/dto/request/BookUpdateRequestDto.java index de23d0de4..acd50ac58 100644 --- a/src/main/java/page/clab/api/domain/book/dto/request/BookUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/book/dto/request/BookUpdateRequestDto.java @@ -1,17 +1,13 @@ package page.clab.api.domain.book.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.List; + @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class BookUpdateRequestDto { @Schema(description = "카테고리", example = "IT/개발") @@ -29,4 +25,7 @@ public class BookUpdateRequestDto { @Schema(description = "이미지 URL", example = "https://shopping-phinf.pstatic.net/main_3243625/32436253723.20230928091945.jpg?type=w300") private String imageUrl; + @Schema(description = "리뷰 링크", example = "[\"https://www.yes24.com/Product/Goods/7516911\",\"https://www.aladin.co.kr/shop/wproduct.aspx?ISBN=8960773433&start=pnaver_02\"]") + private List reviewLinks; + } diff --git a/src/main/java/page/clab/api/domain/book/dto/response/BookDetailsResponseDto.java b/src/main/java/page/clab/api/domain/book/dto/response/BookDetailsResponseDto.java new file mode 100644 index 000000000..7e5c0c459 --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/dto/response/BookDetailsResponseDto.java @@ -0,0 +1,55 @@ +package page.clab.api.domain.book.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.book.domain.Book; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class BookDetailsResponseDto { + + private Long id; + + private String borrowerId; + + private String borrowerName; + + private String category; + + private String title; + + private String author; + + private String publisher; + + private String imageUrl; + + private List reviewLinks; + + private LocalDateTime dueDate; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + public static BookDetailsResponseDto toDto(Book book, LocalDateTime dueDate) { + return BookDetailsResponseDto.builder() + .id(book.getId()) + .borrowerId(book.getBorrower() == null ? null : book.getBorrower().getId()) + .borrowerName(book.getBorrower() == null ? null : book.getBorrower().getName()) + .category(book.getCategory()) + .title(book.getTitle()) + .author(book.getAuthor()) + .publisher(book.getPublisher()) + .imageUrl(book.getImageUrl()) + .reviewLinks(book.getReviewLinks()) + .dueDate(dueDate) + .createdAt(book.getCreatedAt()) + .updatedAt(book.getUpdatedAt()) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordOverdueResponseDto.java b/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordOverdueResponseDto.java new file mode 100644 index 000000000..90145797b --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordOverdueResponseDto.java @@ -0,0 +1,29 @@ +package page.clab.api.domain.book.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import page.clab.api.domain.book.domain.BookLoanStatus; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class BookLoanRecordOverdueResponseDto { + + private Long bookId; + + private String bookTitle; + + private String borrowerId; + + private String borrowerName; + + private LocalDateTime borrowedAt; + + private LocalDateTime dueDate; + + private BookLoanStatus status; + +} diff --git a/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordResponseDto.java b/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordResponseDto.java index d176cdbc6..a89cf9e95 100644 --- a/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordResponseDto.java +++ b/src/main/java/page/clab/api/domain/book/dto/response/BookLoanRecordResponseDto.java @@ -1,26 +1,29 @@ package page.clab.api.domain.book.dto.response; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import page.clab.api.domain.book.domain.BookLoanRecord; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.book.domain.BookLoanStatus; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor @NoArgsConstructor -@Builder +@AllArgsConstructor public class BookLoanRecordResponseDto { + private Long bookLoanRecordId; + private Long bookId; + private String bookTitle; + + private String bookImageUrl; + private String borrowerId; + private String borrowerName; + private LocalDateTime borrowedAt; private LocalDateTime returnedAt; @@ -29,12 +32,6 @@ public class BookLoanRecordResponseDto { private Long loanExtensionCount; - public static BookLoanRecordResponseDto of(BookLoanRecord bookLoanRecord) { - BookLoanRecordResponseDto bookLoanRecordResponseDto = ModelMapperUtil.getModelMapper() - .map(bookLoanRecord, BookLoanRecordResponseDto.class); - bookLoanRecordResponseDto.setBookId(bookLoanRecord.getBook().getId()); - bookLoanRecordResponseDto.setBorrowerId(bookLoanRecord.getBorrower().getId()); - return bookLoanRecordResponseDto; - } + private BookLoanStatus status; } diff --git a/src/main/java/page/clab/api/domain/book/dto/response/BookResponseDto.java b/src/main/java/page/clab/api/domain/book/dto/response/BookResponseDto.java index 74df14178..5920cf05e 100644 --- a/src/main/java/page/clab/api/domain/book/dto/response/BookResponseDto.java +++ b/src/main/java/page/clab/api/domain/book/dto/response/BookResponseDto.java @@ -1,19 +1,12 @@ package page.clab.api.domain.book.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.book.domain.Book; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class BookResponseDto { @@ -37,16 +30,22 @@ public class BookResponseDto { private LocalDateTime createdAt; - private LocalDateTime updateTime; - - public static BookResponseDto of(Book book, LocalDateTime dueDate) { - BookResponseDto bookResponseDto = ModelMapperUtil.getModelMapper().map(book, BookResponseDto.class); - if (book.getBorrower() != null) { - bookResponseDto.setBorrowerId(book.getBorrower().getId()); - bookResponseDto.setBorrowerName(book.getBorrower().getName()); - } - bookResponseDto.setDueDate(dueDate); - return bookResponseDto; + private LocalDateTime updatedAt; + + public static BookResponseDto toDto(Book book, LocalDateTime dueDate) { + return BookResponseDto.builder() + .id(book.getId()) + .borrowerId(book.getBorrower() == null ? null : book.getBorrower().getId()) + .borrowerName(book.getBorrower() == null ? null : book.getBorrower().getName()) + .category(book.getCategory()) + .title(book.getTitle()) + .author(book.getAuthor()) + .publisher(book.getPublisher()) + .imageUrl(book.getImageUrl()) + .dueDate(dueDate) + .createdAt(book.getCreatedAt()) + .updatedAt(book.getUpdatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/book/exception/BookAlreadyAppliedForLoanException.java b/src/main/java/page/clab/api/domain/book/exception/BookAlreadyAppliedForLoanException.java new file mode 100644 index 000000000..f530a265a --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/exception/BookAlreadyAppliedForLoanException.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.book.exception; + +public class BookAlreadyAppliedForLoanException extends RuntimeException { + + public BookAlreadyAppliedForLoanException(String message) { + super(message); + } + +} diff --git a/src/main/java/page/clab/api/domain/book/exception/BookAlreadyReturnedException.java b/src/main/java/page/clab/api/domain/book/exception/BookAlreadyReturnedException.java new file mode 100644 index 000000000..ab71fcefb --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/exception/BookAlreadyReturnedException.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.book.exception; + +public class BookAlreadyReturnedException extends RuntimeException { + + public BookAlreadyReturnedException(String message) { + super(message); + } + +} diff --git a/src/main/java/page/clab/api/domain/book/exception/LoanNotPendingException.java b/src/main/java/page/clab/api/domain/book/exception/LoanNotPendingException.java new file mode 100644 index 000000000..b3115adc3 --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/exception/LoanNotPendingException.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.book.exception; + +public class LoanNotPendingException extends RuntimeException { + + public LoanNotPendingException(String message) { + super(message); + } + +} diff --git a/src/main/java/page/clab/api/domain/book/exception/MaxBorrowLimitExceededException.java b/src/main/java/page/clab/api/domain/book/exception/MaxBorrowLimitExceededException.java new file mode 100644 index 000000000..e44ef23d4 --- /dev/null +++ b/src/main/java/page/clab/api/domain/book/exception/MaxBorrowLimitExceededException.java @@ -0,0 +1,9 @@ +package page.clab.api.domain.book.exception; + +public class MaxBorrowLimitExceededException extends RuntimeException { + + public MaxBorrowLimitExceededException(String message) { + super(message); + } + +} diff --git a/src/main/java/page/clab/api/domain/comment/api/CommentController.java b/src/main/java/page/clab/api/domain/comment/api/CommentController.java index b44de6ee9..6075f765c 100644 --- a/src/main/java/page/clab/api/domain/comment/api/CommentController.java +++ b/src/main/java/page/clab/api/domain/comment/api/CommentController.java @@ -20,14 +20,15 @@ import page.clab.api.domain.comment.application.CommentService; import page.clab.api.domain.comment.dto.request.CommentRequestDto; import page.clab.api.domain.comment.dto.request.CommentUpdateRequestDto; -import page.clab.api.domain.comment.dto.response.CommentGetAllResponseDto; -import page.clab.api.domain.comment.dto.response.CommentGetMyResponseDto; +import page.clab.api.domain.comment.dto.response.CommentMyResponseDto; +import page.clab.api.domain.comment.dto.response.CommentResponseDto; +import page.clab.api.domain.comment.dto.response.DeletedCommentResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/comments") +@RequestMapping("/api/v1/comments") @RequiredArgsConstructor @Tag(name = "Comment", description = "댓글") @Slf4j @@ -38,81 +39,82 @@ public class CommentController { @Operation(summary = "[U] 댓글 생성", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/{boardId}") - public ResponseModel createComment( + public ApiResponse createComment( @RequestParam(name = "parentId", required = false) Long parentId, @PathVariable(name = "boardId") Long boardId, - @Valid @RequestBody CommentRequestDto commentRequestDto + @Valid @RequestBody CommentRequestDto requestDto ) { - Long id = commentService.createComment(parentId, boardId, commentRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = commentService.createComment(parentId, boardId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 댓글 목록 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/{boardId}") - public ResponseModel getComments( + public ApiResponse> getComments( @PathVariable(name = "boardId") Long boardId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto comments = commentService.getAllComments(boardId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(comments); - return responseModel; + PagedResponseDto comments = commentService.getAllComments(boardId, pageable); + return ApiResponse.success(comments); } @Operation(summary = "[U] 나의 댓글 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my-comments") - public ResponseModel getMyComments( + public ApiResponse> getMyComments( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto comments = commentService.getMyComments(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(comments); - return responseModel; + PagedResponseDto comments = commentService.getMyComments(pageable); + return ApiResponse.success(comments); } @Operation(summary = "[U] 댓글 수정", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{commentId}") - public ResponseModel updateComment( + public ApiResponse updateComment( @PathVariable(name = "commentId") Long commentId, - @Valid @RequestBody CommentUpdateRequestDto commentUpdateRequestDto + @Valid @RequestBody CommentUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = commentService.updateComment(commentId, commentUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = commentService.updateComment(commentId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 댓글 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{commentId}") - public ResponseModel deleteComment( + public ApiResponse deleteComment( @PathVariable(name = "commentId") Long commentId ) throws PermissionDeniedException { Long id = commentService.deleteComment(commentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @PostMapping("/likes/{commentId}") @Operation(summary = "[U] 댓글 좋아요 누르기/취소하기", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel toggleLikeStatus( + public ApiResponse toggleLikeStatus( @PathVariable(name = "commentId") Long commentId ) { Long id = commentService.toggleLikeStatus(commentId); - ResponseModel responseModel= ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted/{boardId}") + @Operation(summary = "[S] 게시글의 삭제된 댓글 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedComments( + @PathVariable(name = "boardId") Long boardId, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto comments = commentService.getDeletedComments(boardId, pageable); + return ApiResponse.success(comments); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/comment/application/CommentService.java b/src/main/java/page/clab/api/domain/comment/application/CommentService.java index ba0540185..d890ff4d4 100644 --- a/src/main/java/page/clab/api/domain/comment/application/CommentService.java +++ b/src/main/java/page/clab/api/domain/comment/application/CommentService.java @@ -1,12 +1,12 @@ package page.clab.api.domain.comment.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.Hibernate; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.board.application.BoardService; import page.clab.api.domain.board.domain.Board; import page.clab.api.domain.comment.dao.CommentLikeRepository; @@ -15,14 +15,16 @@ import page.clab.api.domain.comment.domain.CommentLike; import page.clab.api.domain.comment.dto.request.CommentRequestDto; import page.clab.api.domain.comment.dto.request.CommentUpdateRequestDto; -import page.clab.api.domain.comment.dto.response.CommentGetAllResponseDto; -import page.clab.api.domain.comment.dto.response.CommentGetMyResponseDto; +import page.clab.api.domain.comment.dto.response.CommentMyResponseDto; +import page.clab.api.domain.comment.dto.response.CommentResponseDto; +import page.clab.api.domain.comment.dto.response.DeletedCommentResponseDto; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; import java.util.Optional; @@ -31,43 +33,56 @@ @Slf4j public class CommentService { - private final CommentRepository commentRepository; - - private final CommentLikeRepository commentLikeRepository; - private final BoardService boardService; private final MemberService memberService; private final NotificationService notificationService; + private final ValidationService validationService; + + private final CommentRepository commentRepository; + + private final CommentLikeRepository commentLikeRepository; + @Transactional - public Long createComment(Long parentId, Long boardId, CommentRequestDto dto) { - Comment comment = createAndStoreComment(parentId, boardId, dto); + public Long createComment(Long parentId, Long boardId, CommentRequestDto requestDto) { + Comment comment = createAndStoreComment(parentId, boardId, requestDto); sendNotificationForNewComment(comment); return comment.getId(); } - public PagedResponseDto getAllComments(Long boardId, Pageable pageable) { + @Transactional(readOnly = true) + public PagedResponseDto getAllComments(Long boardId, Pageable pageable) { Member currentMember = memberService.getCurrentMember(); Page comments = getCommentByBoardIdAndParentIsNull(boardId, pageable); comments.forEach(comment -> Hibernate.initialize(comment.getChildren())); - Page commentDtos = comments.map(comment -> toCommentGetAllResponseDto(comment, currentMember)); + Page commentDtos = comments.map(comment -> toCommentResponseDto(comment, currentMember)); return new PagedResponseDto<>(commentDtos); } - public PagedResponseDto getMyComments(Pageable pageable) { + @Transactional(readOnly = true) + public PagedResponseDto getMyComments(Pageable pageable) { Member currentMember = memberService.getCurrentMember(); Page comments = getCommentByWriter(currentMember, pageable); - Page commentDtos = comments.map(comment -> toCommentGetMyResponseDto(comment, currentMember)); + Page commentDtos = comments.map(comment -> toCommentMyResponseDto(comment, currentMember)); return new PagedResponseDto<>(commentDtos); } - public Long updateComment(Long commentId, CommentUpdateRequestDto dto) throws PermissionDeniedException { + @Transactional(readOnly = true) + public PagedResponseDto getDeletedComments(Long boardId, Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + Page comments = commentRepository.findAllByIsDeletedTrueAndBoardId(boardId, pageable); + return new PagedResponseDto<>(comments.map(comment -> DeletedCommentResponseDto.toDto(comment, currentMember.getId()))); + } + + @Transactional + public Long updateComment(Long commentId, CommentUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); Comment comment = getCommentByIdOrThrow(commentId); comment.validateAccessPermission(currentMember); - comment.update(dto); + comment.update(requestDto); + validationService.checkValid(comment); return commentRepository.save(comment).getId(); } @@ -75,7 +90,8 @@ public Long deleteComment(Long commentId) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); Comment comment = getCommentByIdOrThrow(commentId); comment.validateAccessPermission(currentMember); - commentRepository.delete(comment); + comment.updateIsDeleted(); + commentRepository.save(comment); return comment.getId(); } @@ -89,16 +105,12 @@ public Long toggleLikeStatus(Long commentId) { commentLikeRepository.delete(commentLikeOpt.get()); } else { comment.incrementLikes(); - CommentLike newLike = new CommentLike(currentMember.getId(), comment.getId()); + CommentLike newLike = CommentLike.create(currentMember.getId(), comment.getId()); commentLikeRepository.save(newLike); } return comment.getLikes(); } - public boolean isCommentExistById(Long id) { - return commentRepository.existsById(id); - } - public Comment getCommentByIdOrThrow(Long id) { return commentRepository.findById(id) .orElseThrow(() -> new NotFoundException("댓글을 찾을 수 없습니다.")); @@ -112,14 +124,15 @@ private Page getCommentByWriter(Member member, Pageable pageable) { return commentRepository.findAllByWriterOrderByCreatedAtDesc(member, pageable); } - private Comment createAndStoreComment(Long parentId, Long boardId, CommentRequestDto dto) { + private Comment createAndStoreComment(Long parentId, Long boardId, CommentRequestDto requestDto) { Member currentMember = memberService.getCurrentMember(); Board board = boardService.getBoardByIdOrThrow(boardId); Comment parent = findParentComment(parentId); - Comment comment = Comment.create(dto, board, currentMember, parent); + Comment comment = CommentRequestDto.toEntity(requestDto, board, currentMember, parent); if (parent != null) { parent.addChildComment(comment); } + validationService.checkValid(comment); return commentRepository.save(comment); } @@ -134,26 +147,28 @@ private void sendNotificationForNewComment(Comment comment) { notificationService.sendNotificationToMember(boardOwner, notificationMessage); } - private CommentGetAllResponseDto toCommentGetAllResponseDto(Comment comment, Member currentMember) { - CommentGetAllResponseDto dto = CommentGetAllResponseDto.of(comment, currentMember.getId()); - dto.setHasLikeByMe(checkLikeStatus(comment.getId(), currentMember.getId())); - dto.getChildren().forEach(childDto -> setLikeStatusForChildren(childDto, currentMember)); - return dto; + private CommentResponseDto toCommentResponseDto(Comment comment, Member currentMember) { + Boolean hasLikeByMe = checkLikeStatus(comment.getId(), currentMember.getId()); + CommentResponseDto responseDto = CommentResponseDto.toDto(comment, currentMember.getId()); + responseDto.getChildren().forEach(childDto -> setLikeStatusForChildren(childDto, currentMember)); + if (!responseDto.getIsDeleted()) { + responseDto.setHasLikeByMe(hasLikeByMe); + } + return responseDto; } private boolean checkLikeStatus(Long commentId, String memberId) { return commentLikeRepository.existsByCommentIdAndMemberId(commentId, memberId); } - private void setLikeStatusForChildren(CommentGetAllResponseDto dto, Member member) { - dto.setHasLikeByMe(checkLikeStatus(dto.getId(), member.getId())); - dto.getChildren().forEach(childDto -> setLikeStatusForChildren(childDto, member)); + private void setLikeStatusForChildren(CommentResponseDto responseDto, Member member) { + responseDto.setHasLikeByMe(checkLikeStatus(responseDto.getId(), member.getId())); + responseDto.getChildren().forEach(childDto -> setLikeStatusForChildren(childDto, member)); } - private CommentGetMyResponseDto toCommentGetMyResponseDto(Comment comment, Member currentMember) { - CommentGetMyResponseDto dto = CommentGetMyResponseDto.of(comment); - dto.setHasLikeByMe(checkLikeStatus(comment.getId(), currentMember.getId())); - return dto; + private CommentMyResponseDto toCommentMyResponseDto(Comment comment, Member currentMember) { + Boolean hasLikeByMe = checkLikeStatus(comment.getId(), currentMember.getId()); + return CommentMyResponseDto.toDto(comment, hasLikeByMe); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/comment/dao/CommentRepository.java b/src/main/java/page/clab/api/domain/comment/dao/CommentRepository.java index 342c18a48..8e9868df9 100644 --- a/src/main/java/page/clab/api/domain/comment/dao/CommentRepository.java +++ b/src/main/java/page/clab/api/domain/comment/dao/CommentRepository.java @@ -1,11 +1,12 @@ package page.clab.api.domain.comment.dao; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import page.clab.api.domain.board.domain.Board; import page.clab.api.domain.comment.domain.Comment; import page.clab.api.domain.member.domain.Member; @@ -16,6 +17,9 @@ public interface CommentRepository extends JpaRepository { Page findAllByWriterOrderByCreatedAtDesc(Member member, Pageable pageable); - Optional findById(Long id); + Long countByBoard(Board board); + + @Query(value = "SELECT c.* FROM comment c WHERE c.is_deleted = true AND c.board_id = ?", nativeQuery = true) + Page findAllByIsDeletedTrueAndBoardId(Long boardId, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/comment/domain/Comment.java b/src/main/java/page/clab/api/domain/comment/domain/Comment.java index 1c1408a8a..bf691a2b9 100644 --- a/src/main/java/page/clab/api/domain/comment/domain/Comment.java +++ b/src/main/java/page/clab/api/domain/comment/domain/Comment.java @@ -10,21 +10,18 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; import page.clab.api.domain.board.domain.Board; -import page.clab.api.domain.comment.dto.request.CommentRequestDto; import page.clab.api.domain.comment.dto.request.CommentUpdateRequestDto; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; -import page.clab.api.global.util.RandomNicknameUtil; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -33,9 +30,9 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Comment { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Comment extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -52,7 +49,7 @@ public class Comment { @Column(nullable = false) private String nickname; - @Column(nullable = false, length = 1000) + @Column(nullable = false) @Size(min = 1, max = 1000, message = "{size.comment.content}") private String content; @@ -66,32 +63,14 @@ public class Comment { @JsonIgnoreProperties("parent") private List children = new ArrayList<>(); - @Column(name = "update_time") - private LocalDateTime updateTime; - - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - @Column(name = "want_anonymous", nullable = false) + @Column(nullable = false) private boolean wantAnonymous; private Long likes; - public static Comment create(CommentRequestDto commentRequestDto, Board board, Member member, Comment parent) { - Comment comment = ModelMapperUtil.getModelMapper().map(commentRequestDto, Comment.class); - comment.setBoard(board); - comment.setWriter(member); - comment.setNickname(RandomNicknameUtil.makeRandomNickname()); - comment.setLikes(0L); - comment.parent = parent; - return comment; - } - public void update(CommentUpdateRequestDto commentUpdateRequestDto) { Optional.ofNullable(commentUpdateRequestDto.getContent()).ifPresent(this::setContent); Optional.of(commentUpdateRequestDto.isWantAnonymous()).ifPresent(this::setWantAnonymous); - this.setUpdateTime(LocalDateTime.now()); } public void addChildComment(Comment child) { @@ -107,6 +86,10 @@ public boolean isOwner(Member member) { return this.writer.isSameMember(member); } + public boolean isOwner(String memberId) { + return this.writer.isSameMember(memberId); + } + public void validateAccessPermission(Member member) throws PermissionDeniedException { if (!isOwner(member) && !member.isAdminRole()) { throw new PermissionDeniedException("해당 댓글을 수정/삭제할 권한이 없습니다."); @@ -123,4 +106,8 @@ public void decrementLikes() { } } + public void updateIsDeleted(){ + this.isDeleted = true; + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/comment/domain/CommentLike.java b/src/main/java/page/clab/api/domain/comment/domain/CommentLike.java index bb4ac7362..c9eec5378 100644 --- a/src/main/java/page/clab/api/domain/comment/domain/CommentLike.java +++ b/src/main/java/page/clab/api/domain/comment/domain/CommentLike.java @@ -6,20 +6,22 @@ import jakarta.persistence.Id; import jakarta.persistence.Index; import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.global.common.domain.BaseEntity; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Table(indexes = @Index(name = "commentLike_index", columnList = "memberId, commentId")) -public class CommentLike { +public class CommentLike extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,9 +31,11 @@ public class CommentLike { private Long commentId; - public CommentLike(String memberId, Long commentId) { - this.memberId = memberId; - this.commentId = commentId; + public static CommentLike create(String memberId, Long commentId) { + return CommentLike.builder() + .memberId(memberId) + .commentId(commentId) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/comment/dto/request/CommentLikeRequestDto.java b/src/main/java/page/clab/api/domain/comment/dto/request/CommentLikeRequestDto.java index aac7672e0..4352ba01e 100644 --- a/src/main/java/page/clab/api/domain/comment/dto/request/CommentLikeRequestDto.java +++ b/src/main/java/page/clab/api/domain/comment/dto/request/CommentLikeRequestDto.java @@ -2,17 +2,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class CommentLikeRequestDto { @NotNull(message = "{notNull.commentLike.commentId}") diff --git a/src/main/java/page/clab/api/domain/comment/dto/request/CommentRequestDto.java b/src/main/java/page/clab/api/domain/comment/dto/request/CommentRequestDto.java index de8eba3ef..8879451d7 100644 --- a/src/main/java/page/clab/api/domain/comment/dto/request/CommentRequestDto.java +++ b/src/main/java/page/clab/api/domain/comment/dto/request/CommentRequestDto.java @@ -2,25 +2,18 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.board.domain.Board; import page.clab.api.domain.comment.domain.Comment; -import page.clab.api.domain.comment.dto.response.CommentGetAllResponseDto; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.util.RandomNicknameUtil; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class CommentRequestDto { @NotNull(message = "{notNull.comment.content}") - @Size(min = 1, max = 1000, message = "{size.comment.content}") @Schema(description = "내용", example = "댓글 내용", required = true) private String content; @@ -28,8 +21,16 @@ public class CommentRequestDto { @Schema(description = "익명 사용 여부", example = "false", required = true) private boolean wantAnonymous; - public static CommentGetAllResponseDto of(Comment comment) { - return ModelMapperUtil.getModelMapper().map(comment, CommentGetAllResponseDto.class); + public static Comment toEntity(CommentRequestDto requestDto, Board board, Member member, Comment parent) { + return Comment.builder() + .board(board) + .writer(member) + .nickname(RandomNicknameUtil.makeRandomNickname()) + .content(requestDto.getContent()) + .parent(parent) + .wantAnonymous(requestDto.isWantAnonymous()) + .likes(0L) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/comment/dto/request/CommentUpdateRequestDto.java b/src/main/java/page/clab/api/domain/comment/dto/request/CommentUpdateRequestDto.java index 79163d79c..c8fc119f9 100644 --- a/src/main/java/page/clab/api/domain/comment/dto/request/CommentUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/comment/dto/request/CommentUpdateRequestDto.java @@ -1,32 +1,20 @@ package page.clab.api.domain.comment.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; +import jakarta.validation.constraints.NotNull; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import page.clab.api.domain.comment.domain.Comment; -import page.clab.api.domain.comment.dto.response.CommentGetAllResponseDto; -import page.clab.api.global.util.ModelMapperUtil; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class CommentUpdateRequestDto { - @Size(min = 1, max = 1000, message = "{size.comment.content}") + @NotNull(message = "{notNull.comment.content}") @Schema(description = "내용", example = "댓글 내용") private String content; + @NotNull(message = "{notNull.comment.wantAnonymous}") @Schema(description = "익명 사용 여부", example = "false") private boolean wantAnonymous; - public static CommentGetAllResponseDto of(Comment comment) { - return ModelMapperUtil.getModelMapper().map(comment, CommentGetAllResponseDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/comment/dto/response/CommentGetAllResponseDto.java b/src/main/java/page/clab/api/domain/comment/dto/response/CommentGetAllResponseDto.java deleted file mode 100644 index 325265658..000000000 --- a/src/main/java/page/clab/api/domain/comment/dto/response/CommentGetAllResponseDto.java +++ /dev/null @@ -1,69 +0,0 @@ -package page.clab.api.domain.comment.dto.response; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import page.clab.api.domain.comment.domain.Comment; -import page.clab.api.global.util.ModelMapperUtil; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -@ToString -public class CommentGetAllResponseDto { - - private Long id; - - private String writer; - - private String writerImageUrl; - - private Long writerRoleLevel; - - private String content; - - private List children; - - private Long likes; - - private boolean hasLikeByMe; - - @JsonProperty("isOwner") - private boolean isOwner; - - private LocalDateTime createdAt; - - public static CommentGetAllResponseDto of(Comment comment, String currentMemberId) { - CommentGetAllResponseDto commentGetAllResponseDto = ModelMapperUtil.getModelMapper().map(comment, CommentGetAllResponseDto.class); - - commentGetAllResponseDto.setWriterRoleLevel(comment.getWriter().getRole().toLong()); - - if(comment.isWantAnonymous()){ - commentGetAllResponseDto.setWriter(comment.getNickname()); - commentGetAllResponseDto.setWriterImageUrl(null); - } - else{ - commentGetAllResponseDto.setWriter(comment.getWriter().getName()); - commentGetAllResponseDto.setWriterImageUrl(comment.getWriter().getImageUrl()); - } - - commentGetAllResponseDto.setOwner(comment.getWriter().getId().equals(currentMemberId)); - - List childrenDto = comment.getChildren().stream() - .map(child -> CommentGetAllResponseDto.of(child, currentMemberId)) - .collect(Collectors.toList()); - commentGetAllResponseDto.setChildren(childrenDto); - return commentGetAllResponseDto; - } - -} diff --git a/src/main/java/page/clab/api/domain/comment/dto/response/CommentGetMyResponseDto.java b/src/main/java/page/clab/api/domain/comment/dto/response/CommentGetMyResponseDto.java deleted file mode 100644 index 3998169a1..000000000 --- a/src/main/java/page/clab/api/domain/comment/dto/response/CommentGetMyResponseDto.java +++ /dev/null @@ -1,60 +0,0 @@ -package page.clab.api.domain.comment.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import page.clab.api.domain.comment.domain.Comment; -import page.clab.api.global.util.ModelMapperUtil; - -import java.time.LocalDateTime; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -@ToString -public class CommentGetMyResponseDto { - - private Long id; - - private Long boardId; - - private String boardCategory; - - private String writer; - - private String writerImageUrl; - - private String content; - - private Long likes; - - private boolean hasLikeByMe; - - private LocalDateTime createdAt; - - public static CommentGetMyResponseDto of(Comment comment) { - CommentGetMyResponseDto commentGetAllResponseDto = ModelMapperUtil.getModelMapper().map(comment, CommentGetMyResponseDto.class); - - commentGetAllResponseDto.setBoardId(comment.getBoard().getId()); - commentGetAllResponseDto.setBoardCategory(comment.getBoard().getCategory()); - - if(comment.isWantAnonymous()){ - commentGetAllResponseDto.setWriter(comment.getNickname()); - commentGetAllResponseDto.setWriter(null); - } - else{ - commentGetAllResponseDto.setWriter(comment.getWriter().getName()); - commentGetAllResponseDto.setWriter(comment.getWriter().getImageUrl()); - } - - commentGetAllResponseDto.setWriterImageUrl(comment.getWriter().getImageUrl()); - - return commentGetAllResponseDto; - } - -} diff --git a/src/main/java/page/clab/api/domain/comment/dto/response/CommentMyResponseDto.java b/src/main/java/page/clab/api/domain/comment/dto/response/CommentMyResponseDto.java new file mode 100644 index 000000000..8ca4dd25d --- /dev/null +++ b/src/main/java/page/clab/api/domain/comment/dto/response/CommentMyResponseDto.java @@ -0,0 +1,46 @@ +package page.clab.api.domain.comment.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.domain.board.domain.BoardCategory; +import page.clab.api.domain.comment.domain.Comment; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class CommentMyResponseDto { + + private Long id; + + private Long boardId; + + private String boardCategory; + + private String writer; + + private String writerImageUrl; + + private String content; + + private Long likes; + + private boolean hasLikeByMe; + + private LocalDateTime createdAt; + + public static CommentMyResponseDto toDto(Comment comment, boolean hasLikeByMe) { + return CommentMyResponseDto.builder() + .id(comment.getId()) + .boardId(comment.getBoard().getId()) + .boardCategory(comment.getBoard().getCategory().getKey()) + .writer(comment.isWantAnonymous() ? comment.getNickname() : comment.getWriter().getName()) + .writerImageUrl(comment.isWantAnonymous() ? null : comment.getWriter().getImageUrl()) + .content(comment.getContent()) + .likes(comment.getLikes()) + .hasLikeByMe(hasLikeByMe) + .createdAt(comment.getCreatedAt()) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/comment/dto/response/CommentResponseDto.java b/src/main/java/page/clab/api/domain/comment/dto/response/CommentResponseDto.java new file mode 100644 index 000000000..1222caece --- /dev/null +++ b/src/main/java/page/clab/api/domain/comment/dto/response/CommentResponseDto.java @@ -0,0 +1,70 @@ +package page.clab.api.domain.comment.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import page.clab.api.domain.comment.domain.Comment; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Setter +@Builder +public class CommentResponseDto { + + private Long id; + + private Boolean isDeleted; + + private String writerId; + + private String writerName; + + private String writerImageUrl; + + private Long writerRoleLevel; + + private String content; + + private List children; + + private Long likes; + + private Boolean hasLikeByMe; + + @JsonProperty("isOwner") + private Boolean isOwner; + + private LocalDateTime createdAt; + + public static CommentResponseDto toDto(Comment comment, String currentMemberId) { + if (comment.getIsDeleted()) { + return CommentResponseDto.builder() + .id(comment.getId()) + .isDeleted(true) + .children(comment.getChildren().stream() + .map(child -> CommentResponseDto.toDto(child, currentMemberId)) + .toList()) + .build(); + } + return CommentResponseDto.builder() + .id(comment.getId()) + .isDeleted(false) + .writerId(comment.isWantAnonymous() ? null : comment.getWriter().getId()) + .writerName(comment.isWantAnonymous() ? comment.getNickname() : comment.getWriter().getName()) + .writerImageUrl(comment.isWantAnonymous() ? null : comment.getWriter().getImageUrl()) + .writerRoleLevel(comment.isWantAnonymous() ? null : comment.getWriter().getRole().toRoleLevel()) + .content(comment.getContent()) + .children(comment.getChildren().stream() + .map(child -> CommentResponseDto.toDto(child, currentMemberId)) + .toList()) + .likes(comment.getLikes()) + .isOwner(comment.isOwner(currentMemberId)) + .createdAt(comment.getCreatedAt()) + .build(); + + } + +} diff --git a/src/main/java/page/clab/api/domain/comment/dto/response/DeletedCommentResponseDto.java b/src/main/java/page/clab/api/domain/comment/dto/response/DeletedCommentResponseDto.java new file mode 100644 index 000000000..2a94dfc4f --- /dev/null +++ b/src/main/java/page/clab/api/domain/comment/dto/response/DeletedCommentResponseDto.java @@ -0,0 +1,49 @@ +package page.clab.api.domain.comment.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import page.clab.api.domain.comment.domain.Comment; + +@Getter +@Setter +@Builder +public class DeletedCommentResponseDto { + + private Long id; + + private String writerId; + + private String writerName; + + private String writerImageUrl; + + private Long writerRoleLevel; + + private String content; + + private Long likes; + + @JsonProperty("isOwner") + private boolean isOwner; + + private LocalDateTime createdAt; + + public static DeletedCommentResponseDto toDto(Comment comment, String currentMemberId) { + return DeletedCommentResponseDto.builder() + .id(comment.getId()) + .writerId(comment.isWantAnonymous() ? null : comment.getWriter().getId()) + .writerName(comment.isWantAnonymous() ? comment.getNickname() : comment.getWriter().getName()) + .writerImageUrl(comment.isWantAnonymous() ? null : comment.getWriter().getImageUrl()) + .writerRoleLevel(comment.isWantAnonymous() ? null : comment.getWriter().getRole().toRoleLevel()) + .content(comment.getContent()) + .likes(comment.getLikes()) + .isOwner(comment.isOwner(currentMemberId)) + .createdAt(comment.getCreatedAt()) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/donation/api/DonationController.java b/src/main/java/page/clab/api/domain/donation/api/DonationController.java index 1864302cb..d86de8788 100644 --- a/src/main/java/page/clab/api/domain/donation/api/DonationController.java +++ b/src/main/java/page/clab/api/domain/donation/api/DonationController.java @@ -21,14 +21,14 @@ import page.clab.api.domain.donation.dto.request.DonationRequestDto; import page.clab.api.domain.donation.dto.request.DonationUpdateRequestDto; import page.clab.api.domain.donation.dto.response.DonationResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; import java.time.LocalDate; @RestController -@RequestMapping("/donations") +@RequestMapping("/api/v1/donations") @RequiredArgsConstructor @Tag(name = "Donation", description = "후원") @Slf4j @@ -39,13 +39,11 @@ public class DonationController { @Operation(summary = "[S] 후원 생성", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("") - public ResponseModel createDonation( - @Valid @RequestBody DonationRequestDto donationRequestDto + public ApiResponse createDonation( + @Valid @RequestBody DonationRequestDto requestDto ) { - Long id = donationService.createDonation(donationRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = donationService.createDonation(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 후원 목록 조회(멤버 ID, 멤버 이름, 기간 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -53,7 +51,7 @@ public ResponseModel createDonation( "멤버 ID, 멤버 이름, 기간 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getDonationsByConditions( + public ApiResponse> getDonationsByConditions( @RequestParam(name = "memberId", required = false) String memberId, @RequestParam(name = "name", required = false) String name, @RequestParam(name = "startDate", required = false) LocalDate startDate, @@ -63,48 +61,52 @@ public ResponseModel getDonationsByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto donations = donationService.getDonationsByConditions(memberId, name, startDate, endDate, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(donations); - return responseModel; + return ApiResponse.success(donations); } @Operation(summary = "[U] 나의 후원 정보", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my-donations") - public ResponseModel getMyDonations( + public ApiResponse> getMyDonations( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto donations = donationService.getMyDonations(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(donations); - return responseModel; + return ApiResponse.success(donations); } @Operation(summary = "[S] 후원 정보 수정", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PatchMapping("/{donationId}") - public ResponseModel updateDonation( + public ApiResponse updateDonation( @PathVariable(name = "donationId") Long donationId, - @Valid @RequestBody DonationUpdateRequestDto donationUpdateRequestDto + @Valid @RequestBody DonationUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = donationService.updateDonation(donationId, donationUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = donationService.updateDonation(donationId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[S] 후원 삭제", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/{donationId}") - public ResponseModel deleteDonation( + public ApiResponse deleteDonation( @PathVariable(name = "donationId") Long donationId ) throws PermissionDeniedException { Long id = donationService.deleteDonation(donationId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 후원 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedDonations( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto donations = donationService.getDeletedDonations(pageable); + return ApiResponse.success(donations); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/donation/application/DonationService.java b/src/main/java/page/clab/api/domain/donation/application/DonationService.java index 27aada693..897fa6e6d 100644 --- a/src/main/java/page/clab/api/domain/donation/application/DonationService.java +++ b/src/main/java/page/clab/api/domain/donation/application/DonationService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.donation.dao.DonationRepository; import page.clab.api.domain.donation.domain.Donation; import page.clab.api.domain.donation.dto.request.DonationRequestDto; @@ -14,6 +15,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; import java.time.LocalDate; @@ -23,37 +25,51 @@ public class DonationService { private final MemberService memberService; + private final ValidationService validationService; + private final DonationRepository donationRepository; - public Long createDonation(DonationRequestDto donationRequestDto) { - Member member = memberService.getCurrentMember(); - Donation donation = Donation.of(donationRequestDto, member); + @Transactional + public Long createDonation(DonationRequestDto requestDto) { + Member currentMember = memberService.getCurrentMember(); + Donation donation = DonationRequestDto.toEntity(requestDto, currentMember); + validationService.checkValid(donation); return donationRepository.save(donation).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getDonationsByConditions(String memberId, String name, LocalDate startDate, LocalDate endDate, Pageable pageable) { Page donations = donationRepository.findByConditions(memberId, name, startDate, endDate, pageable); - return new PagedResponseDto<>(donations.map(DonationResponseDto::of)); + return new PagedResponseDto<>(donations.map(DonationResponseDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getMyDonations(Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page donations = getDonationsByDonor(member, pageable); - return new PagedResponseDto<>(donations.map(DonationResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + Page donations = getDonationsByDonor(currentMember, pageable); + return new PagedResponseDto<>(donations.map(DonationResponseDto::toDto)); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedDonations(Pageable pageable) { + Page donations = donationRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(donations.map(DonationResponseDto::toDto)); } + @Transactional public Long updateDonation(Long donationId, DonationUpdateRequestDto donationUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Donation donation = getDonationByIdOrThrow(donationId); - validateDonationUpdatePermission(member); + validateDonationUpdatePermission(currentMember); donation.update(donationUpdateRequestDto); + validationService.checkValid(donation); return donationRepository.save(donation).getId(); } public Long deleteDonation(Long donationId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Donation donation = getDonationByIdOrThrow(donationId); - validateDonationUpdatePermission(member); + validateDonationUpdatePermission(currentMember); donationRepository.delete(donation); return donation.getId(); } diff --git a/src/main/java/page/clab/api/domain/donation/dao/DonationRepository.java b/src/main/java/page/clab/api/domain/donation/dao/DonationRepository.java index 5b119d7d0..ef2636d32 100644 --- a/src/main/java/page/clab/api/domain/donation/dao/DonationRepository.java +++ b/src/main/java/page/clab/api/domain/donation/dao/DonationRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.donation.domain.Donation; import page.clab.api.domain.member.domain.Member; @@ -14,4 +15,7 @@ public interface DonationRepository extends JpaRepository, Donat Page findByDonorOrderByCreatedAtDesc(Member member, Pageable pageable); + @Query(value = "SELECT d.* FROM donation d WHERE d.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/donation/domain/Donation.java b/src/main/java/page/clab/api/domain/donation/domain/Donation.java index 5028238ed..26f8c2081 100644 --- a/src/main/java/page/clab/api/domain/donation/domain/Donation.java +++ b/src/main/java/page/clab/api/domain/donation/domain/Donation.java @@ -9,27 +9,29 @@ import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import page.clab.api.domain.donation.dto.request.DonationRequestDto; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.donation.dto.request.DonationUpdateRequestDto; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Donation { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE donation SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Donation extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -43,20 +45,10 @@ public class Donation { @Min(value = 1, message = "{min.donation.amount}") private Double amount; - @Column(nullable = false, length = 1000) + @Column(nullable = false) @Size(min = 1, max = 1000, message = "{size.donation.message}") private String message; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static Donation of(DonationRequestDto donationRequestDto, Member member) { - Donation donation = ModelMapperUtil.getModelMapper().map(donationRequestDto, Donation.class); - donation.setDonor(member); - return donation; - } - public void update(DonationUpdateRequestDto donationUpdateRequestDto) { Optional.ofNullable(donationUpdateRequestDto.getAmount()).ifPresent(this::setAmount); Optional.ofNullable(donationUpdateRequestDto.getMessage()).ifPresent(this::setMessage); diff --git a/src/main/java/page/clab/api/domain/donation/dto/request/DonationRequestDto.java b/src/main/java/page/clab/api/domain/donation/dto/request/DonationRequestDto.java index c89936005..b17733320 100644 --- a/src/main/java/page/clab/api/domain/donation/dto/request/DonationRequestDto.java +++ b/src/main/java/page/clab/api/domain/donation/dto/request/DonationRequestDto.java @@ -1,30 +1,30 @@ package page.clab.api.domain.donation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.donation.domain.Donation; +import page.clab.api.domain.member.domain.Member; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class DonationRequestDto { @NotNull(message = "{notNull.donation.amount}") - @Min(value = 1, message = "{min.donation.amount}") @Schema(description = "금액", example = "100000", required = true) private Double amount; @NotNull(message = "{notNull.donation.message}") - @Size(min = 1, max = 1000, message = "{size.donation.message}") @Schema(description = "후원 메시지", example = "대회 상금 일부 후원", required = true) private String message; + public static Donation toEntity(DonationRequestDto requestDto, Member member) { + return Donation.builder() + .donor(member) + .amount(requestDto.getAmount()) + .message(requestDto.getMessage()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/donation/dto/request/DonationUpdateRequestDto.java b/src/main/java/page/clab/api/domain/donation/dto/request/DonationUpdateRequestDto.java index 79c09aaed..24451e899 100644 --- a/src/main/java/page/clab/api/domain/donation/dto/request/DonationUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/donation/dto/request/DonationUpdateRequestDto.java @@ -2,25 +2,16 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class DonationUpdateRequestDto { @Min(value = 1, message = "{min.donation.amount}") - @Schema(description = "금액", example = "100000", required = true) private Double amount; - @Size(min = 1, max = 1000, message = "{size.donation.message}") @Schema(description = "후원 메시지", example = "대회 상금 일부 후원", required = true) private String message; diff --git a/src/main/java/page/clab/api/domain/donation/dto/response/DonationResponseDto.java b/src/main/java/page/clab/api/domain/donation/dto/response/DonationResponseDto.java index 963c5425a..f953ee30d 100644 --- a/src/main/java/page/clab/api/domain/donation/dto/response/DonationResponseDto.java +++ b/src/main/java/page/clab/api/domain/donation/dto/response/DonationResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.donation.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.donation.domain.Donation; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class DonationResponseDto { @@ -28,13 +22,15 @@ public class DonationResponseDto { private LocalDateTime createdAt; - public static DonationResponseDto of(Donation donation) { - DonationResponseDto donationResponseDto = ModelMapperUtil.getModelMapper().map(donation, DonationResponseDto.class); - if (donation.getDonor() != null) { - donationResponseDto.setDonorId(donation.getDonor().getId()); - donationResponseDto.setName(donation.getDonor().getName()); - } - return donationResponseDto; + public static DonationResponseDto toDto(Donation donation) { + return DonationResponseDto.builder() + .id(donation.getId()) + .donorId(donation.getDonor().getId()) + .name(donation.getDonor().getName()) + .amount(donation.getAmount()) + .message(donation.getMessage()) + .createdAt(donation.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/donation/exception/DonationSearchArgumentLackException.java b/src/main/java/page/clab/api/domain/donation/exception/DonationSearchArgumentLackException.java deleted file mode 100644 index aa07e7d40..000000000 --- a/src/main/java/page/clab/api/domain/donation/exception/DonationSearchArgumentLackException.java +++ /dev/null @@ -1,15 +0,0 @@ -package page.clab.api.domain.donation.exception; - -public class DonationSearchArgumentLackException extends RuntimeException { - - private static final String DEFAULT_MESSAGE = "적어도 memberId 또는 name 중 하나를 제공해야 합니다."; - - public DonationSearchArgumentLackException() { - super(DEFAULT_MESSAGE); - } - - public DonationSearchArgumentLackException(String s) { - super(s); - } - -} diff --git a/src/main/java/page/clab/api/domain/jobPosting/api/JobPostingController.java b/src/main/java/page/clab/api/domain/jobPosting/api/JobPostingController.java index 2b4fd3de3..6e9c2447f 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/api/JobPostingController.java +++ b/src/main/java/page/clab/api/domain/jobPosting/api/JobPostingController.java @@ -23,11 +23,11 @@ import page.clab.api.domain.jobPosting.dto.request.JobPostingUpdateRequestDto; import page.clab.api.domain.jobPosting.dto.response.JobPostingDetailsResponseDto; import page.clab.api.domain.jobPosting.dto.response.JobPostingResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; @RestController -@RequestMapping("/job-postings") +@RequestMapping("/api/v1/job-postings") @RequiredArgsConstructor @Tag(name = "JobPosting", description = "채용 공고") @Slf4j @@ -38,13 +38,11 @@ public class JobPostingController { @Operation(summary = "[A] 채용 공고 등록", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createJobPosting( - @Valid @RequestBody JobPostingRequestDto jobPostingRequestDto + public ApiResponse createJobPosting( + @Valid @RequestBody JobPostingRequestDto requestDto ) { - Long id = jobPostingService.createJobPosting(jobPostingRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = jobPostingService.createJobPosting(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 채용 공고 목록 조회(공고명, 기업명, 경력, 근로 조건 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -52,7 +50,7 @@ public ResponseModel createJobPosting( "공고명, 기업명, 경력, 근로 조건 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getJobPostingsByConditions( + public ApiResponse> getJobPostingsByConditions( @RequestParam(name = "title", required = false) String title, @RequestParam(name = "companyName", required = false) String companyName, @RequestParam(name = "careerLevel", required = false) CareerLevel careerLevel, @@ -62,46 +60,50 @@ public ResponseModel getJobPostingsByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto jobPostings = jobPostingService.getJobPostingsByConditions(title, companyName, careerLevel, employmentType, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(jobPostings); - return responseModel; + return ApiResponse.success(jobPostings); } @Operation(summary = "[U] 채용 공고 상세 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/{jobPostingId}") - public ResponseModel getJobPosting( + public ApiResponse getJobPosting( @PathVariable(name = "jobPostingId") Long jobPostingId ) { JobPostingDetailsResponseDto jobPosting = jobPostingService.getJobPosting(jobPostingId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(jobPosting); - return responseModel; + return ApiResponse.success(jobPosting); } @Operation(summary = "[A] 채용 공고 수정", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/{jobPostingId}") - public ResponseModel updateJobPosting( + public ApiResponse updateJobPosting( @PathVariable(name = "jobPostingId") Long jobPostingId, - @Valid @RequestBody JobPostingUpdateRequestDto jobPostingUpdateRequestDto + @Valid @RequestBody JobPostingUpdateRequestDto requestDto ) { - Long id = jobPostingService.updateJobPosting(jobPostingId, jobPostingUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = jobPostingService.updateJobPosting(jobPostingId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 채용 공고 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{jobPostingId}") - public ResponseModel deleteJobPosting( + public ApiResponse deleteJobPosting( @PathVariable(name = "jobPostingId") Long jobPostingId ) { Long id = jobPostingService.deleteJobPosting(jobPostingId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 채용 공고 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedJobPostings( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto jobPostings = jobPostingService.getDeletedJobPostings(pageable); + return ApiResponse.success(jobPostings); } } diff --git a/src/main/java/page/clab/api/domain/jobPosting/application/JobPostingService.java b/src/main/java/page/clab/api/domain/jobPosting/application/JobPostingService.java index f7f213758..f35f59c31 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/application/JobPostingService.java +++ b/src/main/java/page/clab/api/domain/jobPosting/application/JobPostingService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.jobPosting.dao.JobPostingRepository; import page.clab.api.domain.jobPosting.domain.CareerLevel; import page.clab.api.domain.jobPosting.domain.EmploymentType; @@ -14,37 +15,48 @@ import page.clab.api.domain.jobPosting.dto.response.JobPostingResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor public class JobPostingService { + private final ValidationService validationService; + private final JobPostingRepository jobPostingRepository; - public Long createJobPosting(JobPostingRequestDto jobPostingRequestDto) { - JobPosting jobPosting = jobPostingRepository.findByJobPostingUrl(jobPostingRequestDto.getJobPostingUrl()) - .map(existingJobPosting -> updateExistingJobPosting(existingJobPosting, jobPostingRequestDto)) - .orElseGet(() -> JobPosting.of(jobPostingRequestDto)); + @Transactional + public Long createJobPosting(JobPostingRequestDto requestDto) { + JobPosting jobPosting = jobPostingRepository.findByJobPostingUrl(requestDto.getJobPostingUrl()) + .map(existingJobPosting -> existingJobPosting.updateFromRequestDto(requestDto)) + .orElseGet(() -> JobPostingRequestDto.toEntity(requestDto)); + validationService.checkValid(jobPosting); return jobPostingRepository.save(jobPosting).getId(); } - private JobPosting updateExistingJobPosting(JobPosting existingJobPosting, JobPostingRequestDto jobPostingRequestDto) { - return existingJobPosting.updateFromRequestDto(jobPostingRequestDto); - } - + @Transactional(readOnly = true) public PagedResponseDto getJobPostingsByConditions(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable) { Page jobPostings = jobPostingRepository.findByConditions(title, companyName, careerLevel, employmentType, pageable); - return new PagedResponseDto<>(jobPostings.map(JobPostingResponseDto::of)); + return new PagedResponseDto<>(jobPostings.map(JobPostingResponseDto::toDto)); } + @Transactional(readOnly = true) public JobPostingDetailsResponseDto getJobPosting(Long jobPostingId) { JobPosting jobPosting = getJobPostingByIdOrThrow(jobPostingId); - return JobPostingDetailsResponseDto.of(jobPosting); + return JobPostingDetailsResponseDto.toDto(jobPosting); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedJobPostings(Pageable pageable) { + Page jobPostings = jobPostingRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(jobPostings.map(JobPostingDetailsResponseDto::toDto)); } - public Long updateJobPosting(Long jobPostingId, JobPostingUpdateRequestDto jobPostingUpdateRequestDto) { + @Transactional + public Long updateJobPosting(Long jobPostingId, JobPostingUpdateRequestDto requestDto) { JobPosting jobPosting = getJobPostingByIdOrThrow(jobPostingId); - jobPosting.update(jobPostingUpdateRequestDto); + jobPosting.update(requestDto); + validationService.checkValid(jobPosting); return jobPostingRepository.save(jobPosting).getId(); } @@ -56,7 +68,7 @@ public Long deleteJobPosting(Long jobPostingId) { public JobPosting getJobPostingByIdOrThrow(Long jobPostingId) { return jobPostingRepository.findById(jobPostingId) - .orElseThrow(() -> new NotFoundException("존재하지 않는 채용 공고입니다.")); + .orElseThrow(() -> new NotFoundException("존재하지 않는 채용 공고입니다.")); } } diff --git a/src/main/java/page/clab/api/domain/jobPosting/dao/JobPostingRepository.java b/src/main/java/page/clab/api/domain/jobPosting/dao/JobPostingRepository.java index 013873755..ea31c12f3 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/dao/JobPostingRepository.java +++ b/src/main/java/page/clab/api/domain/jobPosting/dao/JobPostingRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.jobPosting.domain.JobPosting; @@ -15,5 +16,8 @@ public interface JobPostingRepository extends JpaRepository, J Optional findByJobPostingUrl(String jobPostingUrl); Page findAllByOrderByCreatedAtDesc(Pageable pageable); - + + @Query(value = "SELECT j.* FROM job_posting j WHERE j.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/jobPosting/domain/JobPosting.java b/src/main/java/page/clab/api/domain/jobPosting/domain/JobPosting.java index 7e8bcea92..4fa3c6449 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/domain/JobPosting.java +++ b/src/main/java/page/clab/api/domain/jobPosting/domain/JobPosting.java @@ -8,27 +8,30 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.validator.constraints.URL; import page.clab.api.domain.jobPosting.dto.request.JobPostingRequestDto; import page.clab.api.domain.jobPosting.dto.request.JobPostingUpdateRequestDto; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class JobPosting { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE job_posting SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class JobPosting extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -50,18 +53,11 @@ public class JobPosting { private String recruitmentPeriod; - @Column(nullable = false, length = 1000) + @Column(nullable = false) + @Size(max = 1000, message = "{size.jobPosting.jobPostingUrl}") @URL(message = "{url.jobPosting.jobPostingUrl}") private String jobPostingUrl; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static JobPosting of(JobPostingRequestDto jobPostingRequestDto) { - return ModelMapperUtil.getModelMapper().map(jobPostingRequestDto, JobPosting.class); - } - public void update(JobPostingUpdateRequestDto jobPostingUpdateRequestDto) { Optional.ofNullable(jobPostingUpdateRequestDto.getTitle()).ifPresent(this::setTitle); Optional.ofNullable(jobPostingUpdateRequestDto.getCareerLevel()).ifPresent(this::setCareerLevel); diff --git a/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingRequestDto.java b/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingRequestDto.java index 4f1d28ea3..12da6148f 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingRequestDto.java +++ b/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingRequestDto.java @@ -2,27 +2,17 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; import page.clab.api.domain.jobPosting.domain.CareerLevel; import page.clab.api.domain.jobPosting.domain.EmploymentType; import page.clab.api.domain.jobPosting.domain.JobPosting; -import page.clab.api.global.util.ModelMapperUtil; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class JobPostingRequestDto { @NotNull(message = "{notNull.jobPosting.title}") - @Size(min = 1, max = 100, message = "{size.jobPosting.title}") @Schema(description = "공고명", example = "[네이버웹툰] Analytics Engineer(경력)", required = true) private String title; @@ -33,7 +23,6 @@ public class JobPostingRequestDto { private EmploymentType employmentType; @NotNull(message = "{notNull.jobPosting.companyName}") - @Size(min = 1, message = "{size.jobPosting.companyName}") @Schema(description = "기업명", example = "네이버", required = true) private String companyName; @@ -41,12 +30,18 @@ public class JobPostingRequestDto { private String recruitmentPeriod; @NotNull(message = "{notNull.jobPosting.jobPostingUrl}") - @URL(message = "{url.jobPosting.jobPostingUrl}") @Schema(description = "채용 공고 URL", example = "https://recruit.navercorp.com/rcrt/view.do?annoId=30001804&sw=&subJobCdArr=1010001%2C1010002%2C1010003%2C1010004%2C1010005%2C1010006%2C1010007%2C1010008%2C1010020%2C1020001%2C1030001%2C1030002%2C1040001%2C1050001%2C1050002%2C1060001&sysCompanyCdArr=&empTypeCdArr=&entTypeCdArr=&workAreaCdArr=", required = false) private String jobPostingUrl; - public static JobPostingRequestDto of(JobPosting jobPosting) { - return ModelMapperUtil.getModelMapper().map(jobPosting, JobPostingRequestDto.class); + public static JobPosting toEntity(JobPostingRequestDto requestDto) { + return JobPosting.builder() + .title(requestDto.getTitle()) + .careerLevel(requestDto.getCareerLevel()) + .employmentType(requestDto.getEmploymentType()) + .companyName(requestDto.getCompanyName()) + .recruitmentPeriod(requestDto.getRecruitmentPeriod()) + .jobPostingUrl(requestDto.getJobPostingUrl()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingUpdateRequestDto.java b/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingUpdateRequestDto.java index cc615180a..ddca43a6d 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/jobPosting/dto/request/JobPostingUpdateRequestDto.java @@ -1,26 +1,15 @@ package page.clab.api.domain.jobPosting.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; import page.clab.api.domain.jobPosting.domain.CareerLevel; import page.clab.api.domain.jobPosting.domain.EmploymentType; -import page.clab.api.domain.jobPosting.domain.JobPosting; -import page.clab.api.global.util.ModelMapperUtil; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class JobPostingUpdateRequestDto { - @Size(min = 1, max = 100, message = "{size.jobPosting.title}") @Schema(description = "공고명", example = "[네이버웹툰] Analytics Engineer(경력)") private String title; @@ -30,19 +19,13 @@ public class JobPostingUpdateRequestDto { @Schema(description = "고용 형태", example = "FULL_TIME") private EmploymentType employmentType; - @Size(min = 1, message = "{size.jobPosting.companyName}") @Schema(description = "기업명", example = "네이버") private String companyName; @Schema(description = "채용 기간", example = "2024.01.11 ~ 2024.01.28") private String recruitmentPeriod; - @URL(message = "{url.jobPosting.jobPostingUrl}") @Schema(description = "채용 공고 URL", example = "https://recruit.navercorp.com/rcrt/view.do?annoId=30001804&sw=&subJobCdArr=1010001%2C1010002%2C1010003%2C1010004%2C1010005%2C1010006%2C1010007%2C1010008%2C1010020%2C1020001%2C1030001%2C1030002%2C1040001%2C1050001%2C1050002%2C1060001&sysCompanyCdArr=&empTypeCdArr=&entTypeCdArr=&workAreaCdArr=") private String jobPostingUrl; - public static JobPostingUpdateRequestDto of(JobPosting jobPosting) { - return ModelMapperUtil.getModelMapper().map(jobPosting, JobPostingUpdateRequestDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingDetailsResponseDto.java b/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingDetailsResponseDto.java index c3f2f1ef5..3d12924a2 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingDetailsResponseDto.java @@ -1,20 +1,14 @@ package page.clab.api.domain.jobPosting.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.jobPosting.domain.CareerLevel; import page.clab.api.domain.jobPosting.domain.EmploymentType; import page.clab.api.domain.jobPosting.domain.JobPosting; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class JobPostingDetailsResponseDto { @@ -34,8 +28,17 @@ public class JobPostingDetailsResponseDto { private LocalDateTime createdAt; - public static JobPostingDetailsResponseDto of(JobPosting jobPosting) { - return ModelMapperUtil.getModelMapper().map(jobPosting, JobPostingDetailsResponseDto.class); + public static JobPostingDetailsResponseDto toDto(JobPosting jobPosting) { + return JobPostingDetailsResponseDto.builder() + .id(jobPosting.getId()) + .title(jobPosting.getTitle()) + .careerLevel(jobPosting.getCareerLevel()) + .employmentType(jobPosting.getEmploymentType()) + .companyName(jobPosting.getCompanyName()) + .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) + .jobPostingUrl(jobPosting.getJobPostingUrl()) + .createdAt(jobPosting.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingResponseDto.java b/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingResponseDto.java index e7b60c2e6..60b48d537 100644 --- a/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingResponseDto.java +++ b/src/main/java/page/clab/api/domain/jobPosting/dto/response/JobPostingResponseDto.java @@ -1,18 +1,13 @@ package page.clab.api.domain.jobPosting.dto.response; -import java.util.List; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.jobPosting.domain.JobPosting; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; +import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class JobPostingResponseDto { @@ -23,14 +18,22 @@ public class JobPostingResponseDto { private String recruitmentPeriod; private String jobPostingUrl; + + private LocalDateTime createdAt; - public static JobPostingResponseDto of(JobPosting jobPosting) { - return ModelMapperUtil.getModelMapper().map(jobPosting, JobPostingResponseDto.class); + public static JobPostingResponseDto toDto(JobPosting jobPosting) { + return JobPostingResponseDto.builder() + .id(jobPosting.getId()) + .title(jobPosting.getTitle()) + .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) + .jobPostingUrl(jobPosting.getJobPostingUrl()) + .createdAt(jobPosting.getCreatedAt()) + .build(); } - public static List of(List jobPostingList) { + public static List toDto(List jobPostingList) { return jobPostingList.stream() - .map(JobPostingResponseDto::of) + .map(JobPostingResponseDto::toDto) .toList(); } diff --git a/src/main/java/page/clab/api/domain/login/api/AccountLockInfoController.java b/src/main/java/page/clab/api/domain/login/api/AccountLockInfoController.java index c1d71c507..363a25f99 100644 --- a/src/main/java/page/clab/api/domain/login/api/AccountLockInfoController.java +++ b/src/main/java/page/clab/api/domain/login/api/AccountLockInfoController.java @@ -17,10 +17,10 @@ import page.clab.api.domain.login.application.AccountLockInfoService; import page.clab.api.domain.login.dto.response.AccountLockInfoResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; @RestController -@RequestMapping("/account-lock-info") +@RequestMapping("/api/v1/account-lock-info") @RequiredArgsConstructor @Tag(name = "Login", description = "로그인") @Slf4j @@ -31,41 +31,35 @@ public class AccountLockInfoController { @Operation(summary = "[S] 멤버 밴 등록", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("/ban/{memberId}") - public ResponseModel banMember( + public ApiResponse banMember( HttpServletRequest request, @PathVariable(name = "memberId") String memberId ) { Long id = accountLockInfoService.banMemberById(request, memberId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[S] 멤버 밴 해제", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("/unban/{memberId}") - public ResponseModel unbanMember( + public ApiResponse unbanMember( HttpServletRequest request, @PathVariable(name = "memberId") String memberId ) { Long id = accountLockInfoService.unbanMemberById(request, memberId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[S] 밴 멤버 조회", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @GetMapping("") - public ResponseModel getBanList( + public ApiResponse> getBanMembers( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto loginFailInfoResponseDtos = accountLockInfoService.getBanList(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(loginFailInfoResponseDtos); - return responseModel; + PagedResponseDto banMembers = accountLockInfoService.getBanMembers(pageable); + return ApiResponse.success(banMembers); } } diff --git a/src/main/java/page/clab/api/domain/login/api/LoginAttemptLogController.java b/src/main/java/page/clab/api/domain/login/api/LoginAttemptLogController.java index 9d136ae0f..bb6df4dcf 100644 --- a/src/main/java/page/clab/api/domain/login/api/LoginAttemptLogController.java +++ b/src/main/java/page/clab/api/domain/login/api/LoginAttemptLogController.java @@ -15,10 +15,10 @@ import page.clab.api.domain.login.application.LoginAttemptLogService; import page.clab.api.domain.login.dto.response.LoginAttemptLogResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; @RestController -@RequestMapping("/login-attempt-logs") +@RequestMapping("/api/v1/login-attempt-logs") @RequiredArgsConstructor @Tag(name = "LoginAttemptLog", description = "로그인 시도 로그") @Slf4j @@ -29,16 +29,14 @@ public class LoginAttemptLogController { @Operation(summary = "[S] 계정별 로그인 시도 로그 조회", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @GetMapping("/{memberId}") - public ResponseModel getLoginAttemptLogs( + public ApiResponse> getLoginAttemptLogs( @PathVariable(name = "memberId") String memberId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto loginAttemptLogs = loginAttemptLogService.getLoginAttemptLogs(memberId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(loginAttemptLogs); - return responseModel; + return ApiResponse.success(loginAttemptLogs); } } diff --git a/src/main/java/page/clab/api/domain/login/api/LoginController.java b/src/main/java/page/clab/api/domain/login/api/LoginController.java index b741910ed..205cf8b0b 100644 --- a/src/main/java/page/clab/api/domain/login/api/LoginController.java +++ b/src/main/java/page/clab/api/domain/login/api/LoginController.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,10 +24,12 @@ import page.clab.api.domain.login.dto.response.TokenHeader; import page.clab.api.domain.login.exception.LoginFaliedException; import page.clab.api.domain.login.exception.MemberLockedException; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; + +import java.util.List; @RestController -@RequestMapping("/login") +@RequestMapping("/api/v1/login") @RequiredArgsConstructor @Tag(name = "Login", description = "로그인") @Slf4j @@ -39,65 +42,67 @@ public class LoginController { @Operation(summary = "멤버 로그인", description = "ROLE_ANONYMOUS 권한이 필요함") @PostMapping("") - public ResponseModel login( - HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse, - @Valid @RequestBody LoginRequestDto loginRequestDto + public ApiResponse login( + HttpServletRequest request, + HttpServletResponse response, + @Valid @RequestBody LoginRequestDto requestDto ) throws MemberLockedException, LoginFaliedException { - LoginHeader headerData = loginService.login(httpServletRequest, loginRequestDto); - httpServletResponse.setHeader(authHeader, headerData.toJson()); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + LoginHeader headerData = loginService.login(request, requestDto); + response.setHeader(authHeader, headerData.toJson()); + return ApiResponse.success(); } @Operation(summary = "TOTP 인증", description = "ROLE_ANONYMOUS 권한이 필요함") @PostMapping("/authenticator") - public ResponseModel authenticator( - HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse, - @Valid @RequestBody TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto + public ApiResponse authenticator( + HttpServletRequest request, + HttpServletResponse response, + @Valid @RequestBody TwoFactorAuthenticationRequestDto requestDto ) throws LoginFaliedException, MemberLockedException { - TokenHeader headerData = loginService.authenticator(httpServletRequest, twoFactorAuthenticationRequestDto); - httpServletResponse.setHeader(authHeader, headerData.toJson()); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + TokenHeader headerData = loginService.authenticator(request, requestDto); + response.setHeader(authHeader, headerData.toJson()); + return ApiResponse.success(); } @Operation(summary = "[S] TOTP 초기화", description = "ROLE_SUPER 권한이 필요함") @DeleteMapping("/authenticator/{memberId}") @Secured({"ROLE_SUPER"}) - public ResponseModel deleteAuthenticator( + public ApiResponse deleteAuthenticator( @PathVariable(name = "memberId") String memberId ) { String id = loginService.resetAuthenticator(memberId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[S] 멤버 토큰 삭제", description = "ROLE_SUPER 이상의 권한이 필요함") @DeleteMapping("/revoke/{memberId}") @Secured({"ROLE_SUPER"}) - public ResponseModel revoke( + public ApiResponse revoke( @PathVariable(name = "memberId") String memberId ) { String id = loginService.revoke(memberId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[U] 멤버 토큰 재발급", description = "ROLE_USER 이상의 권한이 필요함") @PostMapping("/reissue") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - public ResponseModel reissue( - HttpServletRequest httpServletRequest, - HttpServletResponse httpServletResponse + public ApiResponse reissue( + HttpServletRequest request, + HttpServletResponse response ) { - TokenHeader headerData = loginService.reissue(httpServletRequest); - httpServletResponse.setHeader(authHeader, headerData.toJson()); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + TokenHeader headerData = loginService.reissue(request); + response.setHeader(authHeader, headerData.toJson()); + return ApiResponse.success(); + } + + @Operation(summary = "[S] 현재 로그인 중인 멤버 조회", description = "ROLE_SUPER 이상의 권한이 필요함
" + + "Redis에 저장된 토큰을 조회하여 현재 로그인 중인 멤버를 조회합니다.") + @GetMapping("/current") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getCurrentLoggedInUsers() { + List currentLoggedInUsers = loginService.getCurrentLoggedInUsers(); + return ApiResponse.success(currentLoggedInUsers); } } diff --git a/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java b/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java index b5a076967..f5dc7bb61 100644 --- a/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java +++ b/src/main/java/page/clab/api/domain/login/application/AccountLockInfoService.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.login.dao.AccountLockInfoRepository; import page.clab.api.domain.login.domain.AccountLockInfo; import page.clab.api.domain.login.dto.response.AccountLockInfoResponseDto; @@ -39,12 +40,7 @@ public class AccountLockInfoService { @Value("${security.login-attempt.lock-duration-minutes}") private int lockDurationMinutes; - public AccountLockInfo createAccountLockInfo(Member member) { - AccountLockInfo accountLockInfo = new AccountLockInfo(null, member, 0L, false, null); - accountLockInfoRepository.save(accountLockInfo); - return accountLockInfo; - } - + @Transactional public Long banMemberById(HttpServletRequest request, String memberId) { Member member = memberService.getMemberById(memberId); AccountLockInfo accountLockInfo = ensureAccountLockInfo(member); @@ -54,6 +50,7 @@ public Long banMemberById(HttpServletRequest request, String memberId) { return accountLockInfoRepository.save(accountLockInfo).getId(); } + @Transactional public Long unbanMemberById(HttpServletRequest request, String memberId) { Member member = memberService.getMemberById(memberId); AccountLockInfo accountLockInfo = ensureAccountLockInfo(member); @@ -62,12 +59,14 @@ public Long unbanMemberById(HttpServletRequest request, String memberId) { return accountLockInfoRepository.save(accountLockInfo).getId(); } - public PagedResponseDto getBanList(Pageable pageable) { + @Transactional(readOnly = true) + public PagedResponseDto getBanMembers(Pageable pageable) { LocalDateTime banDate = LocalDateTime.of(9999, 12, 31, 23, 59); - Page banList = accountLockInfoRepository.findByLockUntil(banDate, pageable); - return new PagedResponseDto<>(banList.map(AccountLockInfoResponseDto::of)); + Page banMembers = accountLockInfoRepository.findByLockUntil(banDate, pageable); + return new PagedResponseDto<>(banMembers.map(AccountLockInfoResponseDto::toDto)); } + @Transactional public void handleAccountLockInfo(String memberId) throws MemberLockedException, LoginFaliedException { AccountLockInfo accountLockInfo = ensureAccountLockInfoForMemberId(memberId); validateAccountLockStatus(accountLockInfo); @@ -75,6 +74,7 @@ public void handleAccountLockInfo(String memberId) throws MemberLockedException, accountLockInfoRepository.save(accountLockInfo); } + @Transactional public void handleLoginFailure(HttpServletRequest request, String memberId) throws MemberLockedException, LoginFaliedException { AccountLockInfo accountLockInfo = ensureAccountLockInfoForMemberId(memberId); validateAccountLockStatus(accountLockInfo); @@ -87,6 +87,12 @@ public void handleLoginFailure(HttpServletRequest request, String memberId) thro accountLockInfoRepository.save(accountLockInfo); } + public AccountLockInfo createAccountLockInfo(Member member) { + AccountLockInfo accountLockInfo = AccountLockInfo.create(member); + accountLockInfoRepository.save(accountLockInfo); + return accountLockInfo; + } + private AccountLockInfo ensureAccountLockInfo(Member member) { return accountLockInfoRepository.findByMember(member) .orElseGet(() -> createAccountLockInfo(member)); diff --git a/src/main/java/page/clab/api/domain/login/application/AuthenticatorService.java b/src/main/java/page/clab/api/domain/login/application/AuthenticatorService.java index 49b03f0c5..87e0e5b7b 100644 --- a/src/main/java/page/clab/api/domain/login/application/AuthenticatorService.java +++ b/src/main/java/page/clab/api/domain/login/application/AuthenticatorService.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.login.dao.AuthenticatorRepository; import page.clab.api.domain.login.domain.Authenticator; import page.clab.api.global.exception.NotFoundException; @@ -19,6 +20,9 @@ public class AuthenticatorService { private final GoogleAuthenticator googleAuthenticator; + private final EncryptionUtil encryptionUtil; + + @Transactional public String generateSecretKey(String memberId) { GoogleAuthenticatorKey key = googleAuthenticator.createCredentials(); String secretKey = key.getKey(); @@ -32,7 +36,7 @@ public boolean isAuthenticatorValid(String memberId, String totp) { } private boolean validateTotp(Authenticator authenticator, String totp) { - String secretKey = EncryptionUtil.decrypt(authenticator.getSecretKey()); + String secretKey = encryptionUtil.decrypt(authenticator.getSecretKey()); return googleAuthenticator.authorize(secretKey, Integer.parseInt(totp)); } @@ -52,7 +56,7 @@ public Authenticator getAuthenticatorByIdOrThrow(String memberId) { } private void saveAuthenticator(String memberId, String secretKey) { - Authenticator authenticator = Authenticator.create(memberId, EncryptionUtil.encrypt(secretKey)); + Authenticator authenticator = Authenticator.create(memberId, encryptionUtil.encrypt(secretKey)); authenticatorRepository.save(authenticator); } diff --git a/src/main/java/page/clab/api/domain/login/application/LoginAttemptLogService.java b/src/main/java/page/clab/api/domain/login/application/LoginAttemptLogService.java index 12edc704a..d6dafa3d6 100644 --- a/src/main/java/page/clab/api/domain/login/application/LoginAttemptLogService.java +++ b/src/main/java/page/clab/api/domain/login/application/LoginAttemptLogService.java @@ -1,18 +1,19 @@ package page.clab.api.domain.login.application; +import io.ipinfo.api.model.IPResponse; import jakarta.servlet.http.HttpServletRequest; 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 page.clab.api.domain.login.dao.LoginAttemptLogRepository; -import page.clab.api.domain.login.domain.GeoIpInfo; import page.clab.api.domain.login.domain.LoginAttemptLog; import page.clab.api.domain.login.domain.LoginAttemptResult; import page.clab.api.domain.login.dto.response.LoginAttemptLogResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.util.GeoIpUtil; import page.clab.api.global.util.HttpReqResUtil; +import page.clab.api.global.util.IPInfoUtil; @Service @RequiredArgsConstructor @@ -20,16 +21,18 @@ public class LoginAttemptLogService { private final LoginAttemptLogRepository loginAttemptLogRepository; - public void createLoginAttemptLog(HttpServletRequest httpServletRequest, String memberId, LoginAttemptResult loginAttemptResult) { + @Transactional + public void createLoginAttemptLog(HttpServletRequest request, String memberId, LoginAttemptResult loginAttemptResult) { String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - GeoIpInfo geoIpInfo = GeoIpUtil.getInfoByIp(clientIpAddress); - LoginAttemptLog loginAttemptLog = LoginAttemptLog.create(memberId, httpServletRequest, clientIpAddress, geoIpInfo, loginAttemptResult); + IPResponse ipResponse = HttpReqResUtil.isLocalRequest(clientIpAddress) ? null : IPInfoUtil.getIpInfo(request); + LoginAttemptLog loginAttemptLog = LoginAttemptLog.create(memberId, request, clientIpAddress, ipResponse, loginAttemptResult); loginAttemptLogRepository.save(loginAttemptLog); } + @Transactional(readOnly = true) public PagedResponseDto getLoginAttemptLogs(String memberId, Pageable pageable) { Page loginAttemptLogs = getLoginAttemptByMemberId(pageable, memberId); - return new PagedResponseDto<>(loginAttemptLogs.map(LoginAttemptLogResponseDto::of)); + return new PagedResponseDto<>(loginAttemptLogs.map(LoginAttemptLogResponseDto::toDto)); } private Page getLoginAttemptByMemberId(Pageable pageable, String memberId) { diff --git a/src/main/java/page/clab/api/domain/login/application/LoginService.java b/src/main/java/page/clab/api/domain/login/application/LoginService.java index 2b80b9241..1d474486d 100644 --- a/src/main/java/page/clab/api/domain/login/application/LoginService.java +++ b/src/main/java/page/clab/api/domain/login/application/LoginService.java @@ -28,6 +28,8 @@ import page.clab.api.global.common.slack.application.SlackService; import page.clab.api.global.util.HttpReqResUtil; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j @@ -51,14 +53,15 @@ public class LoginService { private final SlackService slackService; @Transactional - public LoginHeader login(HttpServletRequest httpServletRequest, LoginRequestDto loginRequestDto) throws LoginFaliedException, MemberLockedException { - authenticateAndCheckStatus(httpServletRequest, loginRequestDto); - logLoginAttempt(httpServletRequest, loginRequestDto.getId(), true); - Member member = memberService.getMemberByIdOrThrow(loginRequestDto.getId()); + public LoginHeader login(HttpServletRequest request, LoginRequestDto requestDto) throws LoginFaliedException, MemberLockedException { + authenticateAndCheckStatus(request, requestDto); + logLoginAttempt(request, requestDto.getId(), true); + Member member = memberService.getMemberByIdOrThrow(requestDto.getId()); member.updateLastLoginTime(); - return generateLoginHeader(loginRequestDto.getId()); + return generateLoginHeader(requestDto.getId()); } + @Transactional public TokenHeader authenticator(HttpServletRequest httpServletRequest, TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto) throws LoginFaliedException, MemberLockedException { String memberId = twoFactorAuthenticationRequestDto.getMemberId(); Member loginMember = memberService.getMemberById(memberId); @@ -69,7 +72,7 @@ public TokenHeader authenticator(HttpServletRequest httpServletRequest, TwoFacto TokenInfo tokenInfo = generateAndSaveToken(loginMember); sendAdminLoginNotification(loginMember); - return constructTokenHeader(tokenInfo); + return TokenHeader.create(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS, tokenInfo); } public String resetAuthenticator(String memberId) { @@ -93,7 +96,11 @@ public TokenHeader reissue(HttpServletRequest request) { TokenInfo newTokenInfo = jwtTokenProvider.generateToken(redisToken.getId(), redisToken.getRole()); redisTokenService.saveRedisToken(redisToken.getId(), redisToken.getRole(), newTokenInfo, redisToken.getIp()); - return constructTokenHeader(newTokenInfo); + return TokenHeader.create(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS, newTokenInfo); + } + + public List getCurrentLoggedInUsers() { + return redisTokenService.getCurrentLoggedInUsers(); } private void authenticateAndCheckStatus(HttpServletRequest httpServletRequest, LoginRequestDto loginRequestDto) throws LoginFaliedException, MemberLockedException { @@ -117,18 +124,18 @@ private void logLoginAttempt(HttpServletRequest request, String memberId, boolea private LoginHeader generateLoginHeader(String memberId) { if (!authenticatorService.isAuthenticatorExist(memberId)) { String secretKey = authenticatorService.generateSecretKey(memberId); - return new LoginHeader(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS.getHttpStatus(), secretKey); + return LoginHeader.create(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS, secretKey); } - return new LoginHeader(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS.getHttpStatus(), null); + return LoginHeader.create(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS, null); } - private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest httpServletRequest) throws MemberLockedException, LoginFaliedException { + private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request) throws MemberLockedException, LoginFaliedException { if (!authenticatorService.isAuthenticatorValid(memberId, totp)) { - loginAttemptLogService.createLoginAttemptLog(httpServletRequest, memberId, LoginAttemptResult.FAILURE); - accountLockInfoService.handleLoginFailure(httpServletRequest, memberId); + loginAttemptLogService.createLoginAttemptLog(request, memberId, LoginAttemptResult.FAILURE); + accountLockInfoService.handleLoginFailure(request, memberId); throw new LoginFaliedException("잘못된 인증번호입니다."); } - loginAttemptLogService.createLoginAttemptLog(httpServletRequest, memberId, LoginAttemptResult.TOTP); + loginAttemptLogService.createLoginAttemptLog(request, memberId, LoginAttemptResult.TOTP); } private TokenInfo generateAndSaveToken(Member member) { @@ -160,12 +167,4 @@ private void validateToken(RedisToken redisToken) { } } - private TokenHeader constructTokenHeader(TokenInfo newTokenInfo) { - return TokenHeader.builder() - .status(ClabAuthResponseStatus.AUTHENTICATION_SUCCESS.getHttpStatus()) - .accessToken(newTokenInfo.getAccessToken()) - .refreshToken(newTokenInfo.getRefreshToken()) - .build(); - } - } diff --git a/src/main/java/page/clab/api/domain/login/application/RedisTokenService.java b/src/main/java/page/clab/api/domain/login/application/RedisTokenService.java index 48c78280e..72dfa0cf1 100644 --- a/src/main/java/page/clab/api/domain/login/application/RedisTokenService.java +++ b/src/main/java/page/clab/api/domain/login/application/RedisTokenService.java @@ -7,6 +7,11 @@ import page.clab.api.domain.login.dto.response.TokenInfo; import page.clab.api.domain.member.domain.Role; import page.clab.api.global.auth.exception.TokenNotFoundException; +import page.clab.api.global.auth.jwt.JwtTokenProvider; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; @Service @RequiredArgsConstructor @@ -14,6 +19,8 @@ public class RedisTokenService { private final RedisTokenRepository redisTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + public RedisToken getRedisTokenByAccessToken(String accessToken) { return redisTokenRepository.findByAccessToken(accessToken) .orElseThrow(() -> new TokenNotFoundException("존재하지 않는 토큰입니다.")); @@ -24,6 +31,15 @@ public RedisToken getRedisTokenByRefreshToken(String refreshToken) { .orElseThrow(() -> new TokenNotFoundException("존재하지 않는 토큰입니다.")); } + public List getCurrentLoggedInUsers() { + Iterable iterableTokens = redisTokenRepository.findAll(); + return StreamSupport.stream(iterableTokens.spliterator(), false) + .filter(redisToken -> jwtTokenProvider.validateTokenSilently(redisToken.getAccessToken())) + .map(RedisToken::getId) + .distinct() + .collect(Collectors.toList()); + } + public void saveRedisToken(String memberId, Role role, TokenInfo tokenInfo, String ip) { RedisToken redisToken = RedisToken.create(memberId, role, ip, tokenInfo); redisTokenRepository.save(redisToken); diff --git a/src/main/java/page/clab/api/domain/login/domain/AccountLockInfo.java b/src/main/java/page/clab/api/domain/login/domain/AccountLockInfo.java index 579cd4c68..9ef54330a 100644 --- a/src/main/java/page/clab/api/domain/login/domain/AccountLockInfo.java +++ b/src/main/java/page/clab/api/domain/login/domain/AccountLockInfo.java @@ -6,12 +6,14 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDateTime; @@ -19,9 +21,9 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class AccountLockInfo { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class AccountLockInfo extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -37,6 +39,15 @@ public class AccountLockInfo { private LocalDateTime lockUntil; + public static AccountLockInfo create(Member member) { + return AccountLockInfo.builder() + .member(member) + .loginFailCount(0L) + .isLock(false) + .lockUntil(null) + .build(); + } + public void banPermanently() { this.isLock = true; this.lockUntil = LocalDateTime.of(9999, 12, 31, 23, 59); diff --git a/src/main/java/page/clab/api/domain/login/domain/Authenticator.java b/src/main/java/page/clab/api/domain/login/domain/Authenticator.java index d92e5e04e..e084f6cfb 100644 --- a/src/main/java/page/clab/api/domain/login/domain/Authenticator.java +++ b/src/main/java/page/clab/api/domain/login/domain/Authenticator.java @@ -3,13 +3,19 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import page.clab.api.global.common.domain.BaseEntity; @Entity @Getter -@NoArgsConstructor -public class Authenticator { +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Authenticator extends BaseEntity { @Id private String memberId; @@ -17,13 +23,11 @@ public class Authenticator { @Column(nullable = false) private String secretKey; - private Authenticator(String memberId, String secretKey) { - this.memberId = memberId; - this.secretKey = secretKey; - } - public static Authenticator create(String memberId, String secretKey) { - return new Authenticator(memberId, secretKey); + return Authenticator.builder() + .memberId(memberId) + .secretKey(secretKey) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/login/domain/GeoIpInfo.java b/src/main/java/page/clab/api/domain/login/domain/GeoIpInfo.java deleted file mode 100644 index bc5ace29b..000000000 --- a/src/main/java/page/clab/api/domain/login/domain/GeoIpInfo.java +++ /dev/null @@ -1,26 +0,0 @@ -package page.clab.api.domain.login.domain; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class GeoIpInfo { - - private String location; - - private String city; - - private String country; - - private Double latitude; - - private Double longitude; - -} diff --git a/src/main/java/page/clab/api/domain/login/domain/LoginAttemptLog.java b/src/main/java/page/clab/api/domain/login/domain/LoginAttemptLog.java index 49003714f..f6566a517 100644 --- a/src/main/java/page/clab/api/domain/login/domain/LoginAttemptLog.java +++ b/src/main/java/page/clab/api/domain/login/domain/LoginAttemptLog.java @@ -1,5 +1,6 @@ package page.clab.api.domain.login.domain; +import io.ipinfo.api.model.IPResponse; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -7,11 +8,13 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.servlet.http.HttpServletRequest; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDateTime; @@ -19,9 +22,9 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class LoginAttemptLog { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class LoginAttemptLog extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -40,12 +43,12 @@ public class LoginAttemptLog { private LocalDateTime loginAttemptTime; - public static LoginAttemptLog create(String memberId, HttpServletRequest httpServletRequest, String ipAddress, GeoIpInfo geoIpInfo, LoginAttemptResult loginAttemptResult) { + public static LoginAttemptLog create(String memberId, HttpServletRequest httpServletRequest, String ipAddress, IPResponse ipResponse, LoginAttemptResult loginAttemptResult) { return LoginAttemptLog.builder() .memberId(memberId) .userAgent(httpServletRequest.getHeader("User-Agent")) .ipAddress(ipAddress) - .location(geoIpInfo.getLocation()) + .location(ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity()) .loginAttemptResult(loginAttemptResult) .loginAttemptTime(LocalDateTime.now()) .build(); diff --git a/src/main/java/page/clab/api/domain/login/domain/RedisQRKey.java b/src/main/java/page/clab/api/domain/login/domain/RedisQRKey.java index be05a91c3..415ab3f77 100644 --- a/src/main/java/page/clab/api/domain/login/domain/RedisQRKey.java +++ b/src/main/java/page/clab/api/domain/login/domain/RedisQRKey.java @@ -1,6 +1,7 @@ package page.clab.api.domain.login.domain; import jakarta.persistence.Column; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -11,8 +12,8 @@ @Getter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @RedisHash(value = "QRCodeKey", timeToLive = 60 * 3) public class RedisQRKey { diff --git a/src/main/java/page/clab/api/domain/login/domain/RedisToken.java b/src/main/java/page/clab/api/domain/login/domain/RedisToken.java index 48b570355..3be84a176 100644 --- a/src/main/java/page/clab/api/domain/login/domain/RedisToken.java +++ b/src/main/java/page/clab/api/domain/login/domain/RedisToken.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Id; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -15,8 +16,8 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @RedisHash(value = "refresh", timeToLive = 60 * 60 * 24 * 14) public class RedisToken { diff --git a/src/main/java/page/clab/api/domain/login/dto/request/LoginRequestDto.java b/src/main/java/page/clab/api/domain/login/dto/request/LoginRequestDto.java index 0f8b93071..c3160f727 100644 --- a/src/main/java/page/clab/api/domain/login/dto/request/LoginRequestDto.java +++ b/src/main/java/page/clab/api/domain/login/dto/request/LoginRequestDto.java @@ -2,19 +2,18 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; -@Data +@Getter +@Setter public class LoginRequestDto { @NotNull(message = "{notNull.login.id}") - @Size(min = 1, message = "{size.login.id}") @Schema(description = "학번", example = "202312000", required = true) private String id; @NotNull(message = "{notNull.login.password}") - @Size(min = 1, message = "{size.login.password}") @Schema(description = "비밀번호", example = "1234", required = true) private String password; diff --git a/src/main/java/page/clab/api/domain/login/dto/request/TwoFactorAuthenticationRequestDto.java b/src/main/java/page/clab/api/domain/login/dto/request/TwoFactorAuthenticationRequestDto.java index f00716719..feb77992e 100644 --- a/src/main/java/page/clab/api/domain/login/dto/request/TwoFactorAuthenticationRequestDto.java +++ b/src/main/java/page/clab/api/domain/login/dto/request/TwoFactorAuthenticationRequestDto.java @@ -4,11 +4,9 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import lombok.ToString; @Getter @Setter -@ToString public class TwoFactorAuthenticationRequestDto { @NotNull(message = "{notNull.twoFactorAuthenticationRequestDto.memberId}") diff --git a/src/main/java/page/clab/api/domain/login/dto/response/AccountLockInfoResponseDto.java b/src/main/java/page/clab/api/domain/login/dto/response/AccountLockInfoResponseDto.java index b009a9b7c..e7fbf00fe 100644 --- a/src/main/java/page/clab/api/domain/login/dto/response/AccountLockInfoResponseDto.java +++ b/src/main/java/page/clab/api/domain/login/dto/response/AccountLockInfoResponseDto.java @@ -1,16 +1,10 @@ package page.clab.api.domain.login.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.login.domain.AccountLockInfo; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class AccountLockInfoResponseDto { @@ -18,7 +12,7 @@ public class AccountLockInfoResponseDto { private String name; - public static AccountLockInfoResponseDto of(AccountLockInfo accountLockInfo) { + public static AccountLockInfoResponseDto toDto(AccountLockInfo accountLockInfo) { return AccountLockInfoResponseDto.builder() .id(accountLockInfo.getMember().getId()) .name(accountLockInfo.getMember().getName()) diff --git a/src/main/java/page/clab/api/domain/login/dto/response/LoginAttemptLogResponseDto.java b/src/main/java/page/clab/api/domain/login/dto/response/LoginAttemptLogResponseDto.java index 9ba638256..c9aa7fe40 100644 --- a/src/main/java/page/clab/api/domain/login/dto/response/LoginAttemptLogResponseDto.java +++ b/src/main/java/page/clab/api/domain/login/dto/response/LoginAttemptLogResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.login.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.login.domain.LoginAttemptLog; import page.clab.api.domain.login.domain.LoginAttemptResult; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class LoginAttemptLogResponseDto { @@ -29,8 +23,15 @@ public class LoginAttemptLogResponseDto { private LocalDateTime loginAttemptTime; - public static LoginAttemptLogResponseDto of(LoginAttemptLog loginAttemptLog) { - return ModelMapperUtil.getModelMapper().map(loginAttemptLog, LoginAttemptLogResponseDto.class); + public static LoginAttemptLogResponseDto toDto(LoginAttemptLog loginAttemptLog) { + return LoginAttemptLogResponseDto.builder() + .id(loginAttemptLog.getId()) + .userAgent(loginAttemptLog.getUserAgent()) + .ipAddress(loginAttemptLog.getIpAddress()) + .location(loginAttemptLog.getLocation()) + .loginAttemptResult(loginAttemptLog.getLoginAttemptResult()) + .loginAttemptTime(loginAttemptLog.getLoginAttemptTime()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/login/dto/response/LoginHeader.java b/src/main/java/page/clab/api/domain/login/dto/response/LoginHeader.java index 598bfda59..8b93824e5 100644 --- a/src/main/java/page/clab/api/domain/login/dto/response/LoginHeader.java +++ b/src/main/java/page/clab/api/domain/login/dto/response/LoginHeader.java @@ -2,23 +2,27 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.global.auth.domain.ClabAuthResponseStatus; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class LoginHeader { private int status; private String secretKey; + private LoginHeader(int status, String secretKey) { + this.status = status; + this.secretKey = secretKey; + } + + public static LoginHeader create(ClabAuthResponseStatus status, String secretKey) { + return new LoginHeader(status.getHttpStatus(), secretKey); + } + public String toJson() { Gson gson = new GsonBuilder().serializeNulls().create(); return gson.toJson(this); diff --git a/src/main/java/page/clab/api/domain/login/dto/response/TokenHeader.java b/src/main/java/page/clab/api/domain/login/dto/response/TokenHeader.java index 913658faa..60c05abcc 100644 --- a/src/main/java/page/clab/api/domain/login/dto/response/TokenHeader.java +++ b/src/main/java/page/clab/api/domain/login/dto/response/TokenHeader.java @@ -2,17 +2,12 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.global.auth.domain.ClabAuthResponseStatus; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class TokenHeader { private int status; @@ -21,6 +16,16 @@ public class TokenHeader { private String refreshToken; + private TokenHeader(int status, String accessToken, String refreshToken) { + this.status = status; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public static TokenHeader create(ClabAuthResponseStatus status, TokenInfo tokenInfo) { + return new TokenHeader(status.getHttpStatus(), tokenInfo.getAccessToken(), tokenInfo.getRefreshToken()); + } + public String toJson() { Gson gson = new GsonBuilder().serializeNulls().create(); return gson.toJson(this); diff --git a/src/main/java/page/clab/api/domain/login/dto/response/TokenInfo.java b/src/main/java/page/clab/api/domain/login/dto/response/TokenInfo.java index 141f3601b..19e35f431 100644 --- a/src/main/java/page/clab/api/domain/login/dto/response/TokenInfo.java +++ b/src/main/java/page/clab/api/domain/login/dto/response/TokenInfo.java @@ -1,16 +1,24 @@ package page.clab.api.domain.login.dto.response; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; +import lombok.Getter; +@Getter @Builder -@Data -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class TokenInfo { private String accessToken; private String refreshToken; + public static TokenInfo create(String accessToken, String refreshToken) { + return TokenInfo.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/login/exception/DuplicateLoginException.java b/src/main/java/page/clab/api/domain/login/exception/DuplicateLoginException.java deleted file mode 100644 index 02a207420..000000000 --- a/src/main/java/page/clab/api/domain/login/exception/DuplicateLoginException.java +++ /dev/null @@ -1,15 +0,0 @@ -package page.clab.api.domain.login.exception; - -public class DuplicateLoginException extends RuntimeException { - - private static final String DEFAULT_MESSAGE = "중복 로그인이 감지되었습니다."; - - public DuplicateLoginException() { - super(DEFAULT_MESSAGE); - } - - public DuplicateLoginException(String s) { - super(s); - } - -} diff --git a/src/main/java/page/clab/api/domain/member/api/MemberCloudController.java b/src/main/java/page/clab/api/domain/member/api/MemberCloudController.java index cf8ccfce6..d4b8beb72 100644 --- a/src/main/java/page/clab/api/domain/member/api/MemberCloudController.java +++ b/src/main/java/page/clab/api/domain/member/api/MemberCloudController.java @@ -15,12 +15,12 @@ import page.clab.api.domain.member.application.MemberCloudService; import page.clab.api.domain.member.dto.response.CloudUsageInfo; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.file.dto.response.FileInfo; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/member-clouds") +@RequestMapping("/api/v1/member-clouds") @RequiredArgsConstructor @Tag(name = "Member Cloud", description = "멤버 클라우드") @Slf4j @@ -31,44 +31,38 @@ public class MemberCloudController { @Operation(summary = "[S] 모든 멤버의 클라우드 사용량 조회", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @GetMapping("") - public ResponseModel getAllCloudUsages( + public ApiResponse> getAllCloudUsages( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto cloudUsageInfos = memberCloudService.getAllCloudUsages(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(cloudUsageInfos); - return responseModel; + return ApiResponse.success(cloudUsageInfos); } @Operation(summary = "[U] 멤버의 클라우드 사용량 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/{memberId}") - public ResponseModel getCloudUsageByMemberId( + public ApiResponse getCloudUsageByMemberId( @PathVariable(name = "memberId") String memberId ) throws PermissionDeniedException { CloudUsageInfo usage = memberCloudService.getCloudUsageByMemberId(memberId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(usage); - return responseModel; + return ApiResponse.success(usage); } @Operation(summary = "[U] 멤버 업로드 파일 리스트 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 정보만 조회 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/files/{memberId}") - public ResponseModel getMemberUploadedFiles( + public ApiResponse> getMemberUploadedFiles( @PathVariable(name = "memberId") String memberId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto files = memberCloudService.getFilesInMemberDirectory(memberId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(files); - return responseModel; + return ApiResponse.success(files); } } diff --git a/src/main/java/page/clab/api/domain/member/api/MemberController.java b/src/main/java/page/clab/api/domain/member/api/MemberController.java index 3c98bb834..2b6b4e4e2 100644 --- a/src/main/java/page/clab/api/domain/member/api/MemberController.java +++ b/src/main/java/page/clab/api/domain/member/api/MemberController.java @@ -24,14 +24,14 @@ import page.clab.api.domain.member.dto.response.MemberResponseDto; import page.clab.api.domain.member.dto.response.MyProfileResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; -import page.clab.api.global.common.verificationCode.dto.request.VerificationCodeRequestDto; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.verification.dto.request.VerificationRequestDto; import page.clab.api.global.exception.PermissionDeniedException; import java.util.List; @RestController -@RequestMapping("/members") +@RequestMapping("/api/v1/members") @RequiredArgsConstructor @Tag(name = "Member", description = "멤버") @Slf4j @@ -42,44 +42,38 @@ public class MemberController { @Operation(summary = "[S] 신규 멤버 생성", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("") - public ResponseModel createMember( - @Valid @RequestBody MemberRequestDto memberRequestDto + public ApiResponse createMember( + @Valid @RequestBody MemberRequestDto requestDto ) { - String id = memberService.createMember(memberRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + String id = memberService.createMember(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[S] 모집 단위별 합격자 멤버 통합 생성", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("/{recruitmentId}") - public ResponseModel createMembersByRecruitmentId( + public ApiResponse> createMembersByRecruitmentId( @PathVariable(name = "recruitmentId") Long recruitmentId ) { List ids = memberService.createMembersByRecruitmentId(recruitmentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(ids); - return responseModel; + return ApiResponse.success(ids); } @Operation(summary = "[S] 모집 단위별 합격자 멤버 개별 생성", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("/{recruitmentId}/{memberId}") - public ResponseModel createMemberByRecruitmentId( + public ApiResponse createMemberByRecruitmentId( @PathVariable(name = "recruitmentId") Long recruitmentId, @PathVariable(name = "memberId") String memberId ) { String id = memberService.createMemberByRecruitmentId(recruitmentId, memberId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[A] 멤버 정보 조회(멤버 ID, 이름 기준)", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getMembersByConditions( + public ApiResponse> getMembersByConditions( @RequestParam(name = "id", required = false) String id, @RequestParam(name = "name", required = false) String name, @RequestParam(name = "page", defaultValue = "0") int page, @@ -87,68 +81,58 @@ public ResponseModel getMembersByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto members = memberService.getMembersByConditions(id, name, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(members); - return responseModel; + return ApiResponse.success(members); } @Operation(summary = "[U] 내 프로필 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my-profile") - public ResponseModel getMyProfile(){ - MyProfileResponseDto memberResponseDto = memberService.getMyProfile(); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(memberResponseDto); - return responseModel; + public ApiResponse getMyProfile() { + MyProfileResponseDto myProfile = memberService.getMyProfile(); + return ApiResponse.success(myProfile); } @Operation(summary = "이달의 생일자 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/birthday") - public ResponseModel getBirthdaysThisMonth( + public ApiResponse> getBirthdaysThisMonth( @RequestParam(name = "month") int month, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto birthdayMembers = memberService.getBirthdaysThisMonth(month, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(birthdayMembers); - return responseModel; + return ApiResponse.success(birthdayMembers); } @Operation(summary = "[U] 멤버 정보 수정", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{memberId}") - public ResponseModel updateMemberInfoByMember( + public ApiResponse updateMemberInfoByMember( @PathVariable(name = "memberId") String memberId, - @Valid @RequestBody MemberUpdateRequestDto memberUpdateRequestDto + @Valid @RequestBody MemberUpdateRequestDto requestDto ) throws PermissionDeniedException { - String id = memberService.updateMemberInfo(memberId, memberUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + String id = memberService.updateMemberInfo(memberId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "멤버 비밀번호 재발급 요청", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @PostMapping("/password-reset-requests") - public ResponseModel requestResetMemberPassword( - @Valid @RequestBody MemberResetPasswordRequestDto memberResetPasswordRequestDto + public ApiResponse requestResetMemberPassword( + @Valid @RequestBody MemberResetPasswordRequestDto requestDto ) { - memberService.requestResetMemberPassword(memberResetPasswordRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + String id = memberService.requestResetMemberPassword(requestDto); + return ApiResponse.success(id); } @Operation(summary = "멤버 비밀번호 재발급 인증", description = "ROLE_ANONYMOUS 이상의 권한이 필요함") @PostMapping("/password-reset-verifications") - public ResponseModel verifyResetMemberPassword( - @Valid @RequestBody VerificationCodeRequestDto verificationCodeRequestDto + public ApiResponse verifyResetMemberPassword( + @Valid @RequestBody VerificationRequestDto requestDto ) { - memberService.verifyResetMemberPassword(verificationCodeRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + String id = memberService.verifyResetMemberPassword(requestDto); + return ApiResponse.success(id); } } diff --git a/src/main/java/page/clab/api/domain/member/application/MemberCloudService.java b/src/main/java/page/clab/api/domain/member/application/MemberCloudService.java index a7e1a660d..eff1aa7de 100644 --- a/src/main/java/page/clab/api/domain/member/application/MemberCloudService.java +++ b/src/main/java/page/clab/api/domain/member/application/MemberCloudService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.member.dao.MemberRepository; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.member.dto.response.CloudUsageInfo; @@ -29,31 +30,34 @@ public class MemberCloudService { @Value("${resource.file.path}") private String filePath; + @Transactional(readOnly = true) public PagedResponseDto getAllCloudUsages(Pageable pageable) { Page members = memberRepository.findAllByOrderByCreatedAtDesc(pageable); - return new PagedResponseDto<>(members.map(this::getCloudUsageForMember).getContent(), pageable, members.getSize()); + return new PagedResponseDto<>(members.map(this::getCloudUsageForMember)); } + @Transactional(readOnly = true) public CloudUsageInfo getCloudUsageByMemberId(String memberId) throws PermissionDeniedException { Member currentMember = getCurrentMember(); Member targetMember = validateMemberExistence(memberId); targetMember.validateAccessPermissionForCloud(currentMember); File directory = getMemberDirectory(targetMember.getId()); long usage = FileSystemUtil.calculateDirectorySize(directory); - return new CloudUsageInfo(targetMember.getId(), usage); + return CloudUsageInfo.create(targetMember.getId(), usage); } + @Transactional(readOnly = true) public PagedResponseDto getFilesInMemberDirectory(String memberId, Pageable pageable) { validateMemberExistence(memberId); File directory = getMemberDirectory(memberId); List files = FileSystemUtil.getFilesInDirectory(directory); - return new PagedResponseDto<>(files.stream().map(FileInfo::of).toList(), pageable, files.size()); + return new PagedResponseDto<>(files.stream().map(FileInfo::toDto).toList(), pageable, files.size()); } private CloudUsageInfo getCloudUsageForMember(Member member) { File directory = getMemberDirectory(member.getId()); long usage = FileSystemUtil.calculateDirectorySize(directory); - return new CloudUsageInfo(member.getId(), usage); + return CloudUsageInfo.create(member.getId(), usage); } private Member validateMemberExistence(String memberId) { diff --git a/src/main/java/page/clab/api/domain/member/application/MemberService.java b/src/main/java/page/clab/api/domain/member/application/MemberService.java index 68ef672ab..3691544a4 100644 --- a/src/main/java/page/clab/api/domain/member/application/MemberService.java +++ b/src/main/java/page/clab/api/domain/member/application/MemberService.java @@ -1,6 +1,5 @@ package page.clab.api.domain.member.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +8,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.application.dao.ApplicationRepository; import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.application.exception.NotApprovedApplicationException; @@ -28,16 +28,18 @@ import page.clab.api.global.auth.util.AuthUtil; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.common.email.application.EmailService; -import page.clab.api.global.common.verificationCode.application.VerificationCodeService; -import page.clab.api.global.common.verificationCode.domain.VerificationCode; -import page.clab.api.global.common.verificationCode.dto.request.VerificationCodeRequestDto; +import page.clab.api.global.common.file.application.FileService; +import page.clab.api.global.common.file.dto.request.DeleteFileRequestDto; +import page.clab.api.global.common.verification.application.VerificationService; +import page.clab.api.global.common.verification.domain.Verification; +import page.clab.api.global.common.verification.dto.request.VerificationRequestDto; import page.clab.api.global.exception.InvalidInformationException; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; import java.time.LocalDate; import java.util.List; -import java.util.Objects; import java.util.concurrent.CompletableFuture; @Service @@ -45,27 +47,37 @@ @Slf4j public class MemberService { - private final MemberRepository memberRepository; + private final VerificationService verificationService; - private final PasswordEncoder passwordEncoder; + private final ValidationService validationService; - private final VerificationCodeService verificationCodeService; + private EmailService emailService; private final ApplicationRepository applicationRepository; private final PositionRepository positionRepository; - private EmailService emailService; + private final MemberRepository memberRepository; + + private final PasswordEncoder passwordEncoder; + + private FileService fileService; @Autowired public void setEmailService(@Lazy EmailService emailService) { this.emailService = emailService; } + @Autowired + public void setFileServie(@Lazy FileService fileService) { + this.fileService = fileService; + } + @Transactional - public String createMember(MemberRequestDto memberRequestDto) { - checkMemberUniqueness(memberRequestDto); - Member member = Member.of(memberRequestDto); + public String createMember(MemberRequestDto requestDto) { + checkMemberUniqueness(requestDto); + Member member = MemberRequestDto.toEntity(requestDto); + validationService.checkValid(member); setupMemberPassword(member); memberRepository.save(member); createPositionByMember(member); @@ -89,46 +101,53 @@ public String createMemberByRecruitmentId(Long recruitmentId, String memberId) { public List getMembers() { List members = memberRepository.findAll(); return members.stream() - .map(MemberResponseDto::of) + .map(MemberResponseDto::toDto) .toList(); } + @Transactional(readOnly = true) public PagedResponseDto getMembersByConditions(String id, String name, Pageable pageable) { Page members = memberRepository.findByConditions(id, name, pageable); - return new PagedResponseDto<>(members.map(MemberResponseDto::of)); + return new PagedResponseDto<>(members.map(MemberResponseDto::toDto)); } + @Transactional(readOnly = true) + public MyProfileResponseDto getMyProfile() { + Member currentMember = getCurrentMember(); + return MyProfileResponseDto.toDto(currentMember); + } + + @Transactional(readOnly = true) public PagedResponseDto getBirthdaysThisMonth(int month, Pageable pageable) { Page birthdayMembers = memberRepository.findBirthdaysThisMonth(month, pageable); - return new PagedResponseDto<>(birthdayMembers.map(MemberBirthdayResponseDto::of)); + return new PagedResponseDto<>(birthdayMembers.map(MemberBirthdayResponseDto::toDto)); } - public String updateMemberInfo(String memberId, MemberUpdateRequestDto memberUpdateRequestDto) throws PermissionDeniedException { + @Transactional + public String updateMemberInfo(String memberId, MemberUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = getCurrentMember(); Member member = getMemberByIdOrThrow(memberId); member.validateAccessPermission(currentMember); - member.update(memberUpdateRequestDto, passwordEncoder); + updateMember(requestDto, member); + validationService.checkValid(member); return memberRepository.save(member).getId(); } @Transactional - public void requestResetMemberPassword(MemberResetPasswordRequestDto memberResetPasswordRequestDto) { - Member member = validateResetPasswordRequest(memberResetPasswordRequestDto); - String code = verificationCodeService.generateVerificationCode(); - verificationCodeService.saveVerificationCode(member.getId(), code); + public String requestResetMemberPassword(MemberResetPasswordRequestDto requestDto) { + Member member = validateResetPasswordRequest(requestDto); + String code = verificationService.generateVerificationCode(); + verificationService.saveVerificationCode(member.getId(), code); emailService.sendPasswordResetEmail(member, code); + return member.getId(); } @Transactional - public void verifyResetMemberPassword(VerificationCodeRequestDto verificationCodeRequestDto) { - Member member = getMemberByIdOrThrow(verificationCodeRequestDto.getMemberId()); - VerificationCode verificationCode = verificationCodeService.validateVerificationCode(verificationCodeRequestDto, member); - updateMemberPasswordWithVerificationCode(verificationCode.getVerificationCode(), member); - } - - public MyProfileResponseDto getMyProfile() { - Member currentMember = getCurrentMember(); - return MyProfileResponseDto.of(currentMember); + public String verifyResetMemberPassword(VerificationRequestDto requestDto) { + Member member = getMemberByIdOrThrow(requestDto.getMemberId()); + Verification verification = verificationService.validateVerificationCode(requestDto, member); + updateMemberPasswordWithVerificationCode(verification.getVerificationCode(), member); + return member.getId(); } public Member getMemberById(String memberId) { @@ -184,7 +203,8 @@ private String createMemberFromApplication(Application application) { } private Member createMemberByApplication(Application application) { - Member member = Member.of(application); + Member member = Application.toMember(application); + validationService.checkValid(member); Member existingMember = memberRepository.findById(member.getId()).orElse(null); if (existingMember != null) { return existingMember; @@ -195,7 +215,7 @@ private Member createMemberByApplication(Application application) { } private void setRandomPasswordAndSendEmail(Member member) { - String password = verificationCodeService.generateVerificationCode(); + String password = verificationService.generateVerificationCode(); member.updatePassword(password, passwordEncoder); CompletableFuture.runAsync(() -> { try { @@ -206,12 +226,12 @@ private void setRandomPasswordAndSendEmail(Member member) { }); } - private void checkMemberUniqueness(MemberRequestDto memberRequestDto) { - if (memberRepository.findById(memberRequestDto.getId()).isPresent()) + private void checkMemberUniqueness(MemberRequestDto requestDto) { + if (memberRepository.findById(requestDto.getId()).isPresent()) throw new AssociatedAccountExistsException("이미 사용 중인 아이디입니다."); - if (memberRepository.findByContact(memberRequestDto.getContact()).isPresent()) + if (memberRepository.findByContact(requestDto.getContact()).isPresent()) throw new AssociatedAccountExistsException("이미 사용 중인 연락처입니다."); - if (memberRepository.findByEmail(memberRequestDto.getEmail()).isPresent()) + if (memberRepository.findByEmail(requestDto.getEmail()).isPresent()) throw new AssociatedAccountExistsException("이미 사용 중인 이메일입니다."); } @@ -219,18 +239,13 @@ public void createPositionByMember(Member member) { if (positionRepository.findByMemberAndYearAndPositionType(member, String.valueOf(LocalDate.now().getYear()), PositionType.MEMBER).isPresent()) { return; } - Position position = Position.builder() - .member(member) - .positionType(PositionType.MEMBER) - .year(String.valueOf(LocalDate.now().getYear())) - .build(); + Position position = Position.create(member); positionRepository.save(position); } - private Member validateResetPasswordRequest(MemberResetPasswordRequestDto memberResetPasswordRequestDto) { - Member member = getMemberByIdOrThrow(memberResetPasswordRequestDto.getId()); - if (!(Objects.equals(member.getName(), memberResetPasswordRequestDto.getName()) - && Objects.equals(member.getEmail(), memberResetPasswordRequestDto.getEmail()))) { + private Member validateResetPasswordRequest(MemberResetPasswordRequestDto requestDto) { + Member member = getMemberByIdOrThrow(requestDto.getId()); + if (!member.isSameName(requestDto.getName()) || !member.isSameEmail(requestDto.getEmail())) { throw new InvalidInformationException("올바르지 않은 정보입니다."); } return member; @@ -238,7 +253,16 @@ private Member validateResetPasswordRequest(MemberResetPasswordRequestDto member private void updateMemberPasswordWithVerificationCode(String verificationCode, Member member) { member.updatePassword(verificationCode, passwordEncoder); - verificationCodeService.deleteVerificationCode(verificationCode); + verificationService.deleteVerificationCode(verificationCode); + } + + private void updateMember(MemberUpdateRequestDto requestDto, Member member) throws PermissionDeniedException { + String previousImageUrl = member.getImageUrl(); + member.update(requestDto, passwordEncoder); + if (requestDto.getImageUrl() != null && requestDto.getImageUrl().isEmpty()) { + member.clearImageUrl(); + fileService.deleteFile(DeleteFileRequestDto.create(previousImageUrl)); + } } public List getAdmins() { diff --git a/src/main/java/page/clab/api/domain/member/domain/Member.java b/src/main/java/page/clab/api/domain/member/domain/Member.java index a3a002a98..9189279c8 100644 --- a/src/main/java/page/clab/api/domain/member/domain/Member.java +++ b/src/main/java/page/clab/api/domain/member/domain/Member.java @@ -10,23 +10,21 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; import org.hibernate.validator.constraints.URL; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; -import page.clab.api.domain.application.domain.Application; import page.clab.api.domain.book.exception.LoanSuspensionException; -import page.clab.api.domain.member.dto.request.MemberRequestDto; import page.clab.api.domain.member.dto.request.MemberUpdateRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; import java.time.LocalDateTime; @@ -38,12 +36,13 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Member implements UserDetails { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Member extends BaseEntity implements UserDetails { @Id - @Column(updatable = false, unique = true, nullable = false) + @Column(nullable = false, updatable = false, unique = true) + @Size(min = 9, max = 9, message = "{size.member.id}") private String id; @JsonIgnore @@ -93,10 +92,6 @@ public class Member implements UserDetails { @Enumerated(EnumType.STRING) private Role role; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - private LocalDateTime lastLoginTime; private LocalDateTime loanSuspensionDate; @@ -136,20 +131,14 @@ public boolean isEnabled() { return true; } - public static Member of(MemberRequestDto memberRequestDto) { - Member member = ModelMapperUtil.getModelMapper().map(memberRequestDto, Member.class); - member.setContact(member.removeHyphensFromContact(member.getContact())); - member.setRole(Role.USER); - return member; + private Member(String id, String password, Role role) { + this.id = id; + this.password = password; + this.role = role; } - public static Member of(Application application) { - Member member = ModelMapperUtil.getModelMapper().map(application, Member.class); - member.setId(application.getStudentId()); - member.setPassword(null); - member.setStudentStatus(StudentStatus.CURRENT); - member.setRole(Role.USER); - return member; + public static Member createUserDetails(Member member) { + return new Member(member.getId(), member.getPassword(), member.getRole()); } public void update(MemberUpdateRequestDto memberUpdateRequestDto, PasswordEncoder passwordEncoder) { @@ -182,6 +171,18 @@ public boolean isSameMember(String memberId) { return id.equals(memberId); } + public boolean isSameName(String memberName) { + return name.equals(memberName); + } + + public boolean isSameEmail(String memberEmail) { + return email.equals(memberEmail); + } + + public boolean isGraduated() { + return studentStatus.equals(StudentStatus.GRADUATED); + } + public boolean isOwner(Member member) { return this.isSameMember(member); } @@ -198,10 +199,6 @@ public void validateAccessPermissionForCloud(Member member) throws PermissionDen } } - public String removeHyphensFromContact(String contact) { - return contact.replaceAll("-", ""); - } - public void updatePassword(String password, PasswordEncoder passwordEncoder) { setPassword(passwordEncoder.encode(password)); } @@ -210,6 +207,10 @@ public void updateLastLoginTime() { lastLoginTime = LocalDateTime.now(); } + public void clearImageUrl() { + this.imageUrl = null; + } + public void checkLoanSuspension() { if (loanSuspensionDate != null && LocalDateTime.now().isBefore(loanSuspensionDate)) { throw new LoanSuspensionException("대출 정지 중입니다. 대출 정지일까지는 책을 대출할 수 없습니다."); diff --git a/src/main/java/page/clab/api/domain/member/domain/Role.java b/src/main/java/page/clab/api/domain/member/domain/Role.java index 889e9c562..4d3388676 100644 --- a/src/main/java/page/clab/api/domain/member/domain/Role.java +++ b/src/main/java/page/clab/api/domain/member/domain/Role.java @@ -14,8 +14,7 @@ public enum Role { private String key; private String description; - // Role을 Long으로 변환 - public Long toLong() { + public Long toRoleLevel() { switch (this) { case USER: return 1L; diff --git a/src/main/java/page/clab/api/domain/member/dto/request/MemberRequestDto.java b/src/main/java/page/clab/api/domain/member/dto/request/MemberRequestDto.java index 198e73d1f..8b99dbcf3 100644 --- a/src/main/java/page/clab/api/domain/member/dto/request/MemberRequestDto.java +++ b/src/main/java/page/clab/api/domain/member/dto/request/MemberRequestDto.java @@ -1,32 +1,21 @@ package page.clab.api.domain.member.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.member.domain.Role; import page.clab.api.domain.member.domain.StudentStatus; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.Contact; import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class MemberRequestDto { @NotNull(message = "{notNull.member.id}") - @Size(min = 9, max = 9, message = "{size.member.id}") @Schema(description = "학번", example = "202310000", required = true) private String id; @@ -34,29 +23,22 @@ public class MemberRequestDto { private String password; @NotNull(message = "{notNull.member.name}") - @Size(min = 1, max = 10, message = "{size.member.name}") @Schema(description = "이름", example = "홍길동", required = true) private String name; @NotNull(message = "{notNull.member.contact}") - @Size(min = 9, max = 13, message = "{size.member.contact}") @Schema(description = "연락처", example = "01012345678", required = true) private String contact; @NotNull(message = "{notNull.member.email}") - @Email(message = "{email.member.email}") - @Size(min = 1, message = "{size.member.email}") @Schema(description = "이메일", example = "clab.coreteam@gamil.com", required = true) private String email; @NotNull(message = "{notNull.member.department}") - @Size(min = 1, message = "{size.member.department}") @Schema(description = "학과", example = "AI컴퓨터공학부", required = true) private String department; @NotNull(message = "{notNull.member.grade}") - @Min(value = 1, message = "{min.member.grade}") - @Max(value = 4, message = "{max.member.grade}") @Schema(description = "학년", example = "1", required = true) private Long grade; @@ -65,16 +47,13 @@ public class MemberRequestDto { private LocalDate birth; @NotNull(message = "{notNull.member.address}") - @Size(min = 1, message = "{size.member.address}") @Schema(description = "주소", example = "경기도 수원시 영통구 광교산로 154-42", required = true) private String address; @NotNull(message = "{notNull.member.interests}") - @Size(min = 1, message = "{size.member.interests}") @Schema(description = "관심 분야", example = "백엔드", required = true) private String interests; - @URL(message = "{url.member.githubUrl}") @Schema(description = "GitHub 주소", example = "https://github.com/kgu-c-lab", required = true) private String githubUrl; @@ -85,8 +64,23 @@ public class MemberRequestDto { @Schema(description = "프로필 이미지", example = "https://www.clab.page/assets/dongmin-860f3a1e.jpeg") private String imageUrl; - public static MemberRequestDto of(Member member) { - return ModelMapperUtil.getModelMapper().map(member, MemberRequestDto.class); + public static Member toEntity(MemberRequestDto requestDto) { + return Member.builder() + .id(requestDto.getId()) + .password(requestDto.getPassword()) + .name(requestDto.getName()) + .contact(Contact.of(requestDto.getContact()).getValue()) + .email(requestDto.getEmail()) + .department(requestDto.getDepartment()) + .grade(requestDto.getGrade()) + .birth(requestDto.getBirth()) + .address(requestDto.getAddress()) + .interests(requestDto.getInterests()) + .githubUrl(requestDto.getGithubUrl()) + .studentStatus(requestDto.getStudentStatus()) + .imageUrl(requestDto.getImageUrl()) + .role(Role.USER) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/member/dto/request/MemberResetPasswordRequestDto.java b/src/main/java/page/clab/api/domain/member/dto/request/MemberResetPasswordRequestDto.java index d2b9ac818..e42bef409 100644 --- a/src/main/java/page/clab/api/domain/member/dto/request/MemberResetPasswordRequestDto.java +++ b/src/main/java/page/clab/api/domain/member/dto/request/MemberResetPasswordRequestDto.java @@ -1,35 +1,23 @@ package page.clab.api.domain.member.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class MemberResetPasswordRequestDto { @NotNull(message = "{notNull.member.id}") - @Size(min = 9, max = 9, message = "{size.member.id}") @Schema(description = "학번", example = "202310000", required = true) private String id; @NotNull(message = "{notNull.member.name}") - @Size(min = 1, max = 10, message = "{size.member.name}") @Schema(description = "이름", example = "홍길동", required = true) private String name; @NotNull(message = "{notNull.member.email}") - @Email(message = "{email.member.email}") - @Size(min = 1, message = "{size.member.email}") @Schema(description = "이메일", example = "clab.coreteam@gamil.com", required = true) private String email; diff --git a/src/main/java/page/clab/api/domain/member/dto/request/MemberUpdateRequestDto.java b/src/main/java/page/clab/api/domain/member/dto/request/MemberUpdateRequestDto.java index 38f756f85..8bd7aefde 100644 --- a/src/main/java/page/clab/api/domain/member/dto/request/MemberUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/member/dto/request/MemberUpdateRequestDto.java @@ -1,57 +1,37 @@ package page.clab.api.domain.member.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.Size; -import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.member.domain.StudentStatus; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class MemberUpdateRequestDto { @Schema(description = "비밀번호", example = "1234") private String password; - @Size(min = 11, max = 11, message = "{size.member.contact}") @Schema(description = "연락처", example = "01012345678") private String contact; - @Email(message = "{email.member.email}") - @Size(min = 1, message = "{size.member.email}") @Schema(description = "이메일", example = "clab.coreteam@gamil.com") private String email; - @Min(value = 1, message = "{min.member.grade}") - @Max(value = 4, message = "{max.member.grade}") @Schema(description = "학년", example = "1") private Long grade; @Schema(description = "생년월일", example = "2004-01-01") private LocalDate birth; - @Size(min = 1, message = "{size.member.address}") @Schema(description = "주소", example = "경기도 수원시 영통구 광교산로 154-42") private String address; - @Size(min = 1, message = "{size.member.interests}") @Schema(description = "관심 분야", example = "백엔드") private String interests; - @URL(message = "{url.member.githubUrl}") @Schema(description = "GitHub 주소", example = "https://github.com/kgu-c-lab") private String githubUrl; @@ -61,8 +41,4 @@ public class MemberUpdateRequestDto { @Schema(description = "프로필 이미지", example = "https://www.clab.page/assets/dongmin-860f3a1e.jpeg") private String imageUrl; - public static MemberUpdateRequestDto of(Member member) { - return ModelMapperUtil.getModelMapper().map(member, MemberUpdateRequestDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/member/dto/response/CloudUsageInfo.java b/src/main/java/page/clab/api/domain/member/dto/response/CloudUsageInfo.java index 097bf7516..0bcd7220f 100644 --- a/src/main/java/page/clab/api/domain/member/dto/response/CloudUsageInfo.java +++ b/src/main/java/page/clab/api/domain/member/dto/response/CloudUsageInfo.java @@ -1,15 +1,9 @@ package page.clab.api.domain.member.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class CloudUsageInfo { @@ -17,4 +11,11 @@ public class CloudUsageInfo { private Long usage; + public static CloudUsageInfo create(String memberId, Long usage) { + return CloudUsageInfo.builder() + .memberId(memberId) + .usage(usage) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/member/dto/response/MemberBirthdayResponseDto.java b/src/main/java/page/clab/api/domain/member/dto/response/MemberBirthdayResponseDto.java index ecdc9e5f0..e1985d827 100644 --- a/src/main/java/page/clab/api/domain/member/dto/response/MemberBirthdayResponseDto.java +++ b/src/main/java/page/clab/api/domain/member/dto/response/MemberBirthdayResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.member.dto.response; -import java.time.LocalDate; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class MemberBirthdayResponseDto { @@ -24,8 +18,13 @@ public class MemberBirthdayResponseDto { private String imageUrl; - public static MemberBirthdayResponseDto of(Member member) { - return ModelMapperUtil.getModelMapper().map(member, MemberBirthdayResponseDto.class); + public static MemberBirthdayResponseDto toDto(Member member) { + return MemberBirthdayResponseDto.builder() + .id(member.getId()) + .name(member.getName()) + .birth(member.getBirth()) + .imageUrl(member.getImageUrl()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/member/dto/response/MemberResponseDto.java b/src/main/java/page/clab/api/domain/member/dto/response/MemberResponseDto.java index 82b5b45b7..6e3f77fd0 100644 --- a/src/main/java/page/clab/api/domain/member/dto/response/MemberResponseDto.java +++ b/src/main/java/page/clab/api/domain/member/dto/response/MemberResponseDto.java @@ -1,21 +1,15 @@ package page.clab.api.domain.member.dto.response; -import java.time.LocalDate; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.member.domain.Role; import page.clab.api.domain.member.domain.StudentStatus; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class MemberResponseDto { @@ -51,8 +45,25 @@ public class MemberResponseDto { private LocalDateTime loanSuspensionDate; - public static MemberResponseDto of(Member member) { - return ModelMapperUtil.getModelMapper().map(member, MemberResponseDto.class); + public static MemberResponseDto toDto(Member member) { + return MemberResponseDto.builder() + .id(member.getId()) + .name(member.getName()) + .contact(member.getContact()) + .email(member.getEmail()) + .department(member.getDepartment()) + .grade(member.getGrade()) + .birth(member.getBirth()) + .address(member.getAddress()) + .interests(member.getInterests()) + .githubUrl(member.getGithubUrl()) + .studentStatus(member.getStudentStatus()) + .imageUrl(member.getImageUrl()) + .role(member.getRole()) + .createdAt(member.getCreatedAt()) + .lastLoginTime(member.getLastLoginTime()) + .loanSuspensionDate(member.getLoanSuspensionDate()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/member/dto/response/MyProfileResponseDto.java b/src/main/java/page/clab/api/domain/member/dto/response/MyProfileResponseDto.java index 2c33db90f..6751a5ae8 100644 --- a/src/main/java/page/clab/api/domain/member/dto/response/MyProfileResponseDto.java +++ b/src/main/java/page/clab/api/domain/member/dto/response/MyProfileResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.member.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.member.domain.StudentStatus; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class MyProfileResponseDto { @@ -31,16 +25,28 @@ public class MyProfileResponseDto { private String githubUrl; + private StudentStatus studentStatus; + private String imageUrl; private Long roleLevel; private LocalDateTime createdAt; - public static MyProfileResponseDto of(Member member) { - MyProfileResponseDto myProfileResponseDto = ModelMapperUtil.getModelMapper().map(member, MyProfileResponseDto.class); - myProfileResponseDto.setRoleLevel(member.getRole().toLong()); - return myProfileResponseDto; + public static MyProfileResponseDto toDto(Member member) { + return MyProfileResponseDto.builder() + .name(member.getName()) + .id(member.getId()) + .interests(member.getInterests()) + .contact(member.getContact()) + .email(member.getEmail()) + .address(member.getAddress()) + .githubUrl(member.getGithubUrl()) + .studentStatus(member.getStudentStatus()) + .imageUrl(member.getImageUrl()) + .roleLevel(member.getRole().toRoleLevel()) + .createdAt(member.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/membershipFee/api/MembershipFeeController.java b/src/main/java/page/clab/api/domain/membershipFee/api/MembershipFeeController.java index 188d35799..2bd53105d 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/api/MembershipFeeController.java +++ b/src/main/java/page/clab/api/domain/membershipFee/api/MembershipFeeController.java @@ -18,15 +18,16 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import page.clab.api.domain.membershipFee.application.MembershipFeeService; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; import page.clab.api.domain.membershipFee.dto.request.MembershipFeeRequestDto; import page.clab.api.domain.membershipFee.dto.request.MembershipFeeUpdateRequestDto; import page.clab.api.domain.membershipFee.dto.response.MembershipFeeResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/membership-fees") +@RequestMapping("/api/v1/membership-fees") @RequiredArgsConstructor @Tag(name = "MembershipFee", description = "회비") @Slf4j @@ -37,57 +38,63 @@ public class MembershipFeeController { @Operation(summary = "[U] 회비 신청", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createMembershipFee( - @Valid @RequestBody MembershipFeeRequestDto MembershipFeeRequestDto + public ApiResponse createMembershipFee( + @Valid @RequestBody MembershipFeeRequestDto requestDto ) { - Long id = membershipFeeService.createMembershipFee(MembershipFeeRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = membershipFeeService.createMembershipFee(requestDto); + return ApiResponse.success(id); } - @Operation(summary = "[U] 회비 정보 조회(멤버 ID, 멤버 이름, 카테고리 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + + @Operation(summary = "[U] 회비 정보 조회(멤버 ID, 멤버 이름, 카테고리, 상태 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + "3개의 파라미터를 자유롭게 조합하여 필터링 가능
" + - "멤버 ID, 멤버 이름, 카테고리 중 하나라도 입력하지 않으면 전체 조회됨") + "멤버 ID, 멤버 이름, 카테고리 중 하나라도 입력하지 않으면 전체 조회됨
" + + "계좌 정보는 관리자 이상의 권한만 조회 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getMembershipFeesByConditions( + public ApiResponse> getMembershipFeesByConditions( @RequestParam(name = "memberId", required = false) String memberId, @RequestParam(name = "memberName", required = false) String memberName, @RequestParam(name = "category", required = false) String category, + @RequestParam(name = "status", required = false) MembershipFeeStatus status, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto MembershipFees = membershipFeeService.getMembershipFeesByConditions(memberId, memberName, category, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(MembershipFees); - return responseModel; + PagedResponseDto membershipFees = membershipFeeService.getMembershipFeesByConditions(memberId, memberName, category, status, pageable); + return ApiResponse.success(membershipFees); } @Operation(summary = "[S] 회비 정보 수정", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PatchMapping("/{membershipFeeId}") - public ResponseModel updateMembershipFee( + public ApiResponse updateMembershipFee( @PathVariable(name = "membershipFeeId") Long membershipFeeId, - @Valid @RequestBody MembershipFeeUpdateRequestDto membershipFeeUpdateRequestDto + @Valid @RequestBody MembershipFeeUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = membershipFeeService.updateMembershipFee(membershipFeeId, membershipFeeUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = membershipFeeService.updateMembershipFee(membershipFeeId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[S] 회비 삭제", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/{membershipFeeId}") - public ResponseModel deleteMembershipFee( + public ApiResponse deleteMembershipFee( @PathVariable(name = "membershipFeeId") Long membershipFeeId ) throws PermissionDeniedException { Long id = membershipFeeService.deleteMembershipFee(membershipFeeId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 회비 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedMembershipFees( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto membershipFees = membershipFeeService.getDeletedMembershipFees(pageable); + return ApiResponse.success(membershipFees); } } diff --git a/src/main/java/page/clab/api/domain/membershipFee/application/MembershipFeeService.java b/src/main/java/page/clab/api/domain/membershipFee/application/MembershipFeeService.java index f0f1f060d..292930d26 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/application/MembershipFeeService.java +++ b/src/main/java/page/clab/api/domain/membershipFee/application/MembershipFeeService.java @@ -4,10 +4,12 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.membershipFee.dao.MembershipFeeRepository; import page.clab.api.domain.membershipFee.domain.MembershipFee; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; import page.clab.api.domain.membershipFee.dto.request.MembershipFeeRequestDto; import page.clab.api.domain.membershipFee.dto.request.MembershipFeeUpdateRequestDto; import page.clab.api.domain.membershipFee.dto.response.MembershipFeeResponseDto; @@ -15,6 +17,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -24,32 +27,49 @@ public class MembershipFeeService { private final NotificationService notificationService; + private final ValidationService validationService; + private final MembershipFeeRepository membershipFeeRepository; - public Long createMembershipFee(MembershipFeeRequestDto membershipFeeRequestDto) { - Member member = memberService.getCurrentMember(); - MembershipFee membershipFee = MembershipFee.create(membershipFeeRequestDto, member); + @Transactional + public Long createMembershipFee(MembershipFeeRequestDto requestDto) { + Member currentMember = memberService.getCurrentMember(); + MembershipFee membershipFee = MembershipFeeRequestDto.toEntity(requestDto, currentMember); + validationService.checkValid(membershipFee); notificationService.sendNotificationToAdmins("새로운 회비 내역이 등록되었습니다."); return membershipFeeRepository.save(membershipFee).getId(); } - public PagedResponseDto getMembershipFeesByConditions(String memberId, String memberName, String category, Pageable pageable) { - Page membershipFeesPage = membershipFeeRepository.findByConditions(memberId, memberName, category, pageable); - return new PagedResponseDto<>(membershipFeesPage.map(MembershipFeeResponseDto::of)); + @Transactional(readOnly = true) + public PagedResponseDto getMembershipFeesByConditions(String memberId, String memberName, String category, MembershipFeeStatus status, Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + boolean isAdminOrSuper = currentMember.isAdminRole(); + Page membershipFeesPage = membershipFeeRepository.findByConditions(memberId, memberName, category, status, pageable); + return new PagedResponseDto<>(membershipFeesPage.map(membershipFee -> MembershipFeeResponseDto.toDto(membershipFee, isAdminOrSuper))); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedMembershipFees(Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + boolean isAdminOrSuper = currentMember.isAdminRole(); + Page membershipFees = membershipFeeRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(membershipFees.map(membershipFee -> MembershipFeeResponseDto.toDto(membershipFee, isAdminOrSuper))); } - public Long updateMembershipFee(Long membershipFeeId, MembershipFeeUpdateRequestDto membershipFeeUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + @Transactional + public Long updateMembershipFee(Long membershipFeeId, MembershipFeeUpdateRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); MembershipFee membershipFee = getMembershipFeeByIdOrThrow(membershipFeeId); - membershipFee.validateAccessPermission(member); - membershipFee.update(membershipFeeUpdateRequestDto); + membershipFee.validateAccessPermission(currentMember); + membershipFee.update(requestDto); + validationService.checkValid(membershipFee); return membershipFeeRepository.save(membershipFee).getId(); } public Long deleteMembershipFee(Long membershipFeeId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); MembershipFee membershipFee = getMembershipFeeByIdOrThrow(membershipFeeId); - membershipFee.validateAccessPermission(member); + membershipFee.validateAccessPermission(currentMember); membershipFeeRepository.delete(membershipFee); return membershipFee.getId(); } diff --git a/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepository.java b/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepository.java index 31bcfc42f..d15aa92e2 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepository.java +++ b/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.membershipFee.domain.MembershipFee; @@ -10,4 +11,7 @@ public interface MembershipFeeRepository extends JpaRepository findAllByOrderByCreatedAtDesc(Pageable pageable); + @Query(value = "SELECT m.* FROM membership_fee m WHERE m.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryCustom.java b/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryCustom.java index 4ab426d72..d10b1ca11 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryCustom.java +++ b/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryCustom.java @@ -3,9 +3,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import page.clab.api.domain.membershipFee.domain.MembershipFee; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; public interface MembershipFeeRepositoryCustom { - Page findByConditions(String memberId, String memberName, String category, Pageable pageable); + Page findByConditions(String memberId, String memberName, String category, MembershipFeeStatus status, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryImpl.java b/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryImpl.java index 03e7dea71..fcbda24aa 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/membershipFee/dao/MembershipFeeRepositoryImpl.java @@ -9,6 +9,7 @@ import org.springframework.stereotype.Repository; import page.clab.api.domain.member.domain.QMember; import page.clab.api.domain.membershipFee.domain.MembershipFee; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; import page.clab.api.domain.membershipFee.domain.QMembershipFee; import java.util.List; @@ -20,7 +21,7 @@ public class MembershipFeeRepositoryImpl implements MembershipFeeRepositoryCusto private final JPAQueryFactory queryFactory; @Override - public Page findByConditions(String memberId, String memberName, String category, Pageable pageable) { + public Page findByConditions(String memberId, String memberName, String category, MembershipFeeStatus status, Pageable pageable) { QMembershipFee qMembershipFee = QMembershipFee.membershipFee; QMember qMember = QMember.member; BooleanBuilder builder = new BooleanBuilder(); @@ -28,6 +29,7 @@ public Page findByConditions(String memberId, String memberName, if (memberId != null && !memberId.isEmpty()) builder.and(qMembershipFee.applicant.id.eq(memberId)); if (memberName != null && !memberName.isEmpty()) builder.and(qMember.name.eq(memberName)); if (category != null && !category.isEmpty()) builder.and(qMembershipFee.category.eq(category)); + if (status != null) builder.and(qMembershipFee.status.eq(status)); List membershipFees = queryFactory.selectFrom(qMembershipFee) .leftJoin(qMembershipFee.applicant, qMember) diff --git a/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFee.java b/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFee.java index 24d0e10f3..da52ea1d1 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFee.java +++ b/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFee.java @@ -2,34 +2,38 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.member.domain.Member; -import page.clab.api.domain.membershipFee.dto.request.MembershipFeeRequestDto; import page.clab.api.domain.membershipFee.dto.request.MembershipFeeUpdateRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class MembershipFee { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE membership_fee SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class MembershipFee extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -43,30 +47,28 @@ public class MembershipFee { @Size(min = 1, message = "{size.membershipFee.category}") private String category; + private String account; + @Column(nullable = false) private Long amount; - @Column(nullable = false, length = 1000) + @Column(nullable = false) @Size(min = 1, max = 1000, message = "{size.membershipFee.content}") private String content; private String imageUrl; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static MembershipFee create(MembershipFeeRequestDto membershipFeeRequestDto, Member member) { - MembershipFee membershipFee = ModelMapperUtil.getModelMapper().map(membershipFeeRequestDto, MembershipFee.class); - membershipFee.setApplicant(member); - return membershipFee; - } + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private MembershipFeeStatus status; public void update(MembershipFeeUpdateRequestDto membershipFeeUpdateRequestDto) { Optional.ofNullable(membershipFeeUpdateRequestDto.getCategory()).ifPresent(this::setCategory); + Optional.ofNullable(membershipFeeUpdateRequestDto.getAccount()).ifPresent(this::setAccount); Optional.ofNullable(membershipFeeUpdateRequestDto.getAmount()).ifPresent(this::setAmount); Optional.ofNullable(membershipFeeUpdateRequestDto.getContent()).ifPresent(this::setContent); Optional.ofNullable(membershipFeeUpdateRequestDto.getImageUrl()).ifPresent(this::setImageUrl); + Optional.ofNullable(membershipFeeUpdateRequestDto.getStatus()).ifPresent(this::setStatus); } public boolean isOwner(Member member) { diff --git a/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFeeStatus.java b/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFeeStatus.java new file mode 100644 index 000000000..8540a2690 --- /dev/null +++ b/src/main/java/page/clab/api/domain/membershipFee/domain/MembershipFeeStatus.java @@ -0,0 +1,18 @@ +package page.clab.api.domain.membershipFee.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MembershipFeeStatus { + + PENDING("PENDING", "대기"), + HOLD("HOLD", "보류"), + APPROVED("APPROVED", "승인"), + REJECTED("REJECTED", "반려"); + + private String key; + private String description; + +} diff --git a/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeRequestDto.java b/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeRequestDto.java index df6a625a6..f073d7e10 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeRequestDto.java +++ b/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeRequestDto.java @@ -2,41 +2,44 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.membershipFee.domain.MembershipFee; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class MembershipFeeRequestDto { @NotNull(message = "{notNull.membershipFee.category}") - @Size(min = 1, message = "{size.membershipFee.category}") @Schema(description = "카테고리", example = "지출", required = true) private String category; + @Schema(description = "계좌", example = "110-123-456789") + private String account; + @NotNull(message = "{notNull.membershipFee.amount}") @Schema(description = "금액", example = "10000", required = true) private Long amount; @NotNull(message = "{notNull.membershipFee.content}") - @Size(min = 1, max = 1000, message = "{size.membershipFee.content}") @Schema(description = "내용", example = "2023-2 동아리 종강총회", required = true) private String content; @Schema(description = "증빙 사진", example = "https://images.chosun.com/resizer/mcbrEkwTr5YKQZ89QPO9hmdb0iE=/616x0/smart/cloudfront-ap-northeast-1.images.arcpublishing.com/chosun/LPCZYYKZ4FFIJPDD344FSGCLCY.jpg") private String imageUrl; - public static MembershipFeeRequestDto of(MembershipFee membershipFee) { - return ModelMapperUtil.getModelMapper().map(membershipFee, MembershipFeeRequestDto.class); + public static MembershipFee toEntity(MembershipFeeRequestDto requestDto, Member member) { + return MembershipFee.builder() + .applicant(member) + .category(requestDto.getCategory()) + .account(requestDto.getAccount()) + .amount(requestDto.getAmount()) + .content(requestDto.getContent()) + .imageUrl(requestDto.getImageUrl()) + .status(MembershipFeeStatus.PENDING) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeUpdateRequestDto.java b/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeUpdateRequestDto.java index 8940bfe4a..12a5b6de5 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/membershipFee/dto/request/MembershipFeeUpdateRequestDto.java @@ -1,38 +1,30 @@ package page.clab.api.domain.membershipFee.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import page.clab.api.domain.membershipFee.domain.MembershipFee; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class MembershipFeeUpdateRequestDto { - @Size(min = 1, message = "{size.membershipFee.category}") @Schema(description = "카테고리", example = "지출") private String category; + @Schema(description = "계좌", example = "110-123-456789") + private String account; + + @Schema(description = "금액", example = "10000") private Long amount; - @Size(min = 1, max = 1000, message = "{size.membershipFee.content}") @Schema(description = "내용", example = "2023-2 동아리 종강총회") private String content; @Schema(description = "증빙 사진", example = "https://images.chosun.com/resizer/mcbrEkwTr5YKQZ89QPO9hmdb0iE=/616x0/smart/cloudfront-ap-northeast-1.images.arcpublishing.com/chosun/LPCZYYKZ4FFIJPDD344FSGCLCY.jpg") private String imageUrl; - public static MembershipFeeUpdateRequestDto of(MembershipFee membershipFee) { - return ModelMapperUtil.getModelMapper().map(membershipFee, MembershipFeeUpdateRequestDto.class); - } + @Schema(description = "상태", example = "PENDING") + private MembershipFeeStatus status; } diff --git a/src/main/java/page/clab/api/domain/membershipFee/dto/response/MembershipFeeResponseDto.java b/src/main/java/page/clab/api/domain/membershipFee/dto/response/MembershipFeeResponseDto.java index 2e9e058d9..1ec870dd2 100644 --- a/src/main/java/page/clab/api/domain/membershipFee/dto/response/MembershipFeeResponseDto.java +++ b/src/main/java/page/clab/api/domain/membershipFee/dto/response/MembershipFeeResponseDto.java @@ -1,18 +1,13 @@ package page.clab.api.domain.membershipFee.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.membershipFee.domain.MembershipFee; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.membershipFee.domain.MembershipFeeStatus; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class MembershipFeeResponseDto { @@ -24,20 +19,31 @@ public class MembershipFeeResponseDto { private String category; + private String account; + private Long amount; private String content; private String imageUrl; + private MembershipFeeStatus status; + private LocalDateTime createdAt; - public static MembershipFeeResponseDto of(MembershipFee membershipFee) { - MembershipFeeResponseDto membershipFeeResponseDto = ModelMapperUtil.getModelMapper() - .map(membershipFee, MembershipFeeResponseDto.class); - membershipFeeResponseDto.setMemberId(membershipFee.getApplicant().getId()); - membershipFeeResponseDto.setMemberName(membershipFee.getApplicant().getName()); - return membershipFeeResponseDto; + public static MembershipFeeResponseDto toDto(MembershipFee membershipFee, boolean isAdminOrSuper) { + return MembershipFeeResponseDto.builder() + .id(membershipFee.getId()) + .memberId(membershipFee.getApplicant().getId()) + .memberName(membershipFee.getApplicant().getName()) + .category(membershipFee.getCategory()) + .account(isAdminOrSuper ? membershipFee.getAccount() : null) + .amount(membershipFee.getAmount()) + .content(membershipFee.getContent()) + .imageUrl(membershipFee.getImageUrl()) + .status(membershipFee.getStatus()) + .createdAt(membershipFee.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/news/api/NewsController.java b/src/main/java/page/clab/api/domain/news/api/NewsController.java index 3e9f449c4..605c772ff 100644 --- a/src/main/java/page/clab/api/domain/news/api/NewsController.java +++ b/src/main/java/page/clab/api/domain/news/api/NewsController.java @@ -22,11 +22,11 @@ import page.clab.api.domain.news.dto.request.NewsUpdateRequestDto; import page.clab.api.domain.news.dto.response.NewsDetailsResponseDto; import page.clab.api.domain.news.dto.response.NewsResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; @RestController -@RequestMapping("/news") +@RequestMapping("/api/v1/news") @RequiredArgsConstructor @Tag(name = "News", description = "뉴스") @Slf4j @@ -37,13 +37,11 @@ public class NewsController { @Operation(summary = "뉴스 등록", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createNews( - @Valid @RequestBody NewsRequestDto newsRequestDto + public ApiResponse createNews( + @Valid @RequestBody NewsRequestDto requestDto ) { - Long id = newsService.createNews(newsRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = newsService.createNews(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 뉴스 목록 조회(제목, 카테고리 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -51,7 +49,7 @@ public ResponseModel createNews( "제목, 카테고리 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getNewsByConditions( + public ApiResponse> getNewsByConditions( @RequestParam(name = "title", required = false) String title, @RequestParam(name = "category", required = false) String category, @RequestParam(name = "page", defaultValue = "0") int page, @@ -59,46 +57,50 @@ public ResponseModel getNewsByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto news = newsService.getNewsByConditions(title, category, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(news); - return responseModel; + return ApiResponse.success(news); } @Operation(summary = "[U] 뉴스 상세 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/{newsId}") - public ResponseModel getNewsDetails( + public ApiResponse getNewsDetails( @PathVariable(name = "newsId") Long newsId ) { NewsDetailsResponseDto news = newsService.getNewsDetails(newsId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(news); - return responseModel; + return ApiResponse.success(news); } @Operation(summary = "[A] 뉴스 수정", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{newsId}") - public ResponseModel updateNews( + public ApiResponse updateNews( @PathVariable(name = "newsId") Long newsId, - @Valid @RequestBody NewsUpdateRequestDto newsUpdateRequestDto + @Valid @RequestBody NewsUpdateRequestDto requestDto ) { - Long id = newsService.updateNews(newsId, newsUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = newsService.updateNews(newsId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 뉴스 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{newsId}") - public ResponseModel deleteNews( + public ApiResponse deleteNews( @PathVariable(name = "newsId") Long newsId ) { Long id = newsService.deleteNews(newsId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 뉴스 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedNews( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto pagedNews = newsService.getDeletedNews(pageable); + return ApiResponse.success(pagedNews); } } diff --git a/src/main/java/page/clab/api/domain/news/application/NewsService.java b/src/main/java/page/clab/api/domain/news/application/NewsService.java index 672ecce71..aa0f92981 100644 --- a/src/main/java/page/clab/api/domain/news/application/NewsService.java +++ b/src/main/java/page/clab/api/domain/news/application/NewsService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.news.dao.NewsRepository; import page.clab.api.domain.news.domain.News; import page.clab.api.domain.news.dto.request.NewsRequestDto; @@ -11,40 +12,51 @@ import page.clab.api.domain.news.dto.response.NewsDetailsResponseDto; import page.clab.api.domain.news.dto.response.NewsResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.file.application.FileService; -import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.common.file.application.UploadedFileService; import page.clab.api.global.exception.NotFoundException; - -import java.util.List; -import java.util.stream.Collectors; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor public class NewsService { - private final NewsRepository newsRepository; + private final UploadedFileService uploadedFileService; - private final FileService fileService; + private final ValidationService validationService; + + private final NewsRepository newsRepository; - public Long createNews(NewsRequestDto newsRequestDto) { - News news = News.create(newsRequestDto); - attachUploadedFiles(newsRequestDto, news); + @Transactional + public Long createNews(NewsRequestDto requestDto) { + News news = NewsRequestDto.toEntity(requestDto); + validationService.checkValid(news); + news.setUploadedFiles(uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrlList())); return newsRepository.save(news).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getNewsByConditions(String category, String title, Pageable pageable) { Page newsPage = newsRepository.findByConditions(title, category, pageable); - return new PagedResponseDto<>(newsPage.map(NewsResponseDto::of)); + return new PagedResponseDto<>(newsPage.map(NewsResponseDto::toDto)); } + @Transactional(readOnly = true) public NewsDetailsResponseDto getNewsDetails(Long newsId) { News news = getNewsByIdOrThrow(newsId); - return NewsDetailsResponseDto.create(news); + return NewsDetailsResponseDto.toDto(news); } - public Long updateNews(Long newsId, NewsUpdateRequestDto newsUpdateRequestDto) { + @Transactional(readOnly = true) + public PagedResponseDto getDeletedNews(Pageable pageable) { + Page newsPage = newsRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(newsPage.map(NewsDetailsResponseDto::toDto)); + } + + @Transactional + public Long updateNews(Long newsId, NewsUpdateRequestDto requestDto) { News news = getNewsByIdOrThrow(newsId); - news.update(newsUpdateRequestDto); + news.update(requestDto); + validationService.checkValid(news); return newsRepository.save(news).getId(); } @@ -54,16 +66,6 @@ public Long deleteNews(Long newsId) { return news.getId(); } - private void attachUploadedFiles(NewsRequestDto newsRequestDto, News news) { - List fileUrls = newsRequestDto.getFileUrlList(); - if (fileUrls != null) { - List uploadFileList = fileUrls.stream() - .map(fileService::getUploadedFileByUrl) - .collect(Collectors.toList()); - news.setUploadedFiles(uploadFileList); - } - } - public News getNewsByIdOrThrow(Long newsId) { return newsRepository.findById(newsId) .orElseThrow(() -> new NotFoundException("해당 뉴스가 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/news/dao/NewsRepository.java b/src/main/java/page/clab/api/domain/news/dao/NewsRepository.java index b31895bf9..a248bfb79 100644 --- a/src/main/java/page/clab/api/domain/news/dao/NewsRepository.java +++ b/src/main/java/page/clab/api/domain/news/dao/NewsRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.news.domain.News; @@ -13,4 +14,7 @@ public interface NewsRepository extends JpaRepository, NewsRepositor Page findAllByOrderByCreatedAtDesc(Pageable pageable); + @Query(value = "SELECT n.* FROM news n WHERE n.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/news/domain/News.java b/src/main/java/page/clab/api/domain/news/domain/News.java index 3e9467142..fb28b3f04 100644 --- a/src/main/java/page/clab/api/domain/news/domain/News.java +++ b/src/main/java/page/clab/api/domain/news/domain/News.java @@ -11,21 +11,20 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.news.dto.request.NewsRequestDto; import page.clab.api.domain.news.dto.request.NewsUpdateRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.common.file.domain.UploadedFile; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -33,10 +32,12 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE news SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") @Table(indexes = {@Index(name = "idx_article_url", columnList = "articleUrl")}) -public class News { +public class News extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -50,7 +51,7 @@ public class News { @Size(min = 1, message = "{size.news.category}") private String category; - @Column(nullable = false, length = 10000) + @Column(nullable = false) @Size(min = 1, max = 10000, message = "{size.news.content}") private String content; @@ -63,19 +64,11 @@ public class News { @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "news_files") - private List uploadedFiles = new ArrayList<>(); + private List uploadedFiles; @Column(nullable = false) private LocalDate date; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static News create(NewsRequestDto newsRequestDto) { - return ModelMapperUtil.getModelMapper().map(newsRequestDto, News.class); - } - public void update(NewsUpdateRequestDto newsUpdateRequestDto) { Optional.ofNullable(newsUpdateRequestDto.getTitle()).ifPresent(this::setTitle); Optional.ofNullable(newsUpdateRequestDto.getCategory()).ifPresent(this::setCategory); diff --git a/src/main/java/page/clab/api/domain/news/dto/request/NewsRequestDto.java b/src/main/java/page/clab/api/domain/news/dto/request/NewsRequestDto.java index c8a0adb71..793e3234b 100644 --- a/src/main/java/page/clab/api/domain/news/dto/request/NewsRequestDto.java +++ b/src/main/java/page/clab/api/domain/news/dto/request/NewsRequestDto.java @@ -2,38 +2,29 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; -import lombok.ToString; -import org.hibernate.validator.constraints.URL; import page.clab.api.domain.news.domain.News; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; import java.util.List; @Getter @Setter -@ToString public class NewsRequestDto { @NotNull(message = "{notNull.news.title}") - @Size(min = 1, max = 100, message = "{size.news.title}") @Schema(description = "제목", example = "컴퓨터공학과, SW 개발보안 경진대회 최우수상, 우수상 수상", required = true) private String title; @NotNull(message = "{notNull.news.category}") - @Size(min = 1, message = "{size.news.category}") @Schema(description = "카테고리", example = "동아리 소식", required = true) private String category; @NotNull(message = "{notNull.news.content}") - @Size(min = 1, max = 10000, message = "{size.news.content}") @Schema(description = "내용", example = "컴퓨터공학과, SW 개발보안 경진대회 최우수상, 우수상 수상", required = true) private String content; - @URL(message = "{url.news.articleUrl}") @Schema(description = "URL", example = "https://blog.naver.com/kyonggi_love/223199431495", required = true) private String articleUrl; @@ -48,8 +39,15 @@ public class NewsRequestDto { @Schema(description = "날짜", example = "2021-08-31", required = true) private LocalDate date; - public static NewsRequestDto of(News news) { - return ModelMapperUtil.getModelMapper().map(news, NewsRequestDto.class); + public static News toEntity(NewsRequestDto requestDto) { + return News.builder() + .title(requestDto.getTitle()) + .category(requestDto.getCategory()) + .content(requestDto.getContent()) + .articleUrl(requestDto.getArticleUrl()) + .source(requestDto.getSource()) + .date(requestDto.getDate()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/news/dto/request/NewsUpdateRequestDto.java b/src/main/java/page/clab/api/domain/news/dto/request/NewsUpdateRequestDto.java index 4653ed07e..e8d222be9 100644 --- a/src/main/java/page/clab/api/domain/news/dto/request/NewsUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/news/dto/request/NewsUpdateRequestDto.java @@ -1,12 +1,8 @@ package page.clab.api.domain.news.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; -import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.news.domain.News; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; @@ -14,19 +10,15 @@ @Setter public class NewsUpdateRequestDto { - @Size(min = 1, max = 100, message = "{size.news.title}") @Schema(description = "제목", example = "컴퓨터공학과, SW 개발보안 경진대회 최우수상, 우수상 수상") private String title; - @Size(min = 1, message = "{size.news.category}") @Schema(description = "카테고리", example = "동아리 소식") private String category; - @Size(min = 1, max = 10000, message = "{size.news.content}") @Schema(description = "내용", example = "컴퓨터공학과, SW 개발보안 경진대회 최우수상, 우수상 수상") private String content; - @URL(message = "{url.news.articleUrl}") @Schema(description = "URL", example = "https://blog.naver.com/kyonggi_love/223199431495") private String articleUrl; @@ -36,8 +28,4 @@ public class NewsUpdateRequestDto { @Schema(description = "날짜", example = "2021-08-31") private LocalDate date; - public static NewsUpdateRequestDto of(News news) { - return ModelMapperUtil.getModelMapper().map(news, NewsUpdateRequestDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/news/dto/response/NewsDetailsResponseDto.java b/src/main/java/page/clab/api/domain/news/dto/response/NewsDetailsResponseDto.java index 893ef6e7c..ee4916536 100644 --- a/src/main/java/page/clab/api/domain/news/dto/response/NewsDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/news/dto/response/NewsDetailsResponseDto.java @@ -1,19 +1,16 @@ package page.clab.api.domain.news.dto.response; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; -import lombok.ToString; import page.clab.api.domain.news.domain.News; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; @Getter -@Setter -@ToString +@Builder public class NewsDetailsResponseDto { private Long id; @@ -28,14 +25,24 @@ public class NewsDetailsResponseDto { private String source; - private List files = new ArrayList<>(); + private List files; private LocalDate date; private LocalDateTime createdAt; - public static NewsDetailsResponseDto create(News news) { - return ModelMapperUtil.getModelMapper().map(news, NewsDetailsResponseDto.class); + public static NewsDetailsResponseDto toDto(News news) { + return NewsDetailsResponseDto.builder() + .id(news.getId()) + .title(news.getTitle()) + .category(news.getCategory()) + .content(news.getContent()) + .articleUrl(news.getArticleUrl()) + .source(news.getSource()) + .files(UploadedFileResponseDto.toDto(news.getUploadedFiles())) + .date(news.getDate()) + .createdAt(news.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/news/dto/response/NewsResponseDto.java b/src/main/java/page/clab/api/domain/news/dto/response/NewsResponseDto.java index c8c4532d3..f609c5542 100644 --- a/src/main/java/page/clab/api/domain/news/dto/response/NewsResponseDto.java +++ b/src/main/java/page/clab/api/domain/news/dto/response/NewsResponseDto.java @@ -1,15 +1,14 @@ package page.clab.api.domain.news.dto.response; -import java.time.LocalDate; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; -import lombok.ToString; import page.clab.api.domain.news.domain.News; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; +import java.time.LocalDateTime; @Getter -@Setter -@ToString +@Builder public class NewsResponseDto { private Long id; @@ -22,8 +21,17 @@ public class NewsResponseDto { private LocalDate date; - public static NewsResponseDto of(News news) { - return ModelMapperUtil.getModelMapper().map(news, NewsResponseDto.class); + private LocalDateTime createdAt; + + public static NewsResponseDto toDto(News news) { + return NewsResponseDto.builder() + .id(news.getId()) + .title(news.getTitle()) + .category(news.getCategory()) + .articleUrl(news.getArticleUrl()) + .date(news.getDate()) + .createdAt(news.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/notification/api/NotificationController.java b/src/main/java/page/clab/api/domain/notification/api/NotificationController.java index d478539ce..a29ad3b0c 100644 --- a/src/main/java/page/clab/api/domain/notification/api/NotificationController.java +++ b/src/main/java/page/clab/api/domain/notification/api/NotificationController.java @@ -19,12 +19,12 @@ import page.clab.api.domain.notification.application.NotificationService; import page.clab.api.domain.notification.dto.request.NotificationRequestDto; import page.clab.api.domain.notification.dto.response.NotificationResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/notifications") +@RequestMapping("/api/v1/notifications") @RequiredArgsConstructor @Tag(name = "Notification", description = "알림") @Slf4j @@ -35,39 +35,45 @@ public class NotificationController { @Operation(summary = "[U] 알림 생성", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createNotification( - @Valid @RequestBody NotificationRequestDto notificationRequestDto + public ApiResponse createNotification( + @Valid @RequestBody NotificationRequestDto requestDto ) { - Long id = notificationService.createNotification(notificationRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = notificationService.createNotification(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 나의 알림 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getNotifications( + public ApiResponse> getNotifications( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto notifications = notificationService.getNotifications(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(notifications); - return responseModel; + return ApiResponse.success(notifications); } @Operation(summary = "[U] 알림 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{notificationId}") - public ResponseModel deleteNotification( + public ApiResponse deleteNotification( @PathVariable(name = "notificationId") Long notificationId ) throws PermissionDeniedException { Long id = notificationService.deleteNotification(notificationId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 알림 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedNotifications( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto notifications = notificationService.getDeletedNotifications(pageable); + return ApiResponse.success(notifications); } } diff --git a/src/main/java/page/clab/api/domain/notification/application/NotificationService.java b/src/main/java/page/clab/api/domain/notification/application/NotificationService.java index 12975cafc..b649d2c07 100644 --- a/src/main/java/page/clab/api/domain/notification/application/NotificationService.java +++ b/src/main/java/page/clab/api/domain/notification/application/NotificationService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.dao.NotificationRepository; @@ -15,6 +16,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; import java.util.List; import java.util.function.Supplier; @@ -25,6 +27,8 @@ public class NotificationService { private MemberService memberService; + private final ValidationService validationService; + private final NotificationRepository notificationRepository; @Autowired @@ -32,22 +36,31 @@ public void setMemberService(@Lazy MemberService memberService) { this.memberService = memberService; } - public Long createNotification(NotificationRequestDto notificationRequestDto) { - Member member = memberService.getMemberByIdOrThrow(notificationRequestDto.getMemberId()); - Notification notification = Notification.create(notificationRequestDto, member); + @Transactional + public Long createNotification(NotificationRequestDto requestDto) { + Member member = memberService.getMemberByIdOrThrow(requestDto.getMemberId()); + Notification notification = NotificationRequestDto.toEntity(requestDto, member); + validationService.checkValid(notification); return notificationRepository.save(notification).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getNotifications(Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page notifications = getNotificationByMember(member, pageable); - return new PagedResponseDto<>(notifications.map(NotificationResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + Page notifications = getNotificationByMember(currentMember, pageable); + return new PagedResponseDto<>(notifications.map(NotificationResponseDto::toDto)); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedNotifications(Pageable pageable) { + Page notifications = notificationRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(notifications.map(NotificationResponseDto::toDto)); } public Long deleteNotification(Long notificationId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Notification notification = getNotificationByIdOrThrow(notificationId); - notification.validateAccessPermission(member); + notification.validateAccessPermission(currentMember); notificationRepository.delete(notification); return notification.getId(); } @@ -64,6 +77,13 @@ public void sendNotificationToMember(Member member, String content) { notificationRepository.save(notification); } + public void sendNotificationToMembers(List members, String content) { + List notifications = members.stream() + .map(member -> Notification.create(member, content)) + .toList(); + notificationRepository.saveAll(notifications); + } + public void sendNotificationToMember(String memberId, String content) { Member member = memberService.getMemberByIdOrThrow(memberId); Notification notification = Notification.create(member, content); diff --git a/src/main/java/page/clab/api/domain/notification/dao/NotificationRepository.java b/src/main/java/page/clab/api/domain/notification/dao/NotificationRepository.java index 19397c53e..9ff722277 100644 --- a/src/main/java/page/clab/api/domain/notification/dao/NotificationRepository.java +++ b/src/main/java/page/clab/api/domain/notification/dao/NotificationRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.notification.domain.Notification; @@ -10,4 +11,7 @@ public interface NotificationRepository extends JpaRepository findByMemberOrderByCreatedAtDesc(Member member, Pageable pageable); + @Query(value = "SELECT n.* FROM notification n WHERE n.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/notification/domain/Notification.java b/src/main/java/page/clab/api/domain/notification/domain/Notification.java index d598e0884..9892018e3 100644 --- a/src/main/java/page/clab/api/domain/notification/domain/Notification.java +++ b/src/main/java/page/clab/api/domain/notification/domain/Notification.java @@ -8,31 +8,33 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.member.domain.Member; -import page.clab.api.domain.notification.dto.request.NotificationRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import java.time.LocalDateTime; - @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Notification { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE notification SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Notification extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 1000) + @Column(nullable = false) @Size(min = 1, max = 1000, message = "{size.notification.content}") private String content; @@ -40,17 +42,6 @@ public class Notification { @JoinColumn(name = "member_id") private Member member; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static Notification create(NotificationRequestDto notificationRequestDto, Member member) { - return Notification.builder() - .content(notificationRequestDto.getContent()) - .member(member) - .build(); - } - public static Notification create(Member member, String content) { return Notification.builder() .content(content) diff --git a/src/main/java/page/clab/api/domain/notification/dto/request/NotificationRequestDto.java b/src/main/java/page/clab/api/domain/notification/dto/request/NotificationRequestDto.java index dccad7561..6c45b03f8 100644 --- a/src/main/java/page/clab/api/domain/notification/dto/request/NotificationRequestDto.java +++ b/src/main/java/page/clab/api/domain/notification/dto/request/NotificationRequestDto.java @@ -2,28 +2,28 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.notification.domain.Notification; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class NotificationRequestDto { @NotNull(message = "{notNull.notification.memberId}") - @Size(min = 1, message = "{size.notification.memberId}") @Schema(description = "회원 아이디", example = "202312000", required = true) private String memberId; @NotNull(message = "{notNull.notification.content}") - @Size(min = 1, max = 1000, message = "{size.notification.content}") @Schema(description = "내용", example = "알림 내용", required = true) private String content; + public static Notification toEntity(NotificationRequestDto requestDto, Member member) { + return Notification.builder() + .content(requestDto.getContent()) + .member(member) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/notification/dto/response/NotificationResponseDto.java b/src/main/java/page/clab/api/domain/notification/dto/response/NotificationResponseDto.java index 118dc0eee..764e8a542 100644 --- a/src/main/java/page/clab/api/domain/notification/dto/response/NotificationResponseDto.java +++ b/src/main/java/page/clab/api/domain/notification/dto/response/NotificationResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.notification.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.notification.domain.Notification; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class NotificationResponseDto { @@ -22,8 +16,12 @@ public class NotificationResponseDto { private LocalDateTime createdAt; - public static NotificationResponseDto of(Notification notification) { - return ModelMapperUtil.getModelMapper().map(notification, NotificationResponseDto.class); + public static NotificationResponseDto toDto(Notification notification) { + return NotificationResponseDto.builder() + .id(notification.getId()) + .content(notification.getContent()) + .createdAt(notification.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/position/api/PositionController.java b/src/main/java/page/clab/api/domain/position/api/PositionController.java index ff2e2b7dd..4d56e5a30 100644 --- a/src/main/java/page/clab/api/domain/position/api/PositionController.java +++ b/src/main/java/page/clab/api/domain/position/api/PositionController.java @@ -21,11 +21,11 @@ import page.clab.api.domain.position.dto.request.PositionRequestDto; import page.clab.api.domain.position.dto.response.PositionMyResponseDto; import page.clab.api.domain.position.dto.response.PositionResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; @RestController -@RequestMapping("/positions") +@RequestMapping("/api/v1/positions") @RequiredArgsConstructor @Tag(name = "Position", description = "멤버 직책") @Slf4j @@ -36,13 +36,11 @@ public class PositionController { @Operation(summary = "[S] 직책 등록", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("") - public ResponseModel createPosition( - @Valid @RequestBody PositionRequestDto positionRequestDto + public ApiResponse createPosition( + @Valid @RequestBody PositionRequestDto requestDto ) { - Long id = positionService.createPosition(positionRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = positionService.createPosition(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 연도/직책별 목록 조회", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -50,7 +48,7 @@ public ResponseModel createPosition( "연도, 직책 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getPositionsByConditions( + public ApiResponse> getPositionsByConditions( @RequestParam(name = "year", required = false) String year, @RequestParam(name = "positionType", required = false) PositionType positionType, @RequestParam(name = "page", defaultValue = "0") int page, @@ -58,33 +56,39 @@ public ResponseModel getPositionsByConditions( ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto positions = positionService.getPositionsByConditions(year, positionType, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(positions); - return responseModel; + return ApiResponse.success(positions); } @Operation(summary = "[U] 나의 직책 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my-positions") - public ResponseModel getMyPositionsByYear( + public ApiResponse getMyPositionsByYear( @RequestParam(name = "year", required = false) String year ) { PositionMyResponseDto positions = positionService.getMyPositionsByYear(year); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(positions); - return responseModel; + return ApiResponse.success(positions); } @Operation(summary = "[S] 직책 삭제", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/{positionId}") - public ResponseModel deletePosition( + public ApiResponse deletePosition( @PathVariable("positionId") Long positionId ) { Long id = positionService.deletePosition(positionId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 직책 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedPositions( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto positions = positionService.getDeletedPositions(pageable); + return ApiResponse.success(positions); } } diff --git a/src/main/java/page/clab/api/domain/position/application/PositionService.java b/src/main/java/page/clab/api/domain/position/application/PositionService.java index f55855713..8459720d4 100644 --- a/src/main/java/page/clab/api/domain/position/application/PositionService.java +++ b/src/main/java/page/clab/api/domain/position/application/PositionService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.position.dao.PositionRepository; @@ -25,25 +26,37 @@ public class PositionService { private final PositionRepository positionRepository; - public Long createPosition(PositionRequestDto positionRequestDto) { - Member member = memberService.getMemberByIdOrThrow(positionRequestDto.getMemberId()); - return positionRepository.findByMemberAndYearAndPositionType(member, positionRequestDto.getYear(), positionRequestDto.getPositionType()) + @Transactional + public Long createPosition(PositionRequestDto requestDto) { + Member member = memberService.getMemberByIdOrThrow(requestDto.getMemberId()); + return positionRepository.findByMemberAndYearAndPositionType(member, requestDto.getYear(), requestDto.getPositionType()) .map(Position::getId) .orElseGet(() -> { - Position position = Position.of(positionRequestDto); + Position position = PositionRequestDto.toEntity(requestDto); return positionRepository.save(position).getId(); }); } + @Transactional(readOnly = true) public PagedResponseDto getPositionsByConditions(String year, PositionType positionType, Pageable pageable) { Page positions = positionRepository.findByConditions(year, positionType, pageable); - return new PagedResponseDto<>(positions.map(PositionResponseDto::of)); + return new PagedResponseDto<>(positions.map(PositionResponseDto::toDto)); } + @Transactional(readOnly = true) public PositionMyResponseDto getMyPositionsByYear(String year) { - Member member = memberService.getCurrentMember(); - List positions = getPositionsByMemberAndYear(member, year); - return PositionMyResponseDto.of(positions); + Member currentMember = memberService.getCurrentMember(); + List positions = getPositionsByMemberAndYear(currentMember, year); + if (positions.isEmpty()) { + throw new NotFoundException("해당 멤버의 " + year + "년도 직책이 존재하지 않습니다."); + } + return PositionMyResponseDto.toDto(positions); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedPositions(Pageable pageable) { + Page positions = positionRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(positions.map(PositionResponseDto::toDto)); } public Long deletePosition(Long positionId) { diff --git a/src/main/java/page/clab/api/domain/position/dao/PositionRepository.java b/src/main/java/page/clab/api/domain/position/dao/PositionRepository.java index a854eda53..a4dc9333a 100644 --- a/src/main/java/page/clab/api/domain/position/dao/PositionRepository.java +++ b/src/main/java/page/clab/api/domain/position/dao/PositionRepository.java @@ -1,6 +1,9 @@ package page.clab.api.domain.position.dao; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.position.domain.Position; @@ -15,4 +18,7 @@ public interface PositionRepository extends JpaRepository, Posit List findAllByMemberAndYearOrderByPositionTypeAsc(Member member, String year); + @Query(value = "SELECT p.* FROM \"position\" p WHERE p.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/position/domain/Position.java b/src/main/java/page/clab/api/domain/position/domain/Position.java index 180565be9..16cd0541e 100644 --- a/src/main/java/page/clab/api/domain/position/domain/Position.java +++ b/src/main/java/page/clab/api/domain/position/domain/Position.java @@ -7,22 +7,28 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.member.domain.Member; -import page.clab.api.domain.position.dto.request.PositionRequestDto; +import page.clab.api.global.common.domain.BaseEntity; + +import java.time.LocalDate; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Position { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE \"position\" SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Position extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -36,14 +42,13 @@ public class Position { private PositionType positionType; @Column(nullable = false) - @Size(min = 1, message = "{size.executive.year}") private String year; - public static Position of(PositionRequestDto positionRequestDto) { + public static Position create(Member member) { return Position.builder() - .member(Member.builder().id(positionRequestDto.getMemberId()).build()) - .positionType(positionRequestDto.getPositionType()) - .year(positionRequestDto.getYear()) + .member(member) + .positionType(PositionType.MEMBER) + .year(String.valueOf(LocalDate.now().getYear())) .build(); } diff --git a/src/main/java/page/clab/api/domain/position/domain/PositionType.java b/src/main/java/page/clab/api/domain/position/domain/PositionType.java index 3f7ce52f0..30d6a68c2 100644 --- a/src/main/java/page/clab/api/domain/position/domain/PositionType.java +++ b/src/main/java/page/clab/api/domain/position/domain/PositionType.java @@ -11,7 +11,7 @@ public enum PositionType { PRESIDENT("PRESIDENT", "회장"), VICE_PRESIDENT("VICE_PRESIDENT", "부회장"), - OPERATIONS("OPERATIONS", "운영진"), + OPERATION("OPERATION", "운영진"), CORE_TEAM("CORE_TEAM", "코어팀"), MEMBER("MEMBER", "일반회원"); diff --git a/src/main/java/page/clab/api/domain/position/dto/request/PositionRequestDto.java b/src/main/java/page/clab/api/domain/position/dto/request/PositionRequestDto.java index ae513b8eb..1b1d07f32 100644 --- a/src/main/java/page/clab/api/domain/position/dto/request/PositionRequestDto.java +++ b/src/main/java/page/clab/api/domain/position/dto/request/PositionRequestDto.java @@ -2,32 +2,34 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.position.domain.Position; import page.clab.api.domain.position.domain.PositionType; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class PositionRequestDto { - @NotNull(message = "{notNull.executive.memberId}") + @NotNull(message = "{notNull.position.memberId}") @Schema(description = "학번", example = "201912156", required = true) private String memberId; - @NotNull(message = "{notNull.executive.position}") - @Schema(description = "직책", example = "OPERATIONS", required = true) + @NotNull(message = "{notNull.position.position}") + @Schema(description = "직책", example = "OPERATION", required = true) private PositionType positionType; - @NotNull(message = "{notNull.executive.year}") - @Size(min = 4, max = 4, message = "{size.executive.year}") + @NotNull(message = "{notNull.position.year}") @Schema(description = "연도", example = "2023", required = true) private String year; + public static Position toEntity(PositionRequestDto positionRequestDto) { + return Position.builder() + .member(Member.builder().id(positionRequestDto.getMemberId()).build()) + .positionType(positionRequestDto.getPositionType()) + .year(positionRequestDto.getYear()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/position/dto/response/PositionMyResponseDto.java b/src/main/java/page/clab/api/domain/position/dto/response/PositionMyResponseDto.java index eea527faf..cf33beadb 100644 --- a/src/main/java/page/clab/api/domain/position/dto/response/PositionMyResponseDto.java +++ b/src/main/java/page/clab/api/domain/position/dto/response/PositionMyResponseDto.java @@ -1,22 +1,16 @@ package page.clab.api.domain.position.dto.response; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.position.domain.Position; import page.clab.api.domain.position.domain.PositionType; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class PositionMyResponseDto { @@ -34,28 +28,21 @@ public class PositionMyResponseDto { private Map> positionTypes; - public static PositionMyResponseDto of(List positions) { + public static PositionMyResponseDto toDto(List positions) { Member member = positions.getFirst().getMember(); - PositionMyResponseDto positionResponseDto = PositionMyResponseDto.builder() + Map> positionTypesByYear = positions.stream() + .collect(Collectors.groupingBy( + Position::getYear, + Collectors.mapping(Position::getPositionType, Collectors.toList()) + )); + return PositionMyResponseDto.builder() .name(member.getName()) .email(member.getEmail()) .imageUrl(member.getImageUrl()) .interests(member.getInterests()) .githubUrl(member.getGithubUrl()) - .positionTypes(new HashMap<>()) + .positionTypes(positionTypesByYear) .build(); - positions.forEach(position -> { - String year = position.getYear(); - PositionType positionType = position.getPositionType(); - if (positionResponseDto.getPositionTypes().containsKey(year)) { - positionResponseDto.getPositionTypes().get(year).add(positionType); - } else { - List positionTypes = new ArrayList<>(); - positionTypes.add(positionType); - positionResponseDto.getPositionTypes().put(year, positionTypes); - } - }); - return positionResponseDto; } } diff --git a/src/main/java/page/clab/api/domain/position/dto/response/PositionResponseDto.java b/src/main/java/page/clab/api/domain/position/dto/response/PositionResponseDto.java index bfe8c2780..30341d5b9 100644 --- a/src/main/java/page/clab/api/domain/position/dto/response/PositionResponseDto.java +++ b/src/main/java/page/clab/api/domain/position/dto/response/PositionResponseDto.java @@ -1,19 +1,12 @@ package page.clab.api.domain.position.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.position.domain.Position; import page.clab.api.domain.position.domain.PositionType; -import page.clab.api.global.util.ModelMapperUtil; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class PositionResponseDto { @@ -33,15 +26,18 @@ public class PositionResponseDto { private String year; - public static PositionResponseDto of(Position position) { - PositionResponseDto positionResponseDto = ModelMapperUtil.getModelMapper().map(position, PositionResponseDto.class); + public static PositionResponseDto toDto(Position position) { Member member = position.getMember(); - positionResponseDto.setName(member.getName()); - positionResponseDto.setEmail(member.getEmail()); - positionResponseDto.setImageUrl(member.getImageUrl()); - positionResponseDto.setInterests(member.getInterests()); - positionResponseDto.setGithubUrl(member.getGithubUrl()); - return positionResponseDto; + return PositionResponseDto.builder() + .id(position.getId()) + .name(member.getName()) + .email(member.getEmail()) + .imageUrl(member.getImageUrl()) + .interests(member.getInterests()) + .githubUrl(member.getGithubUrl()) + .positionType(position.getPositionType()) + .year(position.getYear()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/product/api/ProductController.java b/src/main/java/page/clab/api/domain/product/api/ProductController.java index 4f6bdc3cb..a7c132c1f 100644 --- a/src/main/java/page/clab/api/domain/product/api/ProductController.java +++ b/src/main/java/page/clab/api/domain/product/api/ProductController.java @@ -21,11 +21,11 @@ import page.clab.api.domain.product.dto.request.ProductRequestDto; import page.clab.api.domain.product.dto.request.ProductUpdateRequestDto; import page.clab.api.domain.product.dto.response.ProductResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; @RestController -@RequestMapping("/products") +@RequestMapping("/api/v1/products") @RequiredArgsConstructor @Tag(name = "Product", description = "서비스") @Slf4j @@ -36,54 +36,58 @@ public class ProductController { @Operation(summary = "[A] 서비스 등록", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createProduct( - @Valid @RequestBody ProductRequestDto productRequestDto + public ApiResponse createProduct( + @Valid @RequestBody ProductRequestDto requestDto ) { - Long id = productService.createProduct(productRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = productService.createProduct(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 서비스 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "서비스명을 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getProductsByConditions( + public ApiResponse> getProductsByConditions( @RequestParam(required = false) String productName, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto productResponseDtos = productService.getProductsByConditions(productName, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(productResponseDtos); - return responseModel; + PagedResponseDto products = productService.getProductsByConditions(productName, pageable); + return ApiResponse.success(products); } @Operation(summary = "[A] 서비스 수정", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{productId}") - public ResponseModel updateProduct( + public ApiResponse updateProduct( @PathVariable(name = "productId") Long productId, - @Valid @RequestBody ProductUpdateRequestDto productUpdateRequestDto + @Valid @RequestBody ProductUpdateRequestDto requestDto ) { - Long id = productService.updateProduct(productId, productUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = productService.updateProduct(productId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[A] 서비스 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("") - public ResponseModel deleteProduct( + public ApiResponse deleteProduct( @RequestParam Long productId ) { Long id = productService.deleteProduct(productId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 서비스 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedProducts( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto products = productService.getDeletedProducts(pageable); + return ApiResponse.success(products); } } diff --git a/src/main/java/page/clab/api/domain/product/application/ProductService.java b/src/main/java/page/clab/api/domain/product/application/ProductService.java index ba4694800..d07005132 100644 --- a/src/main/java/page/clab/api/domain/product/application/ProductService.java +++ b/src/main/java/page/clab/api/domain/product/application/ProductService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.product.dao.ProductRepository; import page.clab.api.domain.product.domain.Product; import page.clab.api.domain.product.dto.request.ProductRequestDto; @@ -11,26 +12,40 @@ import page.clab.api.domain.product.dto.response.ProductResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor public class ProductService { + private final ValidationService validationService; + private final ProductRepository productRepository; - public Long createProduct(ProductRequestDto productRequestDto) { - Product product = Product.of(productRequestDto); + @Transactional + public Long createProduct(ProductRequestDto requestDto) { + Product product = ProductRequestDto.toEntity(requestDto); + validationService.checkValid(product); return productRepository.save(product).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getProductsByConditions(String productName, Pageable pageable) { Page products = productRepository.findByConditions(productName, pageable); - return new PagedResponseDto<>(products.map(ProductResponseDto::of)); + return new PagedResponseDto<>(products.map(ProductResponseDto::toDto)); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedProducts(Pageable pageable) { + Page products = productRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(products.map(ProductResponseDto::toDto)); } - public Long updateProduct(Long productId, ProductUpdateRequestDto productUpdateRequestDto) { + @Transactional + public Long updateProduct(Long productId, ProductUpdateRequestDto requestDto) { Product product = getProductByIdOrThrow(productId); - product.update(productUpdateRequestDto); + product.update(requestDto); + validationService.checkValid(product); return productRepository.save(product).getId(); } diff --git a/src/main/java/page/clab/api/domain/product/dao/ProductRepository.java b/src/main/java/page/clab/api/domain/product/dao/ProductRepository.java index b78eec763..5a1338da8 100644 --- a/src/main/java/page/clab/api/domain/product/dao/ProductRepository.java +++ b/src/main/java/page/clab/api/domain/product/dao/ProductRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.stereotype.Repository; import page.clab.api.domain.product.domain.Product; @@ -12,4 +13,7 @@ public interface ProductRepository extends JpaRepository, Product Page findAllByOrderByCreatedAtDesc(Pageable pageable); + @Query(value = "SELECT p.* FROM product p WHERE p.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/product/domain/Product.java b/src/main/java/page/clab/api/domain/product/domain/Product.java index 4454b97de..2907982aa 100644 --- a/src/main/java/page/clab/api/domain/product/domain/Product.java +++ b/src/main/java/page/clab/api/domain/product/domain/Product.java @@ -6,27 +6,29 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.product.dto.request.ProductRequestDto; import page.clab.api.domain.product.dto.request.ProductUpdateRequestDto; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Product { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE product SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Product extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -36,20 +38,13 @@ public class Product { @Size(min = 1, message = "{size.product.name}") private String name; - @Column(nullable = false, length = 1000) + @Column(nullable = false) @Size(min = 1, max = 1000, message = "{size.product.description}") private String description; @URL(message = "{url.product.url}") private String url; - @CreationTimestamp - private LocalDateTime createdAt; - - public static Product of(ProductRequestDto productRequestDto) { - return ModelMapperUtil.getModelMapper().map(productRequestDto, Product.class); - } - public void update(ProductUpdateRequestDto productUpdateRequestDto) { Optional.ofNullable(productUpdateRequestDto.getName()).ifPresent(this::setName); Optional.ofNullable(productUpdateRequestDto.getDescription()).ifPresent(this::setDescription); diff --git a/src/main/java/page/clab/api/domain/product/dto/request/ProductRequestDto.java b/src/main/java/page/clab/api/domain/product/dto/request/ProductRequestDto.java index ce6612bde..e45d4c676 100644 --- a/src/main/java/page/clab/api/domain/product/dto/request/ProductRequestDto.java +++ b/src/main/java/page/clab/api/domain/product/dto/request/ProductRequestDto.java @@ -2,33 +2,31 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; +import page.clab.api.domain.product.domain.Product; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ProductRequestDto { @NotNull(message = "{notNull.product.name}") - @Size(min = 1, message = "{size.product.name}") @Schema(description = "서비스명", example = "petmily-server", required = true) private String name; @NotNull(message = "{notNull.product.description}") - @Size(min = 1, max = 1000, message = "{size.product.description}") @Schema(description = "설명", example = "펫밀리 (Back) - 제10회 소프트웨어 개발보안 시큐어코딩 해커톤", required = true) private String description; - @URL(message = "{url.product.url}") @Schema(description = "URL", example = "https://github.com/KGU-C-Lab/petmily-server") private String url; + public static Product toEntity(ProductRequestDto requestDto) { + return Product.builder() + .name(requestDto.getName()) + .description(requestDto.getDescription()) + .url(requestDto.getUrl()) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/product/dto/request/ProductUpdateRequestDto.java b/src/main/java/page/clab/api/domain/product/dto/request/ProductUpdateRequestDto.java index 8c4c4dfe9..365613cda 100644 --- a/src/main/java/page/clab/api/domain/product/dto/request/ProductUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/product/dto/request/ProductUpdateRequestDto.java @@ -1,30 +1,19 @@ package page.clab.api.domain.product.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ProductUpdateRequestDto { - @Size(min = 1, message = "{size.product.name}") @Schema(description = "서비스명", example = "petmily-server") private String name; - @Size(min = 1, max = 1000, message = "{size.product.description}") @Schema(description = "설명", example = "펫밀리 (Back) - 제10회 소프트웨어 개발보안 시큐어코딩 해커톤") private String description; - @URL(message = "{url.product.url}") @Schema(description = "URL", example = "https://github.com/KGU-C-Lab/petmily-server") private String url; diff --git a/src/main/java/page/clab/api/domain/product/dto/response/ProductResponseDto.java b/src/main/java/page/clab/api/domain/product/dto/response/ProductResponseDto.java index 7fc2b8226..5d199dfa4 100644 --- a/src/main/java/page/clab/api/domain/product/dto/response/ProductResponseDto.java +++ b/src/main/java/page/clab/api/domain/product/dto/response/ProductResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.product.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.product.domain.Product; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ProductResponseDto { @@ -26,8 +20,14 @@ public class ProductResponseDto { private LocalDateTime createdAt; - public static ProductResponseDto of(Product product) { - return ModelMapperUtil.getModelMapper().map(product, ProductResponseDto.class); + public static ProductResponseDto toDto(Product product) { + return ProductResponseDto.builder() + .id(product.getId()) + .name(product.getName()) + .description(product.getDescription()) + .url(product.getUrl()) + .createdAt(product.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/recruitment/api/RecruitmentController.java b/src/main/java/page/clab/api/domain/recruitment/api/RecruitmentController.java index edf71f1c4..35a551b05 100644 --- a/src/main/java/page/clab/api/domain/recruitment/api/RecruitmentController.java +++ b/src/main/java/page/clab/api/domain/recruitment/api/RecruitmentController.java @@ -5,6 +5,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -13,17 +15,19 @@ 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.RestController; import page.clab.api.domain.recruitment.application.RecruitmentService; import page.clab.api.domain.recruitment.dto.request.RecruitmentRequestDto; import page.clab.api.domain.recruitment.dto.request.RecruitmentUpdateRequestDto; import page.clab.api.domain.recruitment.dto.response.RecruitmentResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.dto.PagedResponseDto; import java.util.List; @RestController -@RequestMapping("/recruitments") +@RequestMapping("/api/v1/recruitments") @RequiredArgsConstructor @Tag(name = "Recruitment", description = "모집 공고") @Slf4j @@ -34,48 +38,52 @@ public class RecruitmentController { @Operation(summary = "[S] 모집 공고 등록", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("") - public ResponseModel createRecruitment( - @Valid @RequestBody RecruitmentRequestDto recruitmentRequestDto + public ApiResponse createRecruitment( + @Valid @RequestBody RecruitmentRequestDto requestDto ) { - Long id = recruitmentService.createRecruitment(recruitmentRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = recruitmentService.createRecruitment(requestDto); + return ApiResponse.success(id); } @Operation(summary = "모집 공고 목록(최근 5건)", description = "ROLE_ANONYMOUS 이상의 권한이 필요함
" + "최근 5건의 모집 공고를 조회") @GetMapping("") - public ResponseModel getRecentRecruitments() { + public ApiResponse> getRecentRecruitments() { List recruitments = recruitmentService.getRecentRecruitments(); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(recruitments); - return responseModel; + return ApiResponse.success(recruitments); } @Operation(summary = "[S] 모집 공고 수정", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PatchMapping("/{recruitmentId}") - public ResponseModel updateRecruitment( + public ApiResponse updateRecruitment( @PathVariable(name = "recruitmentId") Long recruitmentId, - @Valid @RequestBody RecruitmentUpdateRequestDto recruitmentUpdateRequestDto + @Valid @RequestBody RecruitmentUpdateRequestDto requestDto ) { - Long id = recruitmentService.updateRecruitment(recruitmentId, recruitmentUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = recruitmentService.updateRecruitment(recruitmentId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[S] 모집 공고 삭제", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/{recruitmentId}") - public ResponseModel deleteRecruitment( + public ApiResponse deleteRecruitment( @PathVariable(name = "recruitmentId") Long recruitmentId ) { Long id = recruitmentService.deleteRecruitment(recruitmentId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 모집 공고 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedRecruitments( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto recruitments = recruitmentService.getDeletedRecruitments(pageable); + return ApiResponse.success(recruitments); } } diff --git a/src/main/java/page/clab/api/domain/recruitment/application/RecruitmentService.java b/src/main/java/page/clab/api/domain/recruitment/application/RecruitmentService.java index 7e9d81449..a6275550c 100644 --- a/src/main/java/page/clab/api/domain/recruitment/application/RecruitmentService.java +++ b/src/main/java/page/clab/api/domain/recruitment/application/RecruitmentService.java @@ -1,18 +1,21 @@ package page.clab.api.domain.recruitment.application; -import jakarta.transaction.Transactional; 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 page.clab.api.domain.notification.application.NotificationService; import page.clab.api.domain.recruitment.dao.RecruitmentRepository; import page.clab.api.domain.recruitment.domain.Recruitment; import page.clab.api.domain.recruitment.dto.request.RecruitmentRequestDto; import page.clab.api.domain.recruitment.dto.request.RecruitmentUpdateRequestDto; import page.clab.api.domain.recruitment.dto.response.RecruitmentResponseDto; +import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -20,25 +23,38 @@ public class RecruitmentService { private final NotificationService notificationService; + private final ValidationService validationService; + private final RecruitmentRepository recruitmentRepository; @Transactional - public Long createRecruitment(RecruitmentRequestDto recruitmentRequestDto) { - Recruitment recruitment = Recruitment.of(recruitmentRequestDto); + public Long createRecruitment(RecruitmentRequestDto requestDto) { + Recruitment recruitment = RecruitmentRequestDto.toEntity(requestDto); + validationService.checkValid(recruitment); notificationService.sendNotificationToAllMembers("새로운 모집 공고가 등록되었습니다."); return recruitmentRepository.save(recruitment).getId(); } + @Transactional(readOnly = true) public List getRecentRecruitments() { List recruitments = recruitmentRepository.findTop5ByOrderByCreatedAtDesc(); return recruitments.stream() - .map(RecruitmentResponseDto::of) - .collect(Collectors.toList()); + .map(RecruitmentResponseDto::toDto) + .toList(); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedRecruitments(Pageable pageable) { + Page recruitments = recruitmentRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(recruitments + .map(RecruitmentResponseDto::toDto)); } - public Long updateRecruitment(Long recruitmentId, RecruitmentUpdateRequestDto recruitmentUpdateRequestDto) { + @Transactional + public Long updateRecruitment(Long recruitmentId, RecruitmentUpdateRequestDto requestDto) { Recruitment recruitment = getRecruitmentByIdOrThrow(recruitmentId); - recruitment.update(recruitmentUpdateRequestDto); + recruitment.update(requestDto); + validationService.checkValid(recruitment); return recruitmentRepository.save(recruitment).getId(); } diff --git a/src/main/java/page/clab/api/domain/recruitment/dao/RecruitmentRepository.java b/src/main/java/page/clab/api/domain/recruitment/dao/RecruitmentRepository.java index 07b9338ab..bd78152ae 100644 --- a/src/main/java/page/clab/api/domain/recruitment/dao/RecruitmentRepository.java +++ b/src/main/java/page/clab/api/domain/recruitment/dao/RecruitmentRepository.java @@ -1,13 +1,20 @@ package page.clab.api.domain.recruitment.dao; -import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import page.clab.api.domain.recruitment.domain.Recruitment; +import java.util.List; + @Repository public interface RecruitmentRepository extends JpaRepository { List findTop5ByOrderByCreatedAtDesc(); + @Query(value = "SELECT r.* FROM recruitment r WHERE r.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/recruitment/domain/Recruitment.java b/src/main/java/page/clab/api/domain/recruitment/domain/Recruitment.java index 0d5b3bfdc..7f689027c 100644 --- a/src/main/java/page/clab/api/domain/recruitment/domain/Recruitment.java +++ b/src/main/java/page/clab/api/domain/recruitment/domain/Recruitment.java @@ -8,16 +8,17 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.application.domain.ApplicationType; -import page.clab.api.domain.recruitment.dto.request.RecruitmentRequestDto; import page.clab.api.domain.recruitment.dto.request.RecruitmentUpdateRequestDto; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; import java.time.LocalDateTime; import java.util.Optional; @@ -26,9 +27,11 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Recruitment { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE recruitment SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Recruitment extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -52,22 +55,12 @@ public class Recruitment { @Size(min = 1, message = "{size.recruitment.status}") private String status; - private LocalDateTime updateTime; - - @CreationTimestamp - private LocalDateTime createdAt; - - public static Recruitment of(RecruitmentRequestDto recruitmentRequestDto) { - return ModelMapperUtil.getModelMapper().map(recruitmentRequestDto, Recruitment.class); - } - public void update(RecruitmentUpdateRequestDto recruitmentUpdateRequestDto) { Optional.ofNullable(recruitmentUpdateRequestDto.getStartDate()).ifPresent(this::setStartDate); Optional.ofNullable(recruitmentUpdateRequestDto.getEndDate()).ifPresent(this::setEndDate); Optional.ofNullable(recruitmentUpdateRequestDto.getApplicationType()).ifPresent(this::setApplicationType); Optional.ofNullable(recruitmentUpdateRequestDto.getTarget()).ifPresent(this::setTarget); Optional.ofNullable(recruitmentUpdateRequestDto.getStatus()).ifPresent(this::setStatus); - updateTime = LocalDateTime.now(); } } diff --git a/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentRequestDto.java b/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentRequestDto.java index 2e0b6ed1f..7cbf3eff6 100644 --- a/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentRequestDto.java +++ b/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentRequestDto.java @@ -2,22 +2,15 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.application.domain.ApplicationType; import page.clab.api.domain.recruitment.domain.Recruitment; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class RecruitmentRequestDto { @NotNull(message = "{notNull.recruitment.startDate}") @@ -33,17 +26,21 @@ public class RecruitmentRequestDto { private ApplicationType applicationType; @NotNull(message = "{notNull.recruitment.target}") - @Size(min = 1, message = "{size.recruitment.target}") @Schema(description = "대상", example = "2~3학년", required = true) private String target; @NotNull(message = "{notNull.recruitment.status}") - @Size(min = 1, message = "{size.recruitment.status}") @Schema(description = "상태", example = "종료", required = true) private String status; - public static RecruitmentRequestDto of(Recruitment recruitment) { - return ModelMapperUtil.getModelMapper().map(recruitment, RecruitmentRequestDto.class); + public static Recruitment toEntity(RecruitmentRequestDto requestDto) { + return Recruitment.builder() + .startDate(requestDto.getStartDate()) + .endDate(requestDto.getEndDate()) + .applicationType(requestDto.getApplicationType()) + .target(requestDto.getTarget()) + .status(requestDto.getStatus()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentUpdateRequestDto.java b/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentUpdateRequestDto.java index 09105dba3..0a41652e8 100644 --- a/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/recruitment/dto/request/RecruitmentUpdateRequestDto.java @@ -1,23 +1,14 @@ package page.clab.api.domain.recruitment.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.domain.application.domain.ApplicationType; -import page.clab.api.domain.recruitment.domain.Recruitment; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class RecruitmentUpdateRequestDto { @Schema(description = "모집 시작일", example = "2023-11-06T00:00:00") @@ -29,16 +20,10 @@ public class RecruitmentUpdateRequestDto { @Schema(description = "구분", example = "CORE_TEAM") private ApplicationType applicationType; - @Size(min = 1, message = "{size.recruitment.target}") @Schema(description = "대상", example = "2~3학년") private String target; - @Size(min = 1, message = "{size.recruitment.status}") @Schema(description = "상태", example = "종료") private String status; - public static RecruitmentUpdateRequestDto of(Recruitment recruitment) { - return ModelMapperUtil.getModelMapper().map(recruitment, RecruitmentUpdateRequestDto.class); - } - } diff --git a/src/main/java/page/clab/api/domain/recruitment/dto/response/RecruitmentResponseDto.java b/src/main/java/page/clab/api/domain/recruitment/dto/response/RecruitmentResponseDto.java index 578284c5c..f619b6b93 100644 --- a/src/main/java/page/clab/api/domain/recruitment/dto/response/RecruitmentResponseDto.java +++ b/src/main/java/page/clab/api/domain/recruitment/dto/response/RecruitmentResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.recruitment.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.application.domain.ApplicationType; import page.clab.api.domain.recruitment.domain.Recruitment; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class RecruitmentResponseDto { @@ -29,10 +23,18 @@ public class RecruitmentResponseDto { private String status; - private LocalDateTime updateTime; - - public static RecruitmentResponseDto of(Recruitment recruitment) { - return ModelMapperUtil.getModelMapper().map(recruitment, RecruitmentResponseDto.class); + private LocalDateTime updatedAt; + + public static RecruitmentResponseDto toDto(Recruitment recruitment) { + return RecruitmentResponseDto.builder() + .id(recruitment.getId()) + .startDate(recruitment.getStartDate()) + .endDate(recruitment.getEndDate()) + .applicationType(recruitment.getApplicationType()) + .target(recruitment.getTarget()) + .status(recruitment.getStatus()) + .updatedAt(recruitment.getUpdatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/review/api/ReviewController.java b/src/main/java/page/clab/api/domain/review/api/ReviewController.java index 22479bd1a..324cb8bf4 100644 --- a/src/main/java/page/clab/api/domain/review/api/ReviewController.java +++ b/src/main/java/page/clab/api/domain/review/api/ReviewController.java @@ -21,12 +21,12 @@ import page.clab.api.domain.review.dto.request.ReviewRequestDto; import page.clab.api.domain.review.dto.request.ReviewUpdateRequestDto; import page.clab.api.domain.review.dto.response.ReviewResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/reviews") +@RequestMapping("/api/v1/reviews") @RequiredArgsConstructor @Tag(name = "Review", description = "리뷰") @Slf4j @@ -37,13 +37,11 @@ public class ReviewController { @Operation(summary = "[U] 리뷰 등록", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createReview( - @Valid @RequestBody ReviewRequestDto reviewRequestDto + public ApiResponse createReview( + @Valid @RequestBody ReviewRequestDto requestDto ) { - Long id = reviewService.createReview(reviewRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = reviewService.createReview(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 리뷰 목록 조회(멤버 ID, 멤버 이름, 활동 ID, 공개 여부 기준)", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -51,7 +49,7 @@ public ResponseModel createReview( "멤버 ID, 멤버 이름, 활동 ID, 공개 여부 중 하나라도 입력하지 않으면 전체 조회됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getReviewsByConditions( + public ApiResponse> getReviewsByConditions( @RequestParam(name = "memberId", required = false) String memberId, @RequestParam(name = "memberName", required = false) String memberName, @RequestParam(name = "activityId", required = false) Long activityId, @@ -60,49 +58,54 @@ public ResponseModel getReviewsByConditions( @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto reviewResponseDtos = reviewService.getReviewsByConditions(memberId, memberName, activityId, isPublic, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(reviewResponseDtos); - return responseModel; + PagedResponseDto reviews = reviewService.getReviewsByConditions(memberId, memberName, activityId, isPublic, pageable); + return ApiResponse.success(reviews); } @Operation(summary = "[U] 나의 리뷰 목록", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/my") - public ResponseModel getMyReviews( + public ApiResponse> getMyReviews( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto reviewResponseDtos = reviewService.getMyReviews(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(reviewResponseDtos); - return responseModel; + PagedResponseDto myReviews = reviewService.getMyReviews(pageable); + return ApiResponse.success(myReviews); } @Operation(summary = "[U] 리뷰 수정", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{reviewId}") - public ResponseModel updateReview( + public ApiResponse updateReview( @PathVariable(name = "reviewId") Long reviewId, - @Valid @RequestBody ReviewUpdateRequestDto reviewUpdateRequestDto + @Valid @RequestBody ReviewUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = reviewService.updateReview(reviewId, reviewUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = reviewService.updateReview(reviewId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 리뷰 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{reviewId}") - public ResponseModel deleteReview( + public ApiResponse deleteReview( @PathVariable(name = "reviewId") Long reviewId ) throws PermissionDeniedException { Long id = reviewService.deleteReview(reviewId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 리뷰 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedReviews( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto reviews = reviewService.getDeletedReviews(pageable); + return ApiResponse.success(reviews); } } diff --git a/src/main/java/page/clab/api/domain/review/application/ReviewService.java b/src/main/java/page/clab/api/domain/review/application/ReviewService.java index 283e6c877..d9a9717d4 100644 --- a/src/main/java/page/clab/api/domain/review/application/ReviewService.java +++ b/src/main/java/page/clab/api/domain/review/application/ReviewService.java @@ -1,11 +1,11 @@ package page.clab.api.domain.review.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.application.ActivityGroupMemberService; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.activityGroup.domain.ActivityGroupRole; @@ -23,6 +23,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -35,35 +36,49 @@ public class ReviewService { private final NotificationService notificationService; + private final ValidationService validationService; + private final ReviewRepository reviewRepository; @Transactional - public Long createReview(ReviewRequestDto dto) { + public Long createReview(ReviewRequestDto requestDto) { Member currentMember = memberService.getCurrentMember(); - ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupByIdOrThrow(dto.getActivityGroupId()); + ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupByIdOrThrow(requestDto.getActivityGroupId()); validateReviewCreationPermission(activityGroup, currentMember); - Review review = Review.create(dto, currentMember, activityGroup); + Review review = ReviewRequestDto.toEntity(requestDto, currentMember, activityGroup); + validationService.checkValid(review); notifyGroupLeaderOfNewReview(activityGroup, currentMember); return reviewRepository.save(review).getId(); } + @Transactional(readOnly = true) + public PagedResponseDto getReviewsByConditions(String memberId, String memberName, Long activityId, Boolean isPublic, Pageable pageable) { + Member currentMember = memberService.getCurrentMember(); + Page reviews = reviewRepository.findByConditions(memberId, memberName, activityId, isPublic, pageable); + return new PagedResponseDto<>(reviews.map(review -> ReviewResponseDto.toDto(review, currentMember))); + } + + @Transactional(readOnly = true) public PagedResponseDto getMyReviews(Pageable pageable) { Member currentMember = memberService.getCurrentMember(); Page reviews = getReviewByMember(currentMember, pageable); - return new PagedResponseDto<>(reviews.map(review -> ReviewResponseDto.of(review, currentMember))); + return new PagedResponseDto<>(reviews.map(review -> ReviewResponseDto.toDto(review, currentMember))); } - public PagedResponseDto getReviewsByConditions(String memberId, String memberName, Long activityId, Boolean isPublic, Pageable pageable) { + @Transactional(readOnly = true) + public PagedResponseDto getDeletedReviews(Pageable pageable) { Member currentMember = memberService.getCurrentMember(); - Page reviews = reviewRepository.findByConditions(memberId, memberName, activityId, isPublic, pageable); - return new PagedResponseDto<>(reviews.map(review -> ReviewResponseDto.of(review, currentMember))); + Page reviews = reviewRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(reviews.map(review -> ReviewResponseDto.toDto(review, currentMember))); } - public Long updateReview(Long reviewId, ReviewUpdateRequestDto dto) throws PermissionDeniedException { + @Transactional + public Long updateReview(Long reviewId, ReviewUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = memberService.getCurrentMember(); Review review = getReviewByIdOrThrow(reviewId); review.validateAccessPermission(currentMember); - review.update(dto); + review.update(requestDto); + validationService.checkValid(review); return reviewRepository.save(review).getId(); } @@ -87,10 +102,7 @@ private void validateReviewCreationPermission(ActivityGroup activityGroup, Membe private void notifyGroupLeaderOfNewReview(ActivityGroup activityGroup, Member member) { GroupMember groupLeader = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroup.getId(), ActivityGroupRole.LEADER); if (groupLeader != null) { - notificationService.sendNotificationToMember( - groupLeader.getMember().getId(), - "[" + activityGroup.getName() + "] " + member.getName() + "님이 리뷰를 등록하였습니다." - ); + notificationService.sendNotificationToMember(groupLeader.getMember().getId(), "[" + activityGroup.getName() + "] " + member.getName() + "님이 리뷰를 등록하였습니다."); } } @@ -98,10 +110,6 @@ private boolean isExistsByMemberAndActivityGroup(Member member, ActivityGroup ac return reviewRepository.existsByMemberAndActivityGroup(member, activityGroup); } - public boolean isReviewExistsById(Long id) { - return reviewRepository.existsById(id); - } - public Review getReviewByIdOrThrow(Long reviewId) { return reviewRepository.findById(reviewId) .orElseThrow(() -> new NotFoundException("해당 리뷰가 없습니다.")); diff --git a/src/main/java/page/clab/api/domain/review/dao/ReviewRepository.java b/src/main/java/page/clab/api/domain/review/dao/ReviewRepository.java index 3065c2d0d..8bcfcdac2 100644 --- a/src/main/java/page/clab/api/domain/review/dao/ReviewRepository.java +++ b/src/main/java/page/clab/api/domain/review/dao/ReviewRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.member.domain.Member; @@ -16,4 +17,7 @@ public interface ReviewRepository extends JpaRepository, ReviewRep Page findAllByOrderByCreatedAtDesc(Pageable pageable); + @Query(value = "SELECT r.* FROM review r WHERE r.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/review/domain/Review.java b/src/main/java/page/clab/api/domain/review/domain/Review.java index 540758a7d..1dce023a0 100644 --- a/src/main/java/page/clab/api/domain/review/domain/Review.java +++ b/src/main/java/page/clab/api/domain/review/domain/Review.java @@ -7,31 +7,34 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.member.domain.Member; -import page.clab.api.domain.review.dto.request.ReviewRequestDto; import page.clab.api.domain.review.dto.request.ReviewUpdateRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Review { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(callSuper = false) +@SQLDelete(sql = "UPDATE review SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Review extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -45,29 +48,16 @@ public class Review { @JoinColumn(name = "member_id") private Member member; - @NotNull(message = "{notNull.review.content}") + @Column(nullable = false) @Size(min = 1, max = 1000, message = "{size.review.content}") private String content; @Column(nullable = false) private Boolean isPublic; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - - public static Review create(ReviewRequestDto reviewRequestDto, Member member, ActivityGroup activityGroup) { - Review review = ModelMapperUtil.getModelMapper().map(reviewRequestDto, Review.class); - review.setId(null); - review.setMember(member); - review.setActivityGroup(activityGroup); - review.setIsPublic(false); - return review; - } - - public void update(ReviewUpdateRequestDto reviewUpdateRequestDto) { - Optional.ofNullable(reviewUpdateRequestDto.getContent()).ifPresent(this::setContent); - Optional.ofNullable(reviewUpdateRequestDto.getIsPublic()).ifPresent(this::setIsPublic); + public void update(ReviewUpdateRequestDto requestDto) { + Optional.ofNullable(requestDto.getContent()).ifPresent(this::setContent); + Optional.ofNullable(requestDto.getIsPublic()).ifPresent(this::setIsPublic); } public boolean isOwner(Member member) { diff --git a/src/main/java/page/clab/api/domain/review/dto/request/ReviewRequestDto.java b/src/main/java/page/clab/api/domain/review/dto/request/ReviewRequestDto.java index 68ff44f58..ed9fb7de9 100644 --- a/src/main/java/page/clab/api/domain/review/dto/request/ReviewRequestDto.java +++ b/src/main/java/page/clab/api/domain/review/dto/request/ReviewRequestDto.java @@ -2,18 +2,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.review.domain.Review; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ReviewRequestDto { @NotNull(message = "{notNull.review.activityGroupId}") @@ -21,8 +17,17 @@ public class ReviewRequestDto { private Long activityGroupId; @NotNull(message = "{notNull.review.content}") - @Size(min = 1, max = 1000, message = "{size.review.content}") - @Schema(description = "후기", example = "C-Lab에는 다양한 분야에 대한 열정있는 사람들이 모이기 때문에 다양하고 활동적인 스터디 그룹과 프로젝트 팀이 있습니다. 신입생이라도 자유롭게 스터디 그룹을 만들고 사람들을 모아서 원하는 분야와 관련된 기술을 알아보고 같이 공부하기 좋습니다!", required = true) + @Schema(description = "후기", example = "C-Lab에는 다양한 분야에 대한 열정있는 사람들이 모이기 때문에 다양하고 활동적인 스터디 그룹과 프로젝트 팀이 있습니다. " + + "신입생이라도 자유롭게 스터디 그룹을 만들고 사람들을 모아서 원하는 분야와 관련된 기술을 알아보고 같이 공부하기 좋습니다!", required = true) private String content; + public static Review toEntity(ReviewRequestDto requestDto, Member member, ActivityGroup activityGroup) { + return Review.builder() + .activityGroup(activityGroup) + .member(member) + .content(requestDto.getContent()) + .isPublic(false) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/review/dto/request/ReviewUpdateRequestDto.java b/src/main/java/page/clab/api/domain/review/dto/request/ReviewUpdateRequestDto.java index 8b0d9aa5c..9b44c9d27 100644 --- a/src/main/java/page/clab/api/domain/review/dto/request/ReviewUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/review/dto/request/ReviewUpdateRequestDto.java @@ -1,21 +1,13 @@ package page.clab.api.domain.review.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class ReviewUpdateRequestDto { - @Size(min = 1, max = 1000, message = "{size.review.content}") @Schema(description = "후기", example = "C-Lab에는 다양한 분야에 대한 열정있는 사람들이 모이기 때문에 다양하고 활동적인 스터디 그룹과 프로젝트 팀이 있습니다. 신입생이라도 자유롭게 스터디 그룹을 만들고 사람들을 모아서 원하는 분야와 관련된 기술을 알아보고 같이 공부하기 좋습니다!") private String content; diff --git a/src/main/java/page/clab/api/domain/review/dto/response/ReviewResponseDto.java b/src/main/java/page/clab/api/domain/review/dto/response/ReviewResponseDto.java index 2e5e0f232..9c94df489 100644 --- a/src/main/java/page/clab/api/domain/review/dto/response/ReviewResponseDto.java +++ b/src/main/java/page/clab/api/domain/review/dto/response/ReviewResponseDto.java @@ -1,20 +1,14 @@ package page.clab.api.domain.review.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.review.domain.Review; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ReviewResponseDto { @@ -40,16 +34,22 @@ public class ReviewResponseDto { private LocalDateTime createdAt; - public static ReviewResponseDto of(Review review, Member currentMember) { - ReviewResponseDto reviewResponseDto = ModelMapperUtil.getModelMapper().map(review, ReviewResponseDto.class); - reviewResponseDto.setActivityGroupId(review.getActivityGroup().getId()); - reviewResponseDto.setActivityGroupName(review.getActivityGroup().getName()); - reviewResponseDto.setActivityGroupCategory(String.valueOf(review.getActivityGroup().getCategory())); - reviewResponseDto.setMemberId(review.getMember().getId()); - reviewResponseDto.setName(review.getMember().getName()); - reviewResponseDto.setDepartment(review.getMember().getDepartment()); - reviewResponseDto.setIsOwner(review.isOwner(currentMember)); - return reviewResponseDto; + public static ReviewResponseDto toDto(Review review, Member currentMember) { + ActivityGroup activityGroup = review.getActivityGroup(); + Member member = review.getMember(); + return ReviewResponseDto.builder() + .id(review.getId()) + .activityGroupId(activityGroup.getId()) + .activityGroupName(activityGroup.getName()) + .activityGroupCategory(String.valueOf(activityGroup.getCategory())) + .memberId(member.getId()) + .name(member.getName()) + .department(member.getDepartment()) + .content(review.getContent()) + .isPublic(review.getIsPublic()) + .isOwner(review.isOwner(currentMember)) + .createdAt(review.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/schedule/api/ScheduleController.java b/src/main/java/page/clab/api/domain/schedule/api/ScheduleController.java index 5a9210e03..77be53ab4 100644 --- a/src/main/java/page/clab/api/domain/schedule/api/ScheduleController.java +++ b/src/main/java/page/clab/api/domain/schedule/api/ScheduleController.java @@ -16,16 +16,18 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import page.clab.api.domain.schedule.application.ScheduleService; +import page.clab.api.domain.schedule.domain.SchedulePriority; import page.clab.api.domain.schedule.dto.request.ScheduleRequestDto; +import page.clab.api.domain.schedule.dto.response.ScheduleCollectResponseDto; import page.clab.api.domain.schedule.dto.response.ScheduleResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; import java.time.LocalDate; @RestController -@RequestMapping("/schedule") +@RequestMapping("/api/v1/schedule") @RequiredArgsConstructor @Tag(name = "Schedule", description = "일정") public class ScheduleController { @@ -35,59 +37,86 @@ public class ScheduleController { @Operation(summary = "[U] 일정 등록", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createSchedule( - @Valid @RequestBody ScheduleRequestDto scheduleRequestDto + public ApiResponse createSchedule( + @Valid @RequestBody ScheduleRequestDto requestDto ) throws PermissionDeniedException { - Long id = scheduleService.createSchedule(scheduleRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = scheduleService.createSchedule(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 일정 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getSchedulesWithinDateRange( + public ApiResponse> getSchedulesWithinDateRange( @RequestParam(name = "startDate") LocalDate startDate, @RequestParam(name = "endDate") LocalDate endDate, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto scheduleResponseDtos - = scheduleService.getSchedulesWithinDateRange(startDate, endDate, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(scheduleResponseDtos); - return responseModel; + PagedResponseDto schedules = scheduleService.getSchedulesWithinDateRange(startDate, endDate, pageable); + return ApiResponse.success(schedules); + } + + @Operation(summary = "[U] 일정 조회(연도, 월, 중요도 기준)", description = "ROLE_USER 이상의 권한이 필요함
+" + + "3개의 파라미터를 자유롭게 조합하여 필터링 가능
" + + "연도, 월, 중요도 중 하나라도 입력하지 않으면 전체 조회됨") + @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) + @GetMapping("/conditions") + public ApiResponse> getSchedulesByConditions( + @RequestParam(name = "year", required = false) Integer year, + @RequestParam(name = "month", required = false) Integer month, + @RequestParam(name = "priority", required = false) SchedulePriority priority, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto schedules = scheduleService.getSchedulesByConditions(year, month, priority, pageable); + return ApiResponse.success(schedules); } @Operation(summary = "[U] 내 활동 일정 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/activity") - public ResponseModel getActivitySchedules( + public ApiResponse> getActivitySchedules( @RequestParam(name = "startDate") LocalDate startDate, @RequestParam(name = "endDate") LocalDate endDate, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto scheduleResponseDtos - = scheduleService.getActivitySchedules(startDate, endDate, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(scheduleResponseDtos); - return responseModel; + PagedResponseDto schedules = scheduleService.getActivitySchedules(startDate, endDate, pageable); + return ApiResponse.success(schedules); + } + + @Operation(summary = "[U] 일정 모아보기", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) + @GetMapping("/collect") + public ApiResponse getCollectSchedules() { + ScheduleCollectResponseDto schedules = scheduleService.getCollectSchedules(); + return ApiResponse.success(schedules); } @Operation(summary = "[U] 일정 삭제", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{scheduleId}") - public ResponseModel deleteSchedule( + public ApiResponse deleteSchedule( @PathVariable(name = "scheduleId") Long scheduleId ) throws PermissionDeniedException { Long id = scheduleService.deleteSchedule(scheduleId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 일정 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedSchedules( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto schedules = scheduleService.getDeletedSchedules(pageable); + return ApiResponse.success(schedules); } } diff --git a/src/main/java/page/clab/api/domain/schedule/application/ScheduleService.java b/src/main/java/page/clab/api/domain/schedule/application/ScheduleService.java index 765584d5b..2263bf238 100644 --- a/src/main/java/page/clab/api/domain/schedule/application/ScheduleService.java +++ b/src/main/java/page/clab/api/domain/schedule/application/ScheduleService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.activityGroup.application.ActivityGroupAdminService; import page.clab.api.domain.activityGroup.application.ActivityGroupMemberService; import page.clab.api.domain.activityGroup.domain.ActivityGroup; @@ -14,8 +15,10 @@ import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.schedule.dao.ScheduleRepository; import page.clab.api.domain.schedule.domain.Schedule; +import page.clab.api.domain.schedule.domain.SchedulePriority; import page.clab.api.domain.schedule.domain.ScheduleType; import page.clab.api.domain.schedule.dto.request.ScheduleRequestDto; +import page.clab.api.domain.schedule.dto.response.ScheduleCollectResponseDto; import page.clab.api.domain.schedule.dto.response.ScheduleResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; @@ -37,38 +40,57 @@ public class ScheduleService { private final ScheduleRepository scheduleRepository; - public Long createSchedule(ScheduleRequestDto scheduleRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); - ActivityGroup activityGroup = resolveActivityGroupForSchedule(scheduleRequestDto, member); - Schedule schedule = Schedule.create(scheduleRequestDto, member, activityGroup); + @Transactional + public Long createSchedule(ScheduleRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); + ActivityGroup activityGroup = resolveActivityGroupForSchedule(requestDto, currentMember); + Schedule schedule = ScheduleRequestDto.toEntity(requestDto, currentMember, activityGroup); + schedule.validateAccessPermissionForCreation(currentMember); return scheduleRepository.save(schedule).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getSchedulesWithinDateRange(LocalDate startDate, LocalDate endDate, Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page schedules = scheduleRepository.findByDateRangeAndMember(startDate, endDate, member, pageable); - return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + Page schedules = scheduleRepository.findByDateRangeAndMember(startDate, endDate, currentMember, pageable); + return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); } + public PagedResponseDto getSchedulesByConditions(Integer year, Integer month, SchedulePriority priority, Pageable pageable) { + Page schedules = scheduleRepository.findByConditions(year, month, priority, pageable); + return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); + } + + public ScheduleCollectResponseDto getCollectSchedules() { + return scheduleRepository.findCollectSchedules(); + } + + @Transactional(readOnly = true) public PagedResponseDto getActivitySchedules(LocalDate startDate, LocalDate endDate, Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page schedules = scheduleRepository.findActivitySchedulesByDateRangeAndMember(startDate, endDate, member, pageable); - return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + Page schedules = scheduleRepository.findActivitySchedulesByDateRangeAndMember(startDate, endDate, currentMember, pageable); + return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedSchedules(Pageable pageable) { + Page schedules = scheduleRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); } public Long deleteSchedule(Long scheduleId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); Schedule schedule = getScheduleById(scheduleId); - schedule.validateAccessPermission(member); + schedule.validateAccessPermission(currentMember); scheduleRepository.delete(schedule); return schedule.getId(); } - private ActivityGroup resolveActivityGroupForSchedule(ScheduleRequestDto scheduleRequestDto, Member member) throws PermissionDeniedException { - ScheduleType scheduleType = scheduleRequestDto.getScheduleType(); + private ActivityGroup resolveActivityGroupForSchedule(ScheduleRequestDto requestDto, Member member) throws PermissionDeniedException { + ScheduleType scheduleType = requestDto.getScheduleType(); ActivityGroup activityGroup = null; if (!scheduleType.equals(ScheduleType.ALL)) { - Long activityGroupId = Optional.ofNullable(scheduleRequestDto.getActivityGroupId()) + Long activityGroupId = Optional.ofNullable(requestDto.getActivityGroupId()) .orElseThrow(() -> new NullPointerException("스터디 또는 프로젝트 일정은 그룹 id를 입력해야 합니다.")); activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); validateMemberIsGroupLeaderOrAdmin(member, activityGroup); diff --git a/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepository.java b/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepository.java index 84b67afd0..38c547949 100644 --- a/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepository.java +++ b/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepository.java @@ -1,9 +1,15 @@ package page.clab.api.domain.schedule.dao; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import page.clab.api.domain.schedule.domain.Schedule; public interface ScheduleRepository extends JpaRepository, ScheduleRepositoryCustom, QuerydslPredicateExecutor { + @Query(value = "SELECT s.* FROM schedule s WHERE s.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryCustom.java b/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryCustom.java index d5e39981d..2f1a564a4 100644 --- a/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryCustom.java +++ b/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryCustom.java @@ -4,6 +4,8 @@ import org.springframework.data.domain.Pageable; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.schedule.domain.Schedule; +import page.clab.api.domain.schedule.domain.SchedulePriority; +import page.clab.api.domain.schedule.dto.response.ScheduleCollectResponseDto; import java.time.LocalDate; @@ -11,6 +13,10 @@ public interface ScheduleRepositoryCustom { Page findByDateRangeAndMember(LocalDate startDate, LocalDate endDate, Member member, Pageable pageable); + Page findByConditions(Integer year, Integer month, SchedulePriority priority, Pageable pageable); + Page findActivitySchedulesByDateRangeAndMember(LocalDate startDate, LocalDate endDate, Member member, Pageable pageable); + ScheduleCollectResponseDto findCollectSchedules(); + } diff --git a/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryImpl.java b/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryImpl.java index e279f08c1..f902ea083 100644 --- a/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/schedule/dao/ScheduleRepositoryImpl.java @@ -10,7 +10,9 @@ import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.schedule.domain.QSchedule; import page.clab.api.domain.schedule.domain.Schedule; +import page.clab.api.domain.schedule.domain.SchedulePriority; import page.clab.api.domain.schedule.domain.ScheduleType; +import page.clab.api.domain.schedule.dto.response.ScheduleCollectResponseDto; import java.time.LocalDate; import java.time.LocalDateTime; @@ -30,8 +32,8 @@ public Page findByDateRangeAndMember(LocalDate startDate, LocalDate en LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(23, 59, 59); - builder.and(schedule.startDateTime.goe(startDateTime)) - .and(schedule.endDateTime.loe(endDateTime)) + builder.and(schedule.endDateTime.goe(startDateTime)) + .and(schedule.startDateTime.loe(endDateTime)) .and(schedule.scheduleWriter.eq(member)); List results = queryFactory.selectFrom(schedule) @@ -48,6 +50,35 @@ public Page findByDateRangeAndMember(LocalDate startDate, LocalDate en return new PageImpl<>(results, pageable, total); } + @Override + public Page findByConditions(Integer year, Integer month, SchedulePriority priority, Pageable pageable) { + QSchedule schedule = QSchedule.schedule; + BooleanBuilder builder = new BooleanBuilder(); + + if (year != null) { + builder.and(schedule.startDateTime.year().eq(year)); + } + if (month != null) { + builder.and(schedule.startDateTime.month().eq(month)); + } + if (priority != null) { + builder.and(schedule.priority.eq(priority)); + } + + List results = queryFactory.selectFrom(schedule) + .where(builder) + .orderBy(schedule.startDateTime.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = queryFactory.selectFrom(schedule) + .where(builder) + .fetchCount(); + + return new PageImpl<>(results, pageable, total); + } + @Override public Page findActivitySchedulesByDateRangeAndMember(LocalDate startDate, LocalDate endDate, Member member, Pageable pageable) { QSchedule schedule = QSchedule.schedule; @@ -56,8 +87,8 @@ public Page findActivitySchedulesByDateRangeAndMember(LocalDate startD LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(23, 59, 59); - builder.and(schedule.startDateTime.goe(startDateTime)) - .and(schedule.endDateTime.loe(endDateTime)) + builder.and(schedule.endDateTime.goe(startDateTime)) + .and(schedule.startDateTime.loe(endDateTime)) .and(schedule.scheduleWriter.eq(member)) .and(schedule.scheduleType.ne(ScheduleType.ALL)); @@ -75,4 +106,28 @@ public Page findActivitySchedulesByDateRangeAndMember(LocalDate startD return new PageImpl<>(results, pageable, total); } + @Override + public ScheduleCollectResponseDto findCollectSchedules() { + QSchedule schedule = QSchedule.schedule; + BooleanBuilder builder = new BooleanBuilder(); + + LocalDateTime startDateTime = LocalDate.now().withDayOfYear(1).atStartOfDay(); + LocalDateTime endDateTime = LocalDate.now().withDayOfYear(LocalDate.now().lengthOfYear()).atTime(23, 59, 59); + + builder.and(schedule.startDateTime.goe(startDateTime)) + .and(schedule.endDateTime.loe(endDateTime)); + + long total = queryFactory.selectFrom(schedule) + .where(builder) + .fetchCount(); + + builder.and(schedule.priority.eq(SchedulePriority.HIGH)); + + long highPriorityCount = queryFactory.selectFrom(schedule) + .where(builder) + .fetchCount(); + + return ScheduleCollectResponseDto.toDto(total, highPriorityCount); + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/schedule/domain/Schedule.java b/src/main/java/page/clab/api/domain/schedule/domain/Schedule.java index 7cc469f16..7a6534a84 100644 --- a/src/main/java/page/clab/api/domain/schedule/domain/Schedule.java +++ b/src/main/java/page/clab/api/domain/schedule/domain/Schedule.java @@ -9,18 +9,19 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.activityGroup.domain.ActivityGroup; import page.clab.api.domain.member.domain.Member; -import page.clab.api.domain.schedule.dto.request.ScheduleRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDateTime; @@ -28,9 +29,12 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class Schedule { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(callSuper = false) +@SQLDelete(sql = "UPDATE schedule SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class Schedule extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -40,23 +44,21 @@ public class Schedule { @Enumerated(EnumType.STRING) private ScheduleType scheduleType; - @NotNull + @Column(nullable = false) private String title; - @NotNull + @Column(nullable = false) private String detail; - @NotNull - @Column(name = "start_date_time") + @Column(nullable = false) private LocalDateTime startDateTime; - @NotNull - @Column(name = "end_date_time") + @Column(nullable = false) private LocalDateTime endDateTime; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private SchedulePriority priority; @ManyToOne @JoinColumn(name = "member_id") @@ -66,25 +68,24 @@ public class Schedule { @JoinColumn(name = "activityGroup") private ActivityGroup activityGroup; - public static Schedule create(ScheduleRequestDto dto, Member member, ActivityGroup activityGroup) throws PermissionDeniedException { - if (dto.getScheduleType().equals(ScheduleType.ALL) && !member.isAdminRole()) { - throw new PermissionDeniedException("동아리 공통 일정은 ADMIN 이상의 권한만 추가할 수 있습니다."); - } - Schedule schedule = ModelMapperUtil.getModelMapper().map(dto, Schedule.class); - schedule.setId(null); - schedule.setScheduleWriter(member); - schedule.setActivityGroup(activityGroup); - return schedule; - } - public boolean isOwner(Member member) { return this.scheduleWriter.isSameMember(member); } + public boolean isAllSchedule() { + return this.scheduleType.equals(ScheduleType.ALL); + } + public void validateAccessPermission(Member member) throws PermissionDeniedException { if (!isOwner(member) && !member.isAdminRole()) { throw new PermissionDeniedException("해당 일정을 수정/삭제할 권한이 없습니다."); } } + public void validateAccessPermissionForCreation(Member member) throws PermissionDeniedException { + if (this.getScheduleType().equals(ScheduleType.ALL) && !member.isAdminRole()) { + throw new PermissionDeniedException("동아리 공통 일정은 ADMIN 이상의 권한만 추가할 수 있습니다."); + } + } + } diff --git a/src/main/java/page/clab/api/domain/schedule/domain/SchedulePriority.java b/src/main/java/page/clab/api/domain/schedule/domain/SchedulePriority.java new file mode 100644 index 000000000..67daeaef7 --- /dev/null +++ b/src/main/java/page/clab/api/domain/schedule/domain/SchedulePriority.java @@ -0,0 +1,17 @@ +package page.clab.api.domain.schedule.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SchedulePriority { + + HIGH("HIGH", "높음"), + MEDIUM("MEDIUM", "중간"), + LOW("LOW", "낮음"); + + private String key; + private String description; + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/schedule/dto/request/ScheduleRequestDto.java b/src/main/java/page/clab/api/domain/schedule/dto/request/ScheduleRequestDto.java index d1740221a..f8db635e8 100644 --- a/src/main/java/page/clab/api/domain/schedule/dto/request/ScheduleRequestDto.java +++ b/src/main/java/page/clab/api/domain/schedule/dto/request/ScheduleRequestDto.java @@ -2,21 +2,18 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.activityGroup.domain.ActivityGroup; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.schedule.domain.Schedule; +import page.clab.api.domain.schedule.domain.SchedulePriority; import page.clab.api.domain.schedule.domain.ScheduleType; +import java.time.LocalDateTime; + @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Data public class ScheduleRequestDto { @NotNull(message = "{notNull.schedule.scheduleType}") @@ -39,6 +36,23 @@ public class ScheduleRequestDto { @Schema(description = "일정 종료날짜와 시간", example = "2023-11-28 22:00:00.000") private LocalDateTime endDateTime; + @NotNull(message = "{notNull.schedule.priority}") + @Schema(description = "일정 중요도", example = "HIGH") + private SchedulePriority priority; + private Long activityGroupId; + public static Schedule toEntity(ScheduleRequestDto requestDto, Member member, ActivityGroup activityGroup) { + return Schedule.builder() + .scheduleType(requestDto.getScheduleType()) + .title(requestDto.getTitle()) + .detail(requestDto.getDetail()) + .startDateTime(requestDto.getStartDateTime()) + .endDateTime(requestDto.getEndDateTime()) + .priority(requestDto.getPriority()) + .scheduleWriter(member) + .activityGroup(activityGroup) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleCollectResponseDto.java b/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleCollectResponseDto.java new file mode 100644 index 000000000..3b9003d3b --- /dev/null +++ b/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleCollectResponseDto.java @@ -0,0 +1,21 @@ +package page.clab.api.domain.schedule.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ScheduleCollectResponseDto { + + private Long totalScheduleCount; + + private Long totalEventCount; + + public static ScheduleCollectResponseDto toDto(Long totalScheduleCount, Long totalEventCount) { + return ScheduleCollectResponseDto.builder() + .totalScheduleCount(totalScheduleCount) + .totalEventCount(totalEventCount) + .build(); + } + +} diff --git a/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleResponseDto.java b/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleResponseDto.java index ca8e2632d..3f43e6b6a 100644 --- a/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleResponseDto.java +++ b/src/main/java/page/clab/api/domain/schedule/dto/response/ScheduleResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.schedule.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.schedule.domain.Schedule; -import page.clab.api.domain.schedule.domain.ScheduleType; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.domain.schedule.domain.SchedulePriority; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class ScheduleResponseDto { @@ -29,15 +23,18 @@ public class ScheduleResponseDto { private LocalDateTime endDate; - public static ScheduleResponseDto of(Schedule schedule) { - ScheduleResponseDto scheduleResponseDto = ModelMapperUtil.getModelMapper() - .map(schedule, ScheduleResponseDto.class); - - if (schedule.getScheduleType() != ScheduleType.ALL) { - scheduleResponseDto.setActivityName(schedule.getActivityGroup().getName()); - } - - return scheduleResponseDto; + private SchedulePriority priority; + + public static ScheduleResponseDto toDto(Schedule schedule) { + return ScheduleResponseDto.builder() + .id(schedule.getId()) + .title(schedule.getTitle()) + .detail(schedule.getDetail()) + .activityName(schedule.isAllSchedule() ? null : schedule.getActivityGroup().getName()) + .startDate(schedule.getStartDateTime()) + .endDate(schedule.getEndDateTime()) + .priority(schedule.getPriority()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/api/SharedAccountController.java b/src/main/java/page/clab/api/domain/sharedAccount/api/SharedAccountController.java index bd452b3c2..afd1283b8 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/api/SharedAccountController.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/api/SharedAccountController.java @@ -25,13 +25,13 @@ import page.clab.api.domain.sharedAccount.dto.request.SharedAccountUsageRequestDto; import page.clab.api.domain.sharedAccount.dto.response.SharedAccountResponseDto; import page.clab.api.domain.sharedAccount.dto.response.SharedAccountUsageResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.CustomOptimisticLockingFailureException; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/shared-accounts") +@RequestMapping("/api/v1/shared-accounts") @RequiredArgsConstructor @Tag(name = "SharedAccount", description = "공동 계정") @Slf4j @@ -44,52 +44,44 @@ public class SharedAccountController { @Operation(summary = "[S] 공동 계정 추가", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PostMapping("") - public ResponseModel createSharedAccount( - @Valid @RequestBody SharedAccountRequestDto sharedAccountRequestDto + public ApiResponse createSharedAccount( + @Valid @RequestBody SharedAccountRequestDto requestDto ) { - Long id = sharedAccountService.createSharedAccount(sharedAccountRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = sharedAccountService.createSharedAccount(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 공동 계정 조회(상태 포함)", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getSharedAccounts( + public ApiResponse> getSharedAccounts( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto sharedAccounts = sharedAccountService.getSharedAccounts(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(sharedAccounts); - return responseModel; + return ApiResponse.success(sharedAccounts); } @Operation(summary = "[S] 공동 계정 수정", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PatchMapping("/{accountId}") - public ResponseModel updateSharedAccount( + public ApiResponse updateSharedAccount( @PathVariable(name = "accountId") Long accountId, - @Valid @RequestBody SharedAccountUpdateRequestDto sharedAccountUpdateRequestDto + @Valid @RequestBody SharedAccountUpdateRequestDto requestDto ) { - Long id = sharedAccountService.updateSharedAccount(accountId, sharedAccountUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = sharedAccountService.updateSharedAccount(accountId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[S] 공동 계정 삭제", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @DeleteMapping("/{accountId}") - public ResponseModel deleteSharedAccount( + public ApiResponse deleteSharedAccount( @PathVariable(name = "accountId") Long accountId ) { Long id = sharedAccountService.deleteSharedAccount(accountId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } @Operation(summary = "[U] 공동 계정 이용 신청", description = "ROLE_USER 이상의 권한이 필요함
" + @@ -97,42 +89,47 @@ public ResponseModel deleteSharedAccount( "신청시에 계정 상태가 자동으로 바뀌므로 상태 변경은 별도로 안해도 됨") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("/usage") - public ResponseModel requestSharedAccountUsage( - @Valid @RequestBody SharedAccountUsageRequestDto sharedAccountUsageRequestDto + public ApiResponse requestSharedAccountUsage( + @Valid @RequestBody SharedAccountUsageRequestDto requestDto ) throws CustomOptimisticLockingFailureException { - Long id = sharedAccountUsageService.requestSharedAccountUsage(sharedAccountUsageRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = sharedAccountUsageService.requestSharedAccountUsage(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 공동 계정 이용 내역 조회", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("/usage") - public ResponseModel getSharedAccountUsages( + public ApiResponse> getSharedAccountUsages( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto sharedAccountUsages = sharedAccountUsageService.getSharedAccountUsages(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(sharedAccountUsages); - return responseModel; + return ApiResponse.success(sharedAccountUsages); } @Operation(summary = "[U] 공동 계정 이용 상태 변경", description = "ROLE_USER 이상의 권한이 필요함
" + "이용 중 취소/완료, 예약 취소만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/usage/{usageId}") - public ResponseModel updateSharedAccountUsage( + public ApiResponse updateSharedAccountUsage( @PathVariable(name = "usageId") Long usageId, @RequestParam(name = "status") SharedAccountUsageStatus status ) throws PermissionDeniedException { Long id = sharedAccountUsageService.updateSharedAccountUsage(usageId, status); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); } -} + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 공동 계정 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedSharedAccounts( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto sharedAccounts = sharedAccountService.getDeletedSharedAccounts(pageable); + return ApiResponse.success(sharedAccounts); + } +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountService.java b/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountService.java index b66ad15f3..cc87c56cb 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountService.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.sharedAccount.dao.SharedAccountRepository; import page.clab.api.domain.sharedAccount.domain.SharedAccount; import page.clab.api.domain.sharedAccount.dto.request.SharedAccountRequestDto; @@ -11,26 +12,40 @@ import page.clab.api.domain.sharedAccount.dto.response.SharedAccountResponseDto; import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor public class SharedAccountService { + private final ValidationService validationService; + private final SharedAccountRepository sharedAccountRepository; - public Long createSharedAccount(SharedAccountRequestDto sharedAccountRequestDto) { - SharedAccount sharedAccount = SharedAccount.create(sharedAccountRequestDto); + @Transactional + public Long createSharedAccount(SharedAccountRequestDto requestDto) { + SharedAccount sharedAccount = SharedAccountRequestDto.toEntity(requestDto); + validationService.checkValid(sharedAccount); return sharedAccountRepository.save(sharedAccount).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getSharedAccounts(Pageable pageable) { Page sharedAccounts = sharedAccountRepository.findAllByOrderByIdAsc(pageable); - return new PagedResponseDto<>(sharedAccounts.map(SharedAccountResponseDto::of)); + return new PagedResponseDto<>(sharedAccounts.map(SharedAccountResponseDto::toDto)); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedSharedAccounts(Pageable pageable) { + Page sharedAccounts = sharedAccountRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(sharedAccounts.map(SharedAccountResponseDto::toDto)); } - public Long updateSharedAccount(Long accountId, SharedAccountUpdateRequestDto sharedAccountUpdateRequestDto) { + @Transactional + public Long updateSharedAccount(Long accountId, SharedAccountUpdateRequestDto requestDto) { SharedAccount sharedAccount = getSharedAccountByIdOrThrow(accountId); - sharedAccount.update(sharedAccountUpdateRequestDto); + sharedAccount.update(requestDto); + validationService.checkValid(sharedAccount); return sharedAccountRepository.save(sharedAccount).getId(); } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountUsageService.java b/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountUsageService.java index 02ce0da1b..f603d6e85 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountUsageService.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/application/SharedAccountUsageService.java @@ -1,6 +1,5 @@ package page.clab.api.domain.sharedAccount.application; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -9,6 +8,7 @@ import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.sharedAccount.dao.SharedAccountUsageRepository; @@ -38,19 +38,20 @@ public class SharedAccountUsageService { private final SharedAccountUsageRepository sharedAccountUsageRepository; @Transactional - public Long requestSharedAccountUsage(SharedAccountUsageRequestDto sharedAccountUsageRequestDto) throws CustomOptimisticLockingFailureException { + public Long requestSharedAccountUsage(SharedAccountUsageRequestDto requestDto) throws CustomOptimisticLockingFailureException { try { - Long sharedAccountId = sharedAccountUsageRequestDto.getSharedAccountId(); - SharedAccountUsage sharedAccountUsage = prepareSharedAccountUsage(sharedAccountUsageRequestDto, sharedAccountId); + Long sharedAccountId = requestDto.getSharedAccountId(); + SharedAccountUsage sharedAccountUsage = prepareSharedAccountUsage(requestDto, sharedAccountId); return sharedAccountUsage.getId(); } catch (ObjectOptimisticLockingFailureException e) { throw new CustomOptimisticLockingFailureException("공유 계정 이용 요청에 실패했습니다. 다시 시도해주세요."); } } + @Transactional(readOnly = true) public PagedResponseDto getSharedAccountUsages(Pageable pageable) { Page sharedAccountUsages = sharedAccountUsageRepository.findAllByOrderByCreatedAtDesc(pageable); - return new PagedResponseDto<>(sharedAccountUsages.map(SharedAccountUsageResponseDto::of)); + return new PagedResponseDto<>(sharedAccountUsages.map(SharedAccountUsageResponseDto::toDto)); } @Transactional @@ -168,7 +169,7 @@ private void updateUsageStatus(SharedAccountUsage sharedAccountUsage, SharedAcco Member currentMember = memberService.getCurrentMember(); sharedAccountUsage.updateStatus(status, currentMember); sharedAccountUsageRepository.save(sharedAccountUsage); - sharedAccountUsage.getSharedAccount().updateStatus(false); + sharedAccountUsage.getSharedAccount().updateIsInUse(false); sharedAccountService.save(sharedAccountUsage.getSharedAccount()); } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/dao/SharedAccountRepository.java b/src/main/java/page/clab/api/domain/sharedAccount/dao/SharedAccountRepository.java index d99286f3d..1e7efb18c 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/dao/SharedAccountRepository.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/dao/SharedAccountRepository.java @@ -3,10 +3,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import page.clab.api.domain.sharedAccount.domain.SharedAccount; public interface SharedAccountRepository extends JpaRepository { Page findAllByOrderByIdAsc(Pageable pageable); + @Query(value = "SELECT s.* FROM shared_account s WHERE s.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccount.java b/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccount.java index e954133c9..56bb0a8ac 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccount.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccount.java @@ -6,27 +6,29 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.validator.constraints.URL; -import page.clab.api.domain.sharedAccount.dto.request.SharedAccountRequestDto; import page.clab.api.domain.sharedAccount.dto.request.SharedAccountUpdateRequestDto; -import page.clab.api.global.util.ModelMapperUtil; +import page.clab.api.global.common.domain.BaseEntity; -import java.time.LocalDateTime; import java.util.Optional; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class SharedAccount { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE shared_account SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class SharedAccount extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -50,15 +52,6 @@ public class SharedAccount { @Column(nullable = false) private boolean isInUse; - - @CreationTimestamp - private LocalDateTime createdAt; - - public static SharedAccount create(SharedAccountRequestDto sharedAccountRequestDto) { - SharedAccount sharedAccount = ModelMapperUtil.getModelMapper().map(sharedAccountRequestDto, SharedAccount.class); - sharedAccount.setInUse(false); - return sharedAccount; - } public void update(SharedAccountUpdateRequestDto sharedAccountUpdateRequestDto) { Optional.ofNullable(sharedAccountUpdateRequestDto.getUsername()).ifPresent(this::setUsername); @@ -67,7 +60,7 @@ public void update(SharedAccountUpdateRequestDto sharedAccountUpdateRequestDto) Optional.ofNullable(sharedAccountUpdateRequestDto.getPlatformUrl()).ifPresent(this::setPlatformUrl); } - public void updateStatus(boolean isInUse) { + public void updateIsInUse(boolean isInUse) { this.isInUse = isInUse; } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccountUsage.java b/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccountUsage.java index f2f83d445..a711af8d5 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccountUsage.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/domain/SharedAccountUsage.java @@ -17,10 +17,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.sharedAccount.exception.InvalidUsageTimeException; import page.clab.api.domain.sharedAccount.exception.SharedAccountUsageStateException; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; import java.time.LocalDateTime; @@ -32,7 +32,7 @@ @AllArgsConstructor @NoArgsConstructor @Table(indexes = {@Index(name = "idx_shared_account_usage", columnList = "status, startTime, endTime")}) -public class SharedAccountUsage { +public class SharedAccountUsage extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -58,9 +58,6 @@ public class SharedAccountUsage { @Version private Long version; - @CreationTimestamp - private LocalDateTime createdAt; - public static SharedAccountUsage create(SharedAccount sharedAccount, String memberId, LocalDateTime startTime, LocalDateTime endTime) { return SharedAccountUsage.builder() .sharedAccount(sharedAccount) @@ -92,7 +89,7 @@ public void validateUsageTimes(LocalDateTime currentTime) { public void determineStatus(LocalDateTime currentTime) { if (!currentTime.isBefore(startTime) && currentTime.isBefore(endTime)) { this.status = SharedAccountUsageStatus.IN_USE; - this.sharedAccount.setInUse(true); + this.sharedAccount.updateIsInUse(true); } else if (currentTime.isAfter(endTime)) { this.status = SharedAccountUsageStatus.COMPLETED; } else if (currentTime.isBefore(startTime)) { @@ -120,7 +117,7 @@ private void cancelUsage(Member currentMember) { } setStatus(SharedAccountUsageStatus.CANCELED); if (SharedAccountUsageStatus.IN_USE.equals(status)) { - sharedAccount.setInUse(false); + this.sharedAccount.updateIsInUse(false); } } @@ -129,7 +126,7 @@ private void completeUsage(Member currentMember) { throw new SharedAccountUsageStateException("IN_USE 상태에서만 완료할 수 있습니다."); } setStatus(SharedAccountUsageStatus.COMPLETED); - sharedAccount.setInUse(false); + this.sharedAccount.updateIsInUse(false); } private boolean isInUsableState() { diff --git a/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountRequestDto.java b/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountRequestDto.java index 6cab2b5cf..846d27f27 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountRequestDto.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountRequestDto.java @@ -2,39 +2,38 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; +import page.clab.api.domain.sharedAccount.domain.SharedAccount; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class SharedAccountRequestDto { @NotNull(message = "{notNull.sharedAccount.username}") - @Size(min = 1, message = "{size.sharedAccount.username}") - @Schema(description = "아이디", example = "clab8510@gmail.com", required = true) + @Schema(description = "아이디", example = "abc@gmail.com", required = true) private String username; @NotNull(message = "{notNull.sharedAccount.password}") - @Size(min = 1, message = "{size.sharedAccount.password}") - @Schema(description = "비밀번호", example = "Tlfoq8308!", required = true) + @Schema(description = "비밀번호", example = "1234", required = true) private String password; @NotNull(message = "{notNull.sharedAccount.platformName}") - @Size(min = 1, message = "{size.sharedAccount.platformName}") @Schema(description = "플랫폼명", example = "인프런", required = true) private String platformName; @NotNull(message = "{notNull.sharedAccount.platformUrl}") - @URL(message = "{url.sharedAccount.platformUrl}") @Schema(description = "플랫폼 URL", example = "https://www.inflearn.com/", required = true) private String platformUrl; + public static SharedAccount toEntity(SharedAccountRequestDto requestDto) { + return SharedAccount.builder() + .username(requestDto.getUsername()) + .password(requestDto.getPassword()) + .platformName(requestDto.getPlatformName()) + .platformUrl(requestDto.getPlatformUrl()) + .isInUse(false) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUpdateRequestDto.java b/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUpdateRequestDto.java index 3964e7d81..4c8e23f12 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUpdateRequestDto.java @@ -1,34 +1,22 @@ package page.clab.api.domain.sharedAccount.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.URL; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class SharedAccountUpdateRequestDto { - @Size(min = 1, message = "{size.sharedAccount.username}") @Schema(description = "아이디", example = "clab8510@gmail.com") private String username; - @Size(min = 1, message = "{size.sharedAccount.password}") @Schema(description = "비밀번호", example = "Tlfoq8308!") private String password; - @Size(min = 1, message = "{size.sharedAccount.platformName}") @Schema(description = "플랫폼명", example = "인프런") private String platformName; - @URL(message = "{url.sharedAccount.platformUrl}") @Schema(description = "플랫폼 URL", example = "https://www.inflearn.com/") private String platformUrl; diff --git a/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUsageRequestDto.java b/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUsageRequestDto.java index 6413e7fd9..25b047cf8 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUsageRequestDto.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/dto/request/SharedAccountUsageRequestDto.java @@ -2,19 +2,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDateTime; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class SharedAccountUsageRequestDto { @NotNull(message = "{notNull.sharedAccountUsage.sharedAccountId}") diff --git a/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountResponseDto.java b/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountResponseDto.java index 64ec3b52c..c256540d6 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountResponseDto.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountResponseDto.java @@ -1,17 +1,10 @@ package page.clab.api.domain.sharedAccount.dto.response; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.sharedAccount.domain.SharedAccount; -import page.clab.api.global.util.ModelMapperUtil; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class SharedAccountResponseDto { @@ -27,8 +20,15 @@ public class SharedAccountResponseDto { private boolean isInUse; - public static SharedAccountResponseDto of(SharedAccount sharedAccount) { - return ModelMapperUtil.getModelMapper().map(sharedAccount, SharedAccountResponseDto.class); + public static SharedAccountResponseDto toDto(SharedAccount sharedAccount) { + return SharedAccountResponseDto.builder() + .id(sharedAccount.getId()) + .username(sharedAccount.getUsername()) + .password(sharedAccount.getPassword()) + .platformName(sharedAccount.getPlatformName()) + .platformUrl(sharedAccount.getPlatformUrl()) + .isInUse(sharedAccount.isInUse()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountUsageResponseDto.java b/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountUsageResponseDto.java index 8fd0ab6a3..cef10e49a 100644 --- a/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountUsageResponseDto.java +++ b/src/main/java/page/clab/api/domain/sharedAccount/dto/response/SharedAccountUsageResponseDto.java @@ -1,19 +1,13 @@ package page.clab.api.domain.sharedAccount.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.sharedAccount.domain.SharedAccountUsage; import page.clab.api.domain.sharedAccount.domain.SharedAccountUsageStatus; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class SharedAccountUsageResponseDto { @@ -33,11 +27,17 @@ public class SharedAccountUsageResponseDto { private LocalDateTime createdAt; - public static SharedAccountUsageResponseDto of(SharedAccountUsage sharedAccountUsage) { - SharedAccountUsageResponseDto sharedAccountUsageResponseDto = ModelMapperUtil.getModelMapper().map(sharedAccountUsage, SharedAccountUsageResponseDto.class); - sharedAccountUsageResponseDto.setUsername(sharedAccountUsage.getSharedAccount().getUsername()); - sharedAccountUsageResponseDto.setPlatformName(sharedAccountUsage.getSharedAccount().getPlatformName()); - return sharedAccountUsageResponseDto; + public static SharedAccountUsageResponseDto toDto(SharedAccountUsage sharedAccountUsage) { + return SharedAccountUsageResponseDto.builder() + .id(sharedAccountUsage.getId()) + .username(sharedAccountUsage.getSharedAccount().getUsername()) + .platformName(sharedAccountUsage.getSharedAccount().getPlatformName()) + .startTime(sharedAccountUsage.getStartTime()) + .endTime(sharedAccountUsage.getEndTime()) + .memberId(sharedAccountUsage.getMemberId()) + .status(sharedAccountUsage.getStatus()) + .createdAt(sharedAccountUsage.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/domain/sharedAccount/exception/SharedAccountInUseException.java b/src/main/java/page/clab/api/domain/sharedAccount/exception/SharedAccountInUseException.java deleted file mode 100644 index c275a79bc..000000000 --- a/src/main/java/page/clab/api/domain/sharedAccount/exception/SharedAccountInUseException.java +++ /dev/null @@ -1,9 +0,0 @@ -package page.clab.api.domain.sharedAccount.exception; - -public class SharedAccountInUseException extends RuntimeException { - - public SharedAccountInUseException(String message) { - super(message); - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/workExperience/api/WorkExperienceController.java b/src/main/java/page/clab/api/domain/workExperience/api/WorkExperienceController.java index c4abb307f..db5e21ccb 100644 --- a/src/main/java/page/clab/api/domain/workExperience/api/WorkExperienceController.java +++ b/src/main/java/page/clab/api/domain/workExperience/api/WorkExperienceController.java @@ -21,12 +21,12 @@ import page.clab.api.domain.workExperience.dto.request.WorkExperienceRequestDto; import page.clab.api.domain.workExperience.dto.request.WorkExperienceUpdateRequestDto; import page.clab.api.domain.workExperience.dto.response.WorkExperienceResponseDto; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; import page.clab.api.global.exception.PermissionDeniedException; @RestController -@RequestMapping("/work-experiences") +@RequestMapping("/api/v1/work-experiences") @RequiredArgsConstructor @Tag(name = "WorkExperience", description = "경력사항") @Slf4j @@ -37,71 +37,73 @@ public class WorkExperienceController { @Operation(summary = "[U] 경력사항 등록", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping("") - public ResponseModel createWorkExperience( - @Valid @RequestBody WorkExperienceRequestDto workExperienceRequestDto + public ApiResponse createWorkExperience( + @Valid @RequestBody WorkExperienceRequestDto requestDto ) { - Long id = workExperienceService.createWorkExperience(workExperienceRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = workExperienceService.createWorkExperience(requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 나의 경력사항 조회", description = "ROLE_USER 이상의 권한이 필요함
" + "입사일을 기준으로 내림차순 정렬하여 결과를 보여줌") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @GetMapping("") - public ResponseModel getMyWorkExperience( + public ApiResponse> getMyWorkExperience( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto workExperienceResponseDtos = workExperienceService.getMyWorkExperience(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(workExperienceResponseDtos); - return responseModel; + PagedResponseDto myWorkExperience = workExperienceService.getMyWorkExperience(pageable); + return ApiResponse.success(myWorkExperience); } @Operation(summary = "[U] 멤버의 경력사항 검색", description = "ROLE_USER 이상의 권한이 필요함
" + "입사일을 기준으로 내림차순 정렬하여 결과를 보여줌") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) - @GetMapping("/search") - public ResponseModel getWorkExperiencesByConditions( + @GetMapping("/conditions") + public ApiResponse> getWorkExperiencesByConditions( @RequestParam String memberId, @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); - PagedResponseDto workExperienceResponseDtos = workExperienceService.getWorkExperiencesByConditions(memberId, pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(workExperienceResponseDtos); - return responseModel; + PagedResponseDto workExperiences = workExperienceService.getWorkExperiencesByConditions(memberId, pageable); + return ApiResponse.success(workExperiences); } @Operation(summary = "[U] 경력사항 수정", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PatchMapping("/{workExperienceId}") - public ResponseModel updateWorkExperience( + public ApiResponse updateWorkExperience( @PathVariable(name = "workExperienceId") Long workExperienceId, - @Valid @RequestBody WorkExperienceUpdateRequestDto workExperienceUpdateRequestDto + @Valid @RequestBody WorkExperienceUpdateRequestDto requestDto ) throws PermissionDeniedException { - Long id = workExperienceService.updateWorkExperience(workExperienceId, workExperienceUpdateRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + Long id = workExperienceService.updateWorkExperience(workExperienceId, requestDto); + return ApiResponse.success(id); } @Operation(summary = "[U] 경력사항 삭제", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("/{workExperienceId}") - public ResponseModel deleteWorkExperience( + public ApiResponse deleteWorkExperience( @PathVariable(name = "workExperienceId") Long workExperienceId ) throws PermissionDeniedException { Long id = workExperienceService.deleteWorkExperience(workExperienceId); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(id); - return responseModel; + return ApiResponse.success(id); + } + + @GetMapping("/deleted") + @Operation(summary = "[S] 삭제된 경력사항 조회하기", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + public ApiResponse> getDeletedWorkExperiences( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Pageable pageable = PageRequest.of(page, size); + PagedResponseDto workExperiences = workExperienceService.getDeletedWorkExperiences(pageable); + return ApiResponse.success(workExperiences); } } diff --git a/src/main/java/page/clab/api/domain/workExperience/application/WorkExperienceService.java b/src/main/java/page/clab/api/domain/workExperience/application/WorkExperienceService.java index e251ab810..c013af943 100644 --- a/src/main/java/page/clab/api/domain/workExperience/application/WorkExperienceService.java +++ b/src/main/java/page/clab/api/domain/workExperience/application/WorkExperienceService.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.workExperience.dao.WorkExperienceRepository; @@ -14,6 +15,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import page.clab.api.global.validation.ValidationService; @Service @RequiredArgsConstructor @@ -21,38 +23,52 @@ public class WorkExperienceService { private final MemberService memberService; + private final ValidationService validationService; + private final WorkExperienceRepository workExperienceRepository; - public Long createWorkExperience(WorkExperienceRequestDto workExperienceRequestDto) { - Member member = memberService.getCurrentMember(); - WorkExperience workExperience = WorkExperience.of(workExperienceRequestDto, member); + @Transactional + public Long createWorkExperience(WorkExperienceRequestDto requestDto) { + Member currentMember = memberService.getCurrentMember(); + WorkExperience workExperience = WorkExperienceRequestDto.toEntity(requestDto, currentMember); + validationService.checkValid(workExperience); return workExperienceRepository.save(workExperience).getId(); } + @Transactional(readOnly = true) public PagedResponseDto getMyWorkExperience(Pageable pageable) { - Member member = memberService.getCurrentMember(); - Page workExperiences = workExperienceRepository.findAllByMemberOrderByStartDateDesc(member, pageable); - return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::of)); + Member currentMember = memberService.getCurrentMember(); + Page workExperiences = workExperienceRepository.findAllByMemberOrderByStartDateDesc(currentMember, pageable); + return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::toDto)); } + @Transactional(readOnly = true) public PagedResponseDto getWorkExperiencesByConditions(String memberId, Pageable pageable) { Member member = memberService.getMemberByIdOrThrow(memberId); Page workExperiences = workExperienceRepository.findAllByMemberOrderByStartDateDesc(member, pageable); - return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::of)); + return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::toDto)); + } + + @Transactional(readOnly = true) + public PagedResponseDto getDeletedWorkExperiences(Pageable pageable) { + Page workExperiences = workExperienceRepository.findAllByIsDeletedTrue(pageable); + return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::toDto)); } - public Long updateWorkExperience(Long workExperienceId, WorkExperienceUpdateRequestDto workExperienceUpdateRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + @Transactional + public Long updateWorkExperience(Long workExperienceId, WorkExperienceUpdateRequestDto requestDto) throws PermissionDeniedException { + Member currentMember = memberService.getCurrentMember(); WorkExperience workExperience = getWorkExperienceByIdOrThrow(workExperienceId); - workExperience.validateAccessPermission(member); - workExperience.update(workExperienceUpdateRequestDto); + workExperience.validateAccessPermission(currentMember); + workExperience.update(requestDto); + validationService.checkValid(workExperience); return workExperienceRepository.save(workExperience).getId(); } public Long deleteWorkExperience(Long workExperienceId) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); + Member currentMember = memberService.getCurrentMember(); WorkExperience workExperience = getWorkExperienceByIdOrThrow(workExperienceId); - workExperience.validateAccessPermission(member); + workExperience.validateAccessPermission(currentMember); workExperienceRepository.deleteById(workExperienceId); return workExperience.getId(); } diff --git a/src/main/java/page/clab/api/domain/workExperience/dao/WorkExperienceRepository.java b/src/main/java/page/clab/api/domain/workExperience/dao/WorkExperienceRepository.java index 9e50b19e3..4bd2da232 100644 --- a/src/main/java/page/clab/api/domain/workExperience/dao/WorkExperienceRepository.java +++ b/src/main/java/page/clab/api/domain/workExperience/dao/WorkExperienceRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import page.clab.api.domain.member.domain.Member; import page.clab.api.domain.workExperience.domain.WorkExperience; @@ -12,4 +13,7 @@ public interface WorkExperienceRepository extends JpaRepository findAllByMemberOrderByStartDateDesc(Member member, Pageable pageable); + @Query(value = "SELECT w.* FROM work_experience w WHERE w.is_deleted = true", nativeQuery = true) + Page findAllByIsDeletedTrue(Pageable pageable); + } diff --git a/src/main/java/page/clab/api/domain/workExperience/domain/WorkExperience.java b/src/main/java/page/clab/api/domain/workExperience/domain/WorkExperience.java index b039149eb..b74f05ecd 100644 --- a/src/main/java/page/clab/api/domain/workExperience/domain/WorkExperience.java +++ b/src/main/java/page/clab/api/domain/workExperience/domain/WorkExperience.java @@ -7,16 +7,18 @@ import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; import page.clab.api.domain.member.domain.Member; -import page.clab.api.domain.workExperience.dto.request.WorkExperienceRequestDto; import page.clab.api.domain.workExperience.dto.request.WorkExperienceUpdateRequestDto; +import page.clab.api.global.common.domain.BaseEntity; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.util.ModelMapperUtil; import java.time.LocalDate; import java.util.Optional; @@ -25,9 +27,11 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class WorkExperience { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@SQLDelete(sql = "UPDATE work_experience SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class WorkExperience extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -50,12 +54,6 @@ public class WorkExperience { @ManyToOne private Member member; - public static WorkExperience of(WorkExperienceRequestDto workExperienceRequestDto, Member member) { - WorkExperience workExperience = ModelMapperUtil.getModelMapper().map(workExperienceRequestDto, WorkExperience.class); - workExperience.setMember(member); - return workExperience; - } - public void update(WorkExperienceUpdateRequestDto workExperienceUpdateRequestDto) { Optional.ofNullable(workExperienceUpdateRequestDto.getCompanyName()).ifPresent(this::setCompanyName); Optional.ofNullable(workExperienceUpdateRequestDto.getPosition()).ifPresent(this::setPosition); diff --git a/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceRequestDto.java b/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceRequestDto.java index ea2807611..febf392b4 100644 --- a/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceRequestDto.java +++ b/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceRequestDto.java @@ -2,28 +2,22 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import java.time.LocalDate; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.domain.workExperience.domain.WorkExperience; + +import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class WorkExperienceRequestDto { @NotNull(message = "{notNull.workExperience.companyName}") - @Size(min = 1, message = "{size.workExperience.companyName}") @Schema(description = "회사명", example = "네이버 클라우드", required = true) private String companyName; @NotNull(message = "{notNull.workExperience.position}") - @Size(min = 1, message = "{size.workExperience.position}") @Schema(description = "직책", example = "인턴", required = true) private String position; @@ -35,4 +29,14 @@ public class WorkExperienceRequestDto { @Schema(description = "종료일", example = "2023-12-31", required = true) private LocalDate endDate; + public static WorkExperience toEntity(WorkExperienceRequestDto requestDto, Member member) { + return WorkExperience.builder() + .companyName(requestDto.getCompanyName()) + .position(requestDto.getPosition()) + .startDate(requestDto.getStartDate()) + .endDate(requestDto.getEndDate()) + .member(member) + .build(); + } + } diff --git a/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceUpdateRequestDto.java b/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceUpdateRequestDto.java index a35c3cdd4..677833556 100644 --- a/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceUpdateRequestDto.java +++ b/src/main/java/page/clab/api/domain/workExperience/dto/request/WorkExperienceUpdateRequestDto.java @@ -1,27 +1,18 @@ package page.clab.api.domain.workExperience.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import java.time.LocalDate; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder public class WorkExperienceUpdateRequestDto { - @Size(min = 1, message = "{size.workExperience.companyName}") @Schema(description = "회사명", example = "네이버 클라우드") private String companyName; - @Size(min = 1, message = "{size.workExperience.position}") @Schema(description = "직책", example = "인턴") private String position; diff --git a/src/main/java/page/clab/api/domain/workExperience/dto/response/WorkExperienceResponseDto.java b/src/main/java/page/clab/api/domain/workExperience/dto/response/WorkExperienceResponseDto.java index 3ee21f50a..b05302af1 100644 --- a/src/main/java/page/clab/api/domain/workExperience/dto/response/WorkExperienceResponseDto.java +++ b/src/main/java/page/clab/api/domain/workExperience/dto/response/WorkExperienceResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.domain.workExperience.dto.response; -import java.time.LocalDate; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.domain.workExperience.domain.WorkExperience; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDate; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class WorkExperienceResponseDto { @@ -26,8 +20,14 @@ public class WorkExperienceResponseDto { private LocalDate endDate; - public static WorkExperienceResponseDto of(WorkExperience workExperience) { - return ModelMapperUtil.getModelMapper().map(workExperience, WorkExperienceResponseDto.class); + public static WorkExperienceResponseDto toDto(WorkExperience workExperience) { + return WorkExperienceResponseDto.builder() + .id(workExperience.getId()) + .companyName(workExperience.getCompanyName()) + .position(workExperience.getPosition()) + .startDate(workExperience.getStartDate()) + .endDate(workExperience.getEndDate()) + .build(); } } diff --git a/src/main/java/page/clab/api/global/auth/api/RedisIpAccessMonitorController.java b/src/main/java/page/clab/api/global/auth/api/RedisIpAccessMonitorController.java index fd5220026..1ceb81ab7 100644 --- a/src/main/java/page/clab/api/global/auth/api/RedisIpAccessMonitorController.java +++ b/src/main/java/page/clab/api/global/auth/api/RedisIpAccessMonitorController.java @@ -16,10 +16,12 @@ import page.clab.api.global.auth.application.RedisIpAccessMonitorService; import page.clab.api.global.auth.domain.RedisIpAccessMonitor; import page.clab.api.global.common.dto.PagedResponseDto; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; + +import java.util.List; @RestController -@RequestMapping("/ip-access-monitor") +@RequestMapping("/api/v1/ip-access-monitor") @RequiredArgsConstructor @Tag(name = "IP Access Monitor", description = "IP 접근 모니터링") @Slf4j @@ -31,41 +33,36 @@ public class RedisIpAccessMonitorController { "지속적인 비정상 접근으로 인해 Redis에 추가된 IP를 조회") @Secured({"ROLE_SUPER"}) @GetMapping("/abnormal-access") - public ResponseModel getAbnormalAccessBlacklistIps( + public ApiResponse> getAbnormalAccessBlacklistIps( @RequestParam(name = "page", defaultValue = "0") int page, @RequestParam(name = "size", defaultValue = "20") int size ) { Pageable pageable = PageRequest.of(page, size); PagedResponseDto abnormalAccessBlacklistIps = redisIpAccessMonitorService.getAbnormalAccessIps(pageable); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(abnormalAccessBlacklistIps); - return responseModel; + return ApiResponse.success(abnormalAccessBlacklistIps); } @Operation(summary = "[S] 비정상 접근 IP 기록 삭제", description = "ROLE_SUPER 이상의 권한이 필요함
" + "지속적인 비정상 접근으로 인해 차단된 IP를 삭제") @Secured({"ROLE_SUPER"}) @DeleteMapping("/abnormal-access") - public ResponseModel removeAbnormalAccessBlacklistIp( + public ApiResponse removeAbnormalAccessBlacklistIp( HttpServletRequest request, @RequestParam(name = "ipAddress") String ipAddress ) { String deletedIp = redisIpAccessMonitorService.deleteAbnormalAccessIp(request, ipAddress); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(deletedIp); - return responseModel; + return ApiResponse.success(deletedIp); } @Operation(summary = "[S] 비정상 접근 IP 기록 초기화", description = "ROLE_SUPER 이상의 권한이 필요함
" + "지속적인 비정상 접근으로 인해 차단된 IP를 모두 삭제") @Secured({"ROLE_SUPER"}) @DeleteMapping("/abnormal-access/clear") - public ResponseModel clearAbnormalAccessBlacklist( + public ApiResponse> clearAbnormalAccessBlacklist( HttpServletRequest request ) { - redisIpAccessMonitorService.clearAbnormalAccessIps(request); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + List ipAccessMonitors = redisIpAccessMonitorService.clearAbnormalAccessIps(request); + return ApiResponse.success(ipAccessMonitors); } } diff --git a/src/main/java/page/clab/api/global/auth/application/CustomUserDetailsService.java b/src/main/java/page/clab/api/global/auth/application/CustomUserDetailsService.java index 40855d73e..65bac0098 100644 --- a/src/main/java/page/clab/api/global/auth/application/CustomUserDetailsService.java +++ b/src/main/java/page/clab/api/global/auth/application/CustomUserDetailsService.java @@ -22,11 +22,7 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx } private UserDetails createUserDetails(Member member) { - return Member.builder() - .id(member.getUsername()) - .password(member.getPassword()) - .role(member.getRole()) - .build(); + return Member.createUserDetails(member); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/auth/application/RedisIpAccessMonitorService.java b/src/main/java/page/clab/api/global/auth/application/RedisIpAccessMonitorService.java index 0ba1befad..856962c03 100644 --- a/src/main/java/page/clab/api/global/auth/application/RedisIpAccessMonitorService.java +++ b/src/main/java/page/clab/api/global/auth/application/RedisIpAccessMonitorService.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import page.clab.api.global.auth.dao.RedisIpAccessMonitorRepository; import page.clab.api.global.auth.domain.RedisIpAccessMonitor; import page.clab.api.global.common.dto.PagedResponseDto; @@ -45,6 +46,7 @@ public boolean isBlocked(String ipAddress) { return existingAttempt != null && existingAttempt.getAttempts() >= maxAttempts; } + @Transactional(readOnly = true) public PagedResponseDto getAbnormalAccessIps(Pageable pageable) { List allMonitors = StreamSupport .stream(redisIpAccessMonitorRepository.findAll().spliterator(), false) @@ -67,9 +69,13 @@ public String deleteAbnormalAccessIp(HttpServletRequest request, String ipAddres return ipAddress; } - public void clearAbnormalAccessIps(HttpServletRequest request) { + public List clearAbnormalAccessIps(HttpServletRequest request) { + List ipAccessMonitors = StreamSupport + .stream(redisIpAccessMonitorRepository.findAll().spliterator(), false) + .toList(); redisIpAccessMonitorRepository.deleteAll(); slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: ALL"); + return ipAccessMonitors; } } diff --git a/src/main/java/page/clab/api/global/auth/domain/RedisIpAccessMonitor.java b/src/main/java/page/clab/api/global/auth/domain/RedisIpAccessMonitor.java index 742add554..08017755c 100644 --- a/src/main/java/page/clab/api/global/auth/domain/RedisIpAccessMonitor.java +++ b/src/main/java/page/clab/api/global/auth/domain/RedisIpAccessMonitor.java @@ -1,6 +1,7 @@ package page.clab.api.global.auth.domain; import jakarta.persistence.Column; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -15,8 +16,8 @@ @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) @RedisHash(value = "ipAccessMonitor", timeToLive = 60 * 5) public class RedisIpAccessMonitor { diff --git a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java index 6772b9635..c9a99d6e1 100644 --- a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java @@ -59,9 +59,7 @@ private void logAndRespondToSuspiciousAccess(HttpServletRequest request, HttpSer if (!blacklistIpRepository.existsByIpAddress(clientIpAddress)) { blacklistIpRepository.save( - BlacklistIp.builder() - .ipAddress(clientIpAddress) - .build() + BlacklistIp.create(clientIpAddress, "서버 내부 파일 및 디렉토리에 대한 접근 시도") ); slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, clientIpAddress); } diff --git a/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java index 7760b96ee..e33516376 100644 --- a/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java @@ -1,43 +1,88 @@ package page.clab.api.global.auth.filter; +import io.ipinfo.api.IPinfo; +import io.ipinfo.api.errors.RateLimitedException; +import io.ipinfo.api.model.IPResponse; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; +import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import page.clab.api.global.util.GeoIpUtil; +import page.clab.api.global.config.IPInfoConfig; import page.clab.api.global.util.HttpReqResUtil; import java.io.IOException; -import java.net.InetAddress; @Component @Slf4j public class IpAuthenticationFilter implements Filter { + private final IPinfo ipInfo; + + private final AttributeStrategy attributeStrategy; + + private final InterceptorStrategy interceptorStrategy; + + public IpAuthenticationFilter(IPInfoConfig ipInfoConfig) { + ipInfo = ipInfoConfig.ipInfo(); + attributeStrategy = ipInfoConfig.attributeStrategy(); + interceptorStrategy = ipInfoConfig.interceptorStrategy(); + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - String ipAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - InetAddress inetAddress = InetAddress.getByName(ipAddress); - String country = GeoIpUtil.getInfoByIp(inetAddress.toString()).getCountry(); - if (country != null && !country.equals("South Korea")) { - log.info("[{}:{}] 허용되지 않은 국가로부터의 접근입니다.", ipAddress, country); - return; + HttpServletRequest httpRequest = (HttpServletRequest) request; + String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + try { + if (shouldProcessRequest(httpRequest, clientIpAddress)) { + chain.doFilter(request, response); + return; + } + IPResponse ipResponse = storeIpInformation(clientIpAddress, httpRequest); + if (isNonPermittedCountry(ipResponse)) { + log.warn("[{}:{}] Access from non-permitted country", clientIpAddress, ipResponse.getCountryName()); + return; + } + } catch (RateLimitedException e) { + log.error("Rate limit exceeded while getting IP information."); + } catch (Exception e) { + log.error("Failed to get IP information."); } chain.doFilter(request, response); } + private boolean shouldProcessRequest(HttpServletRequest httpRequest, String clientIpAddress) { + return !interceptorStrategy.shouldRun(httpRequest) + || attributeStrategy.hasAttribute(httpRequest) + || HttpReqResUtil.isLocalRequest(clientIpAddress) + || clientIpAddress.equals("0.0.0.0"); + } + + private IPResponse storeIpInformation(String clientIpAddress, HttpServletRequest httpRequest) throws RateLimitedException { + IPResponse ipResponse = ipInfo.lookupIP(clientIpAddress); + attributeStrategy.storeAttribute(httpRequest, ipResponse); + return ipResponse; + } + + private boolean isNonPermittedCountry(IPResponse ipResponse) { + String country = ipResponse.getCountryCode(); + return country != null && !country.equals("KR"); + } + @Override public void init(FilterConfig filterConfig) { - log.info("IP Authentication Filter Init.."); + log.info("IP Authentication Filter initialized."); } @Override public void destroy() { - log.info("IP Authentication Filter Destroy.."); + log.info("IP Authentication Filter destroyed."); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/auth/jwt/JwtTokenProvider.java b/src/main/java/page/clab/api/global/auth/jwt/JwtTokenProvider.java index 795e6041a..c93bd3b72 100644 --- a/src/main/java/page/clab/api/global/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/page/clab/api/global/auth/jwt/JwtTokenProvider.java @@ -26,7 +26,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; -import java.util.stream.Collectors; @Slf4j @Component @@ -68,10 +67,7 @@ public TokenInfo generateToken(String id, Role role) { .signWith(key, SignatureAlgorithm.HS256) .compact(); - return TokenInfo.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); + return TokenInfo.create(accessToken, refreshToken); } public boolean isRefreshToken(String token) { @@ -102,7 +98,7 @@ public Authentication getAuthentication(String accessToken) { Arrays.stream(claims.get("role").toString().split(",")) .map(this::formatRoleString) .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); + .toList(); UserDetails principal = new User(claims.getSubject(), "", authorities); return new UsernamePasswordAuthenticationToken(principal, "", authorities); @@ -132,6 +128,15 @@ public boolean validateToken(String token) { return false; } + public boolean validateTokenSilently(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + public Claims parseClaims(String accessToken) { try { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); diff --git a/src/main/java/page/clab/api/global/common/domain/BaseEntity.java b/src/main/java/page/clab/api/global/common/domain/BaseEntity.java new file mode 100644 index 000000000..819fbf3b3 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/domain/BaseEntity.java @@ -0,0 +1,28 @@ +package page.clab.api.global.common.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public class BaseEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @Column(name = "is_deleted") + protected Boolean isDeleted = Boolean.FALSE; + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/domain/Contact.java b/src/main/java/page/clab/api/global/common/domain/Contact.java new file mode 100644 index 000000000..59c19f77c --- /dev/null +++ b/src/main/java/page/clab/api/global/common/domain/Contact.java @@ -0,0 +1,22 @@ +package page.clab.api.global.common.domain; + +import lombok.Getter; + +@Getter +public class Contact { + + private final String value; + + private Contact(String value) { + this.value = removeHyphens(value); + } + + public static Contact of(String value) { + return new Contact(value); + } + + private static String removeHyphens(String value) { + return value.replaceAll("-", ""); + } + +} diff --git a/src/main/java/page/clab/api/global/common/dto/ApiResponse.java b/src/main/java/page/clab/api/global/common/dto/ApiResponse.java new file mode 100644 index 000000000..01390311e --- /dev/null +++ b/src/main/java/page/clab/api/global/common/dto/ApiResponse.java @@ -0,0 +1,45 @@ +package page.clab.api.global.common.dto; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ApiResponse { + + @Builder.Default + private Boolean success = true; + + private T data; + + public String toJson() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } + + public static ApiResponse success() { + return ApiResponse.builder().build(); + } + + public static ApiResponse success(T data) { + return ApiResponse.builder() + .data(data) + .build(); + } + + public static ApiResponse failure() { + return ApiResponse.builder() + .success(false) + .build(); + } + + public static ApiResponse failure(T data) { + return ApiResponse.builder() + .success(false) + .data(data) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/dto/ErrorResponse.java b/src/main/java/page/clab/api/global/common/dto/ErrorResponse.java new file mode 100644 index 000000000..18beb3ab5 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/dto/ErrorResponse.java @@ -0,0 +1,31 @@ +package page.clab.api.global.common.dto; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ErrorResponse { + + @Builder.Default + private Boolean success = false; + + private T data; + + private String errorMessage; + + public String toJson() { + Gson gson = new GsonBuilder().serializeNulls().create(); + return gson.toJson(this); + } + + public static ErrorResponse failure(Exception e) { + String exceptionName = e.getClass().getSimpleName(); + return ErrorResponse.builder() + .errorMessage(exceptionName.toUpperCase()) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/dto/IPInfoResponse.java b/src/main/java/page/clab/api/global/common/dto/IPInfoResponse.java new file mode 100644 index 000000000..626450e5f --- /dev/null +++ b/src/main/java/page/clab/api/global/common/dto/IPInfoResponse.java @@ -0,0 +1,26 @@ +package page.clab.api.global.common.dto; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class IPInfoResponse { + + private String ip; + + private String city; + + private String region; + + private String country; + + private String loc; + + private String org; + + private String postal; + + private String timezone; + +} diff --git a/src/main/java/page/clab/api/global/common/dto/PagedResponseDto.java b/src/main/java/page/clab/api/global/common/dto/PagedResponseDto.java index 828b8f191..df0e6412c 100644 --- a/src/main/java/page/clab/api/global/common/dto/PagedResponseDto.java +++ b/src/main/java/page/clab/api/global/common/dto/PagedResponseDto.java @@ -1,18 +1,12 @@ package page.clab.api.global.common.dto; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.Setter; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; @Getter -@Setter -@AllArgsConstructor -@Builder public class PagedResponseDto { private final int currentPage; diff --git a/src/main/java/page/clab/api/global/common/dto/ResponseModel.java b/src/main/java/page/clab/api/global/common/dto/ResponseModel.java deleted file mode 100644 index fe6e45e2f..000000000 --- a/src/main/java/page/clab/api/global/common/dto/ResponseModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package page.clab.api.global.common.dto; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -@AllArgsConstructor -@Builder -public class ResponseModel { - - @Builder.Default - private Boolean success = true; - - private Object data; - - public void addData(Object data) { - this.data = data; - } - - public String toJson() { - Gson gson = new GsonBuilder().serializeNulls().create(); - return gson.toJson(this); - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/email/api/EmailController.java b/src/main/java/page/clab/api/global/common/email/api/EmailController.java index 9bd40d0e9..bd21a921f 100644 --- a/src/main/java/page/clab/api/global/common/email/api/EmailController.java +++ b/src/main/java/page/clab/api/global/common/email/api/EmailController.java @@ -2,8 +2,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.List; -import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -13,12 +11,15 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.email.application.EmailService; import page.clab.api.global.common.email.dto.request.EmailDto; +import java.util.List; +import java.util.concurrent.CompletableFuture; + @RestController -@RequestMapping("/emails") +@RequestMapping("/api/v1/emails") @RequiredArgsConstructor @Tag(name = "Email", description = "이메일") @Slf4j @@ -29,31 +30,31 @@ public class EmailController { @Operation(summary = "[A] 메일 전송", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(path = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel broadcastEmail( + public ApiResponse> broadcastEmail( EmailDto emailDto, @RequestParam(name = "multipartFile", required = false) List files ) { - CompletableFuture emailTask = CompletableFuture.runAsync(() -> { - emailService.broadcastEmail(emailDto, files); + CompletableFuture> emailTask = CompletableFuture.supplyAsync(() -> { + return emailService.broadcastEmail(emailDto, files); }); - emailTask.join(); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + + List successfulAddresses = emailTask.join(); + return ApiResponse.success(successfulAddresses); } @Operation(summary = "[A] 전체 메일 전송", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(path = "/all", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel broadcastEmailToAllMember( + public ApiResponse> broadcastEmailToAllMember( EmailDto emailDto, @RequestParam(name = "multipartFile", required = false) List files ) { - CompletableFuture emailTask = CompletableFuture.runAsync(() -> { - emailService.broadcastEmailToAllMember(emailDto, files); + CompletableFuture> emailTask = CompletableFuture.supplyAsync(() -> { + return emailService.broadcastEmailToAllMember(emailDto, files); }); - emailTask.join(); - ResponseModel responseModel = ResponseModel.builder().build(); - return responseModel; + + List successfulEmails = emailTask.join(); + return ApiResponse.success(successfulEmails); } } diff --git a/src/main/java/page/clab/api/global/common/email/application/EmailService.java b/src/main/java/page/clab/api/global/common/email/application/EmailService.java index 07da3e2fa..416f8444b 100644 --- a/src/main/java/page/clab/api/global/common/email/application/EmailService.java +++ b/src/main/java/page/clab/api/global/common/email/application/EmailService.java @@ -21,10 +21,12 @@ import page.clab.api.domain.member.dto.response.MemberResponseDto; import page.clab.api.global.common.email.domain.EmailTemplateType; import page.clab.api.global.common.email.dto.request.EmailDto; +import page.clab.api.global.common.email.exception.MessageSendingFailedException; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.BlockingQueue; @@ -51,42 +53,44 @@ public class EmailService { private static final BlockingQueue emailQueue = new LinkedBlockingQueue<>(); - public void broadcastEmail(EmailDto emailDto, List multipartFiles) { - List convertedFiles; - if(multipartFiles != null && !multipartFiles.isEmpty()) { - convertedFiles = convertMultipartFiles(multipartFiles); - } else { - convertedFiles = null; - } + public List broadcastEmail(EmailDto emailDto, List multipartFiles) { + List convertedFiles = multipartFiles != null && !multipartFiles.isEmpty() + ? convertMultipartFiles(multipartFiles) + : new ArrayList<>(); + + List successfulAddresses = Collections.synchronizedList(new ArrayList<>()); emailDto.getTo().parallelStream().forEach(address -> { try { Member recipient = memberService.getMemberByEmail(address); String emailContent = generateEmailContent(emailDto, recipient.getName()); sendEmailAsync(address, emailDto.getSubject(), emailContent, convertedFiles, emailDto.getEmailTemplateType()); + successfulAddresses.add(address); } catch (MessagingException e) { - throw new RuntimeException(e); + throw new MessageSendingFailedException(address + "에게 이메일을 보내는데 실패했습니다."); } }); + return successfulAddresses; } - public void broadcastEmailToAllMember(EmailDto emailDto, List multipartFiles) { - List convertedFiles; - if(multipartFiles != null && !multipartFiles.isEmpty()) { - convertedFiles = convertMultipartFiles(multipartFiles); - } else { - convertedFiles = null; - } + public List broadcastEmailToAllMember(EmailDto emailDto, List multipartFiles) { + List convertedFiles = multipartFiles != null && !multipartFiles.isEmpty() ? + convertMultipartFiles(multipartFiles) : null; List memberList = memberService.getMembers(); + + List successfulEmails = Collections.synchronizedList(new ArrayList<>()); + memberList.parallelStream().forEach(member -> { try { String emailContent = generateEmailContent(emailDto, member.getName()); sendEmailAsync(member.getEmail(), emailDto.getSubject(), emailContent, convertedFiles, emailDto.getEmailTemplateType()); + successfulEmails.add(member.getEmail()); } catch (MessagingException e) { - throw new RuntimeException(e); + throw new MessageSendingFailedException(member.getEmail() + "에게 이메일을 보내는데 실패했습니다."); } }); + return successfulEmails; } public void broadcastEmailToApprovedMember(Member member, String password) { @@ -102,19 +106,12 @@ public void broadcastEmailToApprovedMember(Member member, String password) { member.getId(), password ); - - EmailDto emailDto = EmailDto.builder() - .to(List.of(member.getEmail())) - .subject(subject) - .content(content) - .emailTemplateType(EmailTemplateType.NORMAL) - .build(); - + EmailDto emailDto = EmailDto.create(List.of(member.getEmail()), subject, content, EmailTemplateType.NORMAL); try { String emailContent = generateEmailContent(emailDto, member.getName()); sendEmailAsync(member.getEmail(), emailDto.getSubject(), emailContent, null, emailDto.getEmailTemplateType()); } catch (MessagingException e) { - throw new RuntimeException(e); + throw new MessageSendingFailedException(member.getEmail() + " 계정 발급 안내 메일 전송에 실패했습니다."); } } @@ -127,18 +124,11 @@ public void sendPasswordResetEmail(Member member, String code) { "재설정시 비밀번호는 인증번호로 대체됩니다.", code ); - - EmailDto emailDto = EmailDto.builder() - .to(List.of(member.getEmail())) - .subject(subject) - .content(content) - .emailTemplateType(EmailTemplateType.NORMAL) - .build(); - + EmailDto emailDto = EmailDto.create(List.of(member.getEmail()), subject, content, EmailTemplateType.NORMAL); try { broadcastEmail(emailDto, null); } catch (Exception e) { - throw new RuntimeException("비밀번호 재발급 인증 메일 발송에 실패했습니다.", e); + throw new MessageSendingFailedException(member.getEmail() + " 비밀번호 재발급 인증 메일 전송에 실패했습니다."); } } @@ -167,7 +157,7 @@ private File convertMultipartFileToFile(MultipartFile multipartFile) { return file; } - private String generateEmailContent(EmailDto emailDto, String name){ + private String generateEmailContent(EmailDto emailDto, String name) { Context context = new Context(); context.setVariable("title", emailDto.getSubject()); context.setVariable("name", name); @@ -262,13 +252,13 @@ public List getFiles() { return files; } - public EmailTemplateType getTemplateType(){ + public EmailTemplateType getTemplateType() { return templateType; } } private void setImageInTemplate(MimeMessageHelper messageHelper, EmailTemplateType templateType) throws MessagingException { - switch(templateType){ + switch(templateType) { case NORMAL -> { messageHelper.addInline("image-1", new ClassPathResource("images/image-1.png")); break; @@ -276,7 +266,7 @@ private void setImageInTemplate(MimeMessageHelper messageHelper, EmailTemplateTy } } - private void checkDir(File file){ + private void checkDir(File file) { if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } diff --git a/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java b/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java index 93db93396..e60fb5994 100644 --- a/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java +++ b/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java @@ -2,18 +2,15 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import java.util.List; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import page.clab.api.global.common.email.domain.EmailTemplateType; +import java.util.List; + @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class EmailDto { @@ -33,11 +30,12 @@ public class EmailDto { @Schema(description = "이메일 템플릿", example = "NORMAL") private EmailTemplateType emailTemplateType; - public static EmailDto of(List to, String subject, String content) { + public static EmailDto create(List to, String subject, String content, EmailTemplateType emailTemplateType) { return EmailDto.builder() .to(to) .subject(subject) .content(content) + .emailTemplateType(emailTemplateType) .build(); } diff --git a/src/main/java/page/clab/api/global/common/email/exception/MessageSendingFailedException.java b/src/main/java/page/clab/api/global/common/email/exception/MessageSendingFailedException.java new file mode 100644 index 000000000..08b8ff787 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/email/exception/MessageSendingFailedException.java @@ -0,0 +1,10 @@ +package page.clab.api.global.common.email.exception; + +public class MessageSendingFailedException extends RuntimeException { + + public MessageSendingFailedException(String message) { + super(message); + } + +} + diff --git a/src/main/java/page/clab/api/global/common/file/api/FileController.java b/src/main/java/page/clab/api/global/common/file/api/FileController.java index d4c1f3e84..52ee5d63c 100644 --- a/src/main/java/page/clab/api/global/common/file/api/FileController.java +++ b/src/main/java/page/clab/api/global/common/file/api/FileController.java @@ -2,9 +2,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import java.io.File; -import java.io.IOException; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -17,15 +14,18 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.file.application.FileService; import page.clab.api.global.common.file.dto.request.DeleteFileRequestDto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; +import java.io.IOException; +import java.util.List; + @RestController -@RequestMapping("/files") +@RequestMapping("/api/v1/files") @RequiredArgsConstructor @Tag(name = "UploadedFile", description = "파일 업로드") @Slf4j @@ -33,122 +33,104 @@ public class FileController { private final FileService fileService; - @Operation(summary = "[U] 게시글 사진 업로드", description = "ROLE_USER 이상의 권한이 필요함") + @Operation(summary = "[U] 게시글 파일 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/boards", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel boardUpload( + public ApiResponse> boardUpload( @RequestParam(name = "multipartFile") List multipartFiles, @RequestParam(name = "storagePeriod") long storagePeriod ) throws IOException, PermissionDeniedException { List responseDtos = fileService.saveFiles(multipartFiles, "boards", storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 뉴스 사진 업로드", description = "ROLE_ADMIN 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/news", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel newsUpload( + public ApiResponse> newsUpload( @RequestParam(name = "multipartFile") List multipartFiles, @RequestParam(name = "storagePeriod") long storagePeriod ) throws IOException, PermissionDeniedException { List responseDtos = fileService.saveFiles(multipartFiles, "news", storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 멤버 프로필 사진 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/profiles", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel profileUpload( + public ApiResponse profileUpload( @RequestParam(name = "multipartFile") MultipartFile multipartFile, @RequestParam(name = "storagePeriod") long storagePeriod ) throws IOException, PermissionDeniedException { - UploadedFileResponseDto responseDto = fileService.saveProfileFile(multipartFile, storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDto); - return responseModel; + UploadedFileResponseDto responseDto = fileService.saveFile(multipartFile, fileService.buildPath("profiles"), storagePeriod); + return ApiResponse.success(responseDto); } @Operation(summary = "[U] 함께하는 활동 사진 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/activity-photos", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel activityUpload( + public ApiResponse> activityUpload( @RequestParam(name = "multipartFile") List multipartFiles, @RequestParam(name = "storagePeriod") long storagePeriod ) throws IOException, PermissionDeniedException { List responseDtos = fileService.saveFiles(multipartFiles, "activity-photos", storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 멤버 클라우드 파일 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/members", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel memberCloudUpload( + public ApiResponse memberCloudUpload( @RequestParam(name = "multipartFile") List multipartFiles, @RequestParam(name = "storagePeriod") long storagePeriod ) throws IOException, PermissionDeniedException { - List responseDtos = fileService.saveCloudFiles(multipartFiles, storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + List responseDtos = fileService.saveFiles(multipartFiles, fileService.buildPath("members"), storagePeriod); + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 양식 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/forms", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel formUpload( + public ApiResponse> formUpload( @RequestParam("multipartFile") List multipartFiles, @RequestParam("storagePeriod") long storagePeriod ) throws IOException, PermissionDeniedException { List responseDtos = fileService.saveFiles(multipartFiles, "forms", storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 활동 그룹 과제 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/assignment/{activityGroupId}/{activityGroupBoardId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel assignmentUpload( + public ApiResponse> assignmentUpload( @PathVariable(name = "activityGroupId") Long activityGroupId, @PathVariable(name = "activityGroupBoardId") Long activityGroupBoardId, @RequestParam(name = "multipartFile") List multipartFiles, @RequestParam(name = "storagePeriod") long storagePeriod ) throws PermissionDeniedException, IOException, NotFoundException { - List responseDtos = fileService.saveAssignmentFiles(multipartFiles, activityGroupId, activityGroupBoardId, storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + List responseDtos = fileService.saveFiles(multipartFiles, fileService.buildPath("assignments", activityGroupId, activityGroupBoardId), storagePeriod); + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 회비 증빙 사진 업로드", description = "ROLE_USER 이상의 권한이 필요함") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @PostMapping(value = "/membership-fee", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseModel assignmentUpload( + public ApiResponse> assignmentUpload( @RequestParam(name = "multipartFile") List multipartFiles, @RequestParam(name = "storagePeriod") long storagePeriod ) throws PermissionDeniedException, IOException, NotFoundException { - List responseDtos = fileService.saveFiles(multipartFiles, "membership-fee", storagePeriod); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(responseDtos); - return responseModel; + List responseDtos = fileService.saveFiles(multipartFiles, "membership-fees", storagePeriod); + return ApiResponse.success(responseDtos); } @Operation(summary = "[U] 파일 삭제", description = "ROLE_USER 이상의 권한이 필요함
" + "본인 외의 정보는 ROLE_SUPER만 가능
" + "/resources/files/~를 입력. 즉 생성시 전달받은 url을 입력.") @Secured({"ROLE_USER", "ROLE_ADMIN", "ROLE_SUPER"}) @DeleteMapping("") - public ResponseModel deleteFile(@RequestBody DeleteFileRequestDto deleteFileRequestDto) + public ApiResponse deleteFile(@RequestBody DeleteFileRequestDto deleteFileRequestDto) throws PermissionDeniedException { String deletedFileUrl = fileService.deleteFile(deleteFileRequestDto); - ResponseModel responseModel = ResponseModel.builder().build(); - responseModel.addData(deletedFileUrl); - return responseModel; + return ApiResponse.success(deletedFileUrl); } } diff --git a/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java b/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java index e8e94a148..25e30269b 100644 --- a/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java +++ b/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java @@ -1,9 +1,5 @@ package page.clab.api.global.common.file.application; -import java.io.File; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -12,6 +8,11 @@ import page.clab.api.global.common.file.dao.UploadFileRepository; import page.clab.api.global.common.file.domain.UploadedFile; +import java.io.File; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + @Service @RequiredArgsConstructor @Slf4j @@ -23,54 +24,66 @@ public class AutoDeleteService { private String filePath; @Scheduled(cron = "0 0 0 * * *") - public void autoDeleteFiles() { + public void autoDeleteExpiredFiles() { + LocalDateTime currentDate = LocalDateTime.now(); List categoryPaths = Arrays.asList( "boards", "news", "books", "profiles", "activity-photos", "members", "forms", "attendance" ); + categoryPaths.stream() .map(category -> filePath + File.separator + category) - .forEach(this::deleteFilesInDirectory); + .forEach(directoryPath -> deleteUselessFilesInDirectory(new File(directoryPath), currentDate)); } - private void deleteFilesInDirectory(String directoryPath) { - File directory = new File(directoryPath); + private void deleteUselessFilesInDirectory(File directory, LocalDateTime currentDate) { if (!directory.exists()) { - log.info("No Directory : " + directory); + log.info("Directory does not exist: {}", directory); return; } - processFilesInDirectory(directory); - } - private void processFilesInDirectory(File directory) { File[] files = directory.listFiles(); if (files == null) { - log.info("No file in Directory : " + directory); + log.info("No files in directory: {}", directory); + return; } + for (File file : files) { if (file.isDirectory()) { - processFilesInDirectory(file); + deleteUselessFilesInDirectory(file, currentDate); } else { - log.info(file.getAbsolutePath() + "file found"); - processFile(file); + checkAndDeleteFileIfExpired(file, currentDate); + checkAndDeleteFileIfInformationDoesNotExistInDB(file); } } } - private void processFile(File file) { - LocalDateTime currentDate = LocalDateTime.now(); + private void checkAndDeleteFileIfExpired(File file, LocalDateTime currentDate) { UploadedFile uploadedFile = uploadFileRepository.findBySavedPath(file.getAbsolutePath()); if (uploadedFile == null) { - log.info("No UploadedFile in DB : " + file.getAbsolutePath()); + log.info("No matching UploadedFile record in DB for file: {}", file.getAbsolutePath()); return; } - LocalDateTime fileCreatedAt = uploadedFile.getCreatedAt(); - long storagePeriod = uploadedFile.getStoragePeriod(); - if (fileCreatedAt.plusDays(storagePeriod).isBefore(currentDate)) { - boolean deleted = file.delete(); - if (!deleted) { - log.info("File Delete Error : " + file.getAbsolutePath()); - } + + LocalDateTime expirationDate = uploadedFile.getCreatedAt().plusDays(uploadedFile.getStoragePeriod()); + if (currentDate.isAfter(expirationDate)) { + deleteFile(file); + } + } + + private void checkAndDeleteFileIfInformationDoesNotExistInDB(File file) { + UploadedFile uploadedFile = uploadFileRepository.findBySavedPath(file.getAbsolutePath()); + if (uploadedFile == null) { + deleteFile(file); + } + } + + private void deleteFile(File file) { + boolean deleted = file.delete(); + if (deleted) { + log.info("Deleted unknown file: {}", file.getAbsolutePath()); + } else { + log.error("Failed to delete unknown file: {}", file.getAbsolutePath()); } } diff --git a/src/main/java/page/clab/api/global/common/file/application/FileHandler.java b/src/main/java/page/clab/api/global/common/file/application/FileHandler.java index b29e5f138..dbf6b970a 100644 --- a/src/main/java/page/clab/api/global/common/file/application/FileHandler.java +++ b/src/main/java/page/clab/api/global/common/file/application/FileHandler.java @@ -8,7 +8,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.common.file.exception.FileUploadFailException; import page.clab.api.global.util.ImageCompressionUtil; @@ -48,51 +47,36 @@ public void init() { filePath = filePath.replace("/", File.separator).replace("\\", File.separator); } - public String saveQRCodeImage(byte[] image, String category, String originalFilename, String extension, UploadedFile uploadedFile) throws IOException { + public void saveQRCodeImage(byte[] image, String category, String saveFilename, String extension) throws IOException { init(); - - fileValidation(originalFilename, extension); - String saveFilename = makeSaveFileName(extension); String savePath = filePath + File.separator + category + File.separator + saveFilename; - ByteArrayInputStream inputStream = new ByteArrayInputStream(image); BufferedImage bufferedImage = ImageIO.read(inputStream); - File file = new File(savePath); - checkDir(file); + ensureParentDirectoryExists(file); ImageIO.write(bufferedImage, extension, file); - save(file, savePath, extension); - uploadedFile.setFileSize(file.length()); - uploadedFile.setSavedPath(savePath); - uploadedFile.setSaveFileName(saveFilename); - uploadedFile.setCategory(category); - return "/" + saveFilename; } - public String saveFile(MultipartFile multipartFile, String category, UploadedFile uploadedFile) throws IOException { + public String saveFile(MultipartFile multipartFile, String category) throws IOException { init(); - String originalFilename = multipartFile.getOriginalFilename(); String extension = FilenameUtils.getExtension(originalFilename); - fileValidation(originalFilename, extension); - String saveFilename = makeSaveFileName(extension); + validateFileAttributes(originalFilename, extension); + + String saveFilename = makeFileName(extension); String savePath = filePath + File.separator + category + File.separator + saveFilename; File file = new File(savePath); - checkDir(file); + ensureParentDirectoryExists(file); multipartFile.transferTo(file); - save(file, savePath, extension); - - uploadedFile.setSavedPath(savePath); - uploadedFile.setSaveFileName(saveFilename); - uploadedFile.setCategory(category); - return "/" + saveFilename; + setFilePermissions(file, savePath, extension); + return savePath; } - private void fileValidation(String originalFilename, String extension) throws FileUploadFailException { + + private void validateFileAttributes(String originalFilename, String extension) throws FileUploadFailException { if (!validateFilename(originalFilename)) { throw new FileUploadFailException("허용되지 않은 파일명 : " + originalFilename); } - if (!validateExtension(extension)) { throw new FileUploadFailException("허용되지 않은 확장자 : " + originalFilename); } @@ -106,17 +90,17 @@ private boolean validateFilename(String fileName) { return !Strings.isNullOrEmpty(fileName); } - private String makeSaveFileName(String extension){ + public String makeFileName(String extension) { return (System.nanoTime() + "_" + UUID.randomUUID() + "." + extension); } - private void checkDir(File file){ + private void ensureParentDirectoryExists(File file) { if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } } - private void save(File file, String savePath, String extension) throws FileUploadFailException { + private void setFilePermissions(File file, String savePath, String extension) throws FileUploadFailException { try { String os = System.getProperty("os.name").toLowerCase(); if (compressibleImageExtensions.contains(extension.toLowerCase())) { @@ -134,4 +118,12 @@ private void save(File file, String savePath, String extension) throws FileUploa } } + public void deleteFile(String savedPath) { + File fileToDelete = new File(savedPath); + boolean deleted = fileToDelete.delete(); + if (!deleted) { + log.info("[{}] 파일을 삭제하는데 실패했습니다.", savedPath); + } + } + } diff --git a/src/main/java/page/clab/api/global/common/file/application/FileService.java b/src/main/java/page/clab/api/global/common/file/application/FileService.java index bc902654f..77e3142d2 100644 --- a/src/main/java/page/clab/api/global/common/file/application/FileService.java +++ b/src/main/java/page/clab/api/global/common/file/application/FileService.java @@ -12,7 +12,6 @@ import page.clab.api.domain.member.application.MemberCloudService; import page.clab.api.domain.member.application.MemberService; import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.common.file.dao.UploadFileRepository; import page.clab.api.global.common.file.domain.UploadedFile; import page.clab.api.global.common.file.dto.request.DeleteFileRequestDto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; @@ -24,8 +23,8 @@ import java.io.File; import java.io.IOException; -import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; @@ -34,14 +33,14 @@ @Slf4j public class FileService { - private final UploadFileRepository uploadFileRepository; - private final FileHandler fileHandler; private final MemberService memberService; private final MemberCloudService memberCloudService; + private final UploadedFileService uploadedFileService; + private final ActivityGroupRepository activityGroupRepository; private final GroupMemberRepository groupMemberRepository; @@ -58,41 +57,19 @@ public class FileService { private String maxFileSize; public String saveQRCodeImage(byte[] QRCodeImage, String path, long storagePeriod, String nowDateTime) throws IOException { - Member member = memberService.getCurrentMember(); - UploadedFile uploadedFile = new UploadedFile(); - + Member currentMember = memberService.getCurrentMember(); String extension = "png"; - String originalFileName = path.replace(File.separator.toString(), "-") + nowDateTime; - String url = fileURL + "/" + path.replace(File.separator.toString(), "/") - + fileHandler.saveQRCodeImage(QRCodeImage, path, originalFileName, extension, uploadedFile); - - uploadedFile.setOriginalFileName(originalFileName); - uploadedFile.setStoragePeriod(storagePeriod); - uploadedFile.setContentType("image/png"); - uploadedFile.setUploader(member); - uploadedFile.setUrl(url); - uploadFileRepository.save(uploadedFile); + String originalFileName = path.replace(File.separator, "-") + nowDateTime; + String saveFilename = fileHandler.makeFileName(extension); + String savePath = filePath + File.separator + path + File.separator + saveFilename; + String url = fileURL + "/" + path.replace(File.separator, "/") + "/" + saveFilename; + + fileHandler.saveQRCodeImage(QRCodeImage, path, saveFilename, extension); + UploadedFile uploadedFile = UploadedFile.create(currentMember, originalFileName, saveFilename, savePath, url, (long) QRCodeImage.length, "image/png", storagePeriod, path); + uploadedFileService.saveUploadedFile(uploadedFile); return url; } - public List saveAssignmentFiles(List multipartFiles, Long activityGroupId, Long activityGroupBoardId, long storagePeriod) throws PermissionDeniedException, IOException { - Member member = memberService.getCurrentMember(); - String path = "assignment" + File.separator + activityGroupId + File.separator+ activityGroupBoardId + File.separator + member.getId(); - return saveFiles(multipartFiles, path, storagePeriod); - } - - public UploadedFileResponseDto saveProfileFile(MultipartFile multipartFile, long storagePeriod) throws PermissionDeniedException, IOException { - Member member = memberService.getCurrentMember(); - String path = "profiles" + File.separator + member.getId(); - return saveFile(multipartFile, path, storagePeriod); - } - - public List saveCloudFiles(List multipartFiles, long storagePeriod) throws PermissionDeniedException, IOException { - Member member = memberService.getCurrentMember(); - String path = "members" + File.separator + member.getId(); - return saveFiles(multipartFiles, path, storagePeriod); - } - public List saveFiles(List multipartFiles, String path, long storagePeriod) throws IOException, PermissionDeniedException { List uploadedFileResponseDtos = new ArrayList<>(); for (MultipartFile multipartFile : multipartFiles) { @@ -103,126 +80,81 @@ public List saveFiles(List multipartFile } public UploadedFileResponseDto saveFile(MultipartFile multipartFile, String path, long storagePeriod) throws IOException, PermissionDeniedException { - Member member = memberService.getCurrentMember(); - - if (!isValidPathVariable(path)) { - throw new NotFoundException("파일 업로드 api 요청 pathVariable이 유효하지 않습니다."); - } + Member currentMember = memberService.getCurrentMember(); - if (!path.startsWith("membership-fee") && path.startsWith("members")) { - String memberId = path.split(Pattern.quote(File.separator))[1]; - double usage = memberCloudService.getCloudUsageByMemberId(memberId).getUsage(); - if (multipartFile.getSize() + usage > FileSystemUtil.convertToBytes(maxFileSize)) { - throw new CloudStorageNotEnoughException("클라우드 저장 공간이 부족합니다."); - } - } + validatePathVariable(path); + validateMemberCloudUsage(multipartFile, path); + checkAndRemoveExistingFile(path); - UploadedFile existingUploadedFile = getUniqueUploadedFileByCategoryAndOriginalName(path, multipartFile.getOriginalFilename()); - if (existingUploadedFile != null) { - deleteFileBySavedPath(existingUploadedFile.getSavedPath()); - } + String savedFilePath = fileHandler.saveFile(multipartFile, path); + String fileName = new File(savedFilePath).getName(); + String url = fileURL + "/" + path.replace(File.separator, "/") + "/" + fileName; - if (path.startsWith("profiles")) { - UploadedFile profileFile = getUniqueUploadedFileByCategory(path); - if (profileFile != null) { - deleteFileBySavedPath(profileFile.getSavedPath()); - } - } - - UploadedFile uploadedFile = new UploadedFile(); - String url = fileURL + "/" + path.replace(File.separator.toString(), "/") + fileHandler.saveFile(multipartFile, path, uploadedFile); - uploadedFile.setOriginalFileName(multipartFile.getOriginalFilename()); - uploadedFile.setStoragePeriod(storagePeriod); - uploadedFile.setFileSize(multipartFile.getSize()); - uploadedFile.setContentType(multipartFile.getContentType()); - uploadedFile.setUploader(member); - uploadedFile.setUrl(url); - uploadFileRepository.save(uploadedFile); - - return UploadedFileResponseDto.builder() - .fileUrl(url) - .originalFileName(uploadedFile.getOriginalFileName()) - .createdAt(uploadedFile.getCreatedAt()) - .storagePeriod(uploadedFile.getStoragePeriod()) - .build(); - } - - public boolean isValidPathVariable(String path) throws AssignmentFileUploadFailException { - switch (path.split(Pattern.quote(File.separator))[0]) { - case "assignment" : { - Long activityGroupId = Long.parseLong(path.split(Pattern.quote(File.separator))[1]); - Long activityGroupBoardId = Long.parseLong(path.split(Pattern.quote(File.separator))[2]); - String memberId = path.split(Pattern.quote(File.separator))[3]; - Member assignmentWriter = memberService.getMemberById(memberId); - if (!activityGroupRepository.existsById(activityGroupId)) { - throw new AssignmentFileUploadFailException("해당 활동은 존재하지 않습니다."); - } - if (!groupMemberRepository.existsByMemberAndActivityGroupId(assignmentWriter, activityGroupId)) { - throw new AssignmentFileUploadFailException("해당 활동에 참여하고 있지 않은 멤버입니다."); - } - if (!activityGroupBoardRepository.existsById(activityGroupBoardId)) { - throw new AssignmentFileUploadFailException("해당 활동그룹 게시판이 존재하지 않습니다."); - } - return true; - } - } - return true; + UploadedFile uploadedFile = UploadedFile.create(currentMember, multipartFile.getOriginalFilename(), fileName, savedFilePath, url, multipartFile.getSize(), multipartFile.getContentType(), storagePeriod, path); + uploadedFileService.saveUploadedFile(uploadedFile); + return UploadedFileResponseDto.toDto(uploadedFile); } public String deleteFile(DeleteFileRequestDto deleteFileRequestDto) throws PermissionDeniedException { - Member member = memberService.getCurrentMember(); - String url = deleteFileRequestDto.getUrl(); - UploadedFile uploadedFile = getUploadedFileByUrl(url); + Member currentMember = memberService.getCurrentMember(); + UploadedFile uploadedFile = uploadedFileService.getUploadedFileByUrl(deleteFileRequestDto.getUrl()); String filePath = uploadedFile.getSavedPath(); File storedFile = new File(filePath); - if (uploadedFile == null || !storedFile.exists()) { + if (!storedFile.exists()) { throw new NotFoundException("존재하지 않는 파일입니다."); } - if (!(uploadedFile.getUploader().getId().equals(member.getId()) || member.isSuperAdminRole())) { - throw new PermissionDeniedException("해당 파일을 삭제할 권한이 없습니다."); - } - if (!storedFile.delete()) { - log.info("파일 삭제 오류 : {}", filePath); - } - String deletedFileUrl = uploadedFile.getUrl(); - deleteFileBySavedPath(filePath); - return deletedFileUrl; + uploadedFile.validateAccessPermission(currentMember); + fileHandler.deleteFile(uploadedFile.getSavedPath()); + return uploadedFile.getUrl(); } - public LocalDateTime getStorageDateTimeOfFile(String fileUrl) { - UploadedFile uploadedFile = getUploadedFileByUrl(fileUrl); - if (uploadedFile == null) { - throw new NotFoundException("파일이 존재하지 않습니다."); + public String buildPath(String baseDirectory, Long... additionalSegments) { + Member currentMember = memberService.getCurrentMember(); + StringBuilder pathBuilder = new StringBuilder(baseDirectory); + for (Long segment : additionalSegments) { + pathBuilder.append(File.separator).append(segment); } - LocalDateTime now = LocalDateTime.now(); - LocalDateTime createdDateTime = uploadedFile.getCreatedAt(); - Long storagePeriod = uploadedFile.getStoragePeriod(); - - return createdDateTime.plusDays(storagePeriod); + pathBuilder.append(File.separator).append(currentMember.getId()); + return pathBuilder.toString(); } - public UploadedFile getUploadedFileByUrl(String url) { - return uploadFileRepository.findByUrl(url) - .orElseThrow(() -> new NotFoundException("파일을 찾을 수 없습니다.")); - } - - public UploadedFile getUniqueUploadedFileByCategoryAndOriginalName(String category, String originalName) { - return uploadFileRepository.findTopByCategoryAndOriginalFileNameOrderByCreatedAtDesc(category, originalName); - } - - public UploadedFile getUniqueUploadedFileByCategory(String category) { - return uploadFileRepository.findTopByCategoryOrderByCreatedAtDesc(category); + public void validatePathVariable(String path) throws AssignmentFileUploadFailException { + if (path.split(Pattern.quote(File.separator))[0].equals("assignment")) { + Long activityGroupId = Long.parseLong(path.split(Pattern.quote(File.separator))[1]); + Long activityGroupBoardId = Long.parseLong(path.split(Pattern.quote(File.separator))[2]); + String memberId = path.split(Pattern.quote(File.separator))[3]; + Member assignmentWriter = memberService.getMemberById(memberId); + if (!activityGroupRepository.existsById(activityGroupId)) { + throw new AssignmentFileUploadFailException("해당 활동은 존재하지 않습니다."); + } + if (!groupMemberRepository.existsByMemberAndActivityGroupId(assignmentWriter, activityGroupId)) { + throw new AssignmentFileUploadFailException("해당 활동에 참여하고 있지 않은 멤버입니다."); + } + if (!activityGroupBoardRepository.existsById(activityGroupBoardId)) { + throw new AssignmentFileUploadFailException("해당 활동 그룹 게시판이 존재하지 않습니다."); + } + } } - public void deleteFileBySavedPath(String savedPath) { - File existingFile = new File(savedPath); - if (existingFile != null) { - existingFile.delete(); + private void validateMemberCloudUsage(MultipartFile multipartFile, String path) throws PermissionDeniedException { + if (path.split(Pattern.quote(File.separator))[0].equals("members")) { + String memberId = path.split(Pattern.quote(File.separator))[1]; + double usage = memberCloudService.getCloudUsageByMemberId(memberId).getUsage(); + if (multipartFile.getSize() + usage > FileSystemUtil.convertToBytes(maxFileSize)) { + throw new CloudStorageNotEnoughException("클라우드 저장 공간이 부족합니다."); + } } } - public String getOriginalFileNameByUrl(String url) { - return getUploadedFileByUrl(url).getOriginalFileName(); + private void checkAndRemoveExistingFile(String path) { + List validPrefixes = Arrays.asList("profiles", "members/", "assignments"); + boolean shouldDelete = validPrefixes.stream().anyMatch(path::startsWith); + if (shouldDelete) { + UploadedFile fileToDelete = uploadedFileService.getUniqueUploadedFileByCategory(path); + if (fileToDelete != null) { + fileHandler.deleteFile(fileToDelete.getSavedPath()); + } + } } } diff --git a/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java b/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java new file mode 100644 index 000000000..597e56436 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java @@ -0,0 +1,48 @@ +package page.clab.api.global.common.file.application; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import page.clab.api.global.common.file.dao.UploadFileRepository; +import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.exception.NotFoundException; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UploadedFileService { + + private final UploadFileRepository uploadFileRepository; + + public UploadedFile saveUploadedFile(UploadedFile uploadedFile) { + return uploadFileRepository.save(uploadedFile); + } + + public UploadedFile getUploadedFileByUrl(String url) { + return uploadFileRepository.findByUrl(url) + .orElseThrow(() -> new NotFoundException("파일을 찾을 수 없습니다.")); + } + + public UploadedFile getUniqueUploadedFileByCategoryAndOriginalName(String category, String originalName) { + return uploadFileRepository.findTopByCategoryAndOriginalFileNameOrderByCreatedAtDesc(category, originalName); + } + + public UploadedFile getUniqueUploadedFileByCategory(String category) { + return uploadFileRepository.findTopByCategoryOrderByCreatedAtDesc(category); + } + + public List getUploadedFilesByUrls(List fileUrls) { + if (fileUrls == null || fileUrls.isEmpty()) { + return new ArrayList<>(); + } + List uploadedFiles = uploadFileRepository.findAllByUrlIn(fileUrls); + if (uploadedFiles.size() != fileUrls.size()) { + throw new NotFoundException("서버에 업로드되지 않은 파일이 포함되어 있습니다."); + } + return uploadedFiles; + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/file/dao/UploadFileRepository.java b/src/main/java/page/clab/api/global/common/file/dao/UploadFileRepository.java index 9452da215..08f0d5ad3 100644 --- a/src/main/java/page/clab/api/global/common/file/dao/UploadFileRepository.java +++ b/src/main/java/page/clab/api/global/common/file/dao/UploadFileRepository.java @@ -1,9 +1,11 @@ package page.clab.api.global.common.file.dao; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import page.clab.api.global.common.file.domain.UploadedFile; +import java.util.List; +import java.util.Optional; + public interface UploadFileRepository extends JpaRepository { UploadedFile findBySavedPath(String savedPath); @@ -14,4 +16,6 @@ public interface UploadFileRepository extends JpaRepository UploadedFile findTopByCategoryOrderByCreatedAtDesc(String category); + List findAllByUrlIn(List fileUrls); + } diff --git a/src/main/java/page/clab/api/global/common/file/domain/UploadedFile.java b/src/main/java/page/clab/api/global/common/file/domain/UploadedFile.java index 6af9253ec..c60bf39a4 100644 --- a/src/main/java/page/clab/api/global/common/file/domain/UploadedFile.java +++ b/src/main/java/page/clab/api/global/common/file/domain/UploadedFile.java @@ -7,22 +7,23 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import java.time.LocalDateTime; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.domain.BaseEntity; +import page.clab.api.global.exception.PermissionDeniedException; @Entity @Getter @Setter @Builder -@AllArgsConstructor -@NoArgsConstructor -public class UploadedFile { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UploadedFile extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -53,10 +54,30 @@ public class UploadedFile { @Column(nullable = false) private String category; - @CreationTimestamp - @Column(name = "created_at", updatable = false) - private LocalDateTime createdAt; - private Long storagePeriod; + public static UploadedFile create(Member uploader, String originalFileName, String saveFileName, String savedPath, String url, Long fileSize, String contentType, Long storagePeriod, String category) { + return UploadedFile.builder() + .uploader(uploader) + .originalFileName(originalFileName) + .saveFileName(saveFileName) + .savedPath(savedPath) + .url(url) + .fileSize(fileSize) + .contentType(contentType) + .storagePeriod(storagePeriod) + .category(category) + .build(); + } + + public boolean isOwner(Member member) { + return this.uploader.isSameMember(member); + } + + public void validateAccessPermission(Member member) throws PermissionDeniedException { + if (!isOwner(member) && !member.isSuperAdminRole()) { + throw new PermissionDeniedException("해당 파일을 삭제할 권한이 없습니다."); + } + } + } diff --git a/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java b/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java index 4c6fb1efc..232628ff9 100644 --- a/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java +++ b/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java @@ -2,16 +2,12 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class DeleteFileRequestDto { @@ -19,4 +15,10 @@ public class DeleteFileRequestDto { @Schema(description = "파일경로", example = "/resources/files/forms/123456.png", required = true) private String url; + public static DeleteFileRequestDto create(String url) { + return DeleteFileRequestDto.builder() + .url(url) + .build(); + } + } diff --git a/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java b/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java index 099365dbc..6f52a9c41 100644 --- a/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java +++ b/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java @@ -1,17 +1,12 @@ package page.clab.api.global.common.file.dto.response; -import java.io.File; -import java.util.Date; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; + +import java.io.File; +import java.util.Date; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class FileInfo { @@ -23,7 +18,7 @@ public class FileInfo { private Date modificationDate; - public static FileInfo of(File file) { + public static FileInfo toDto(File file) { if (file == null || !file.exists() || !file.isFile()) { return null; } diff --git a/src/main/java/page/clab/api/global/common/file/dto/response/UploadFileResponseDto.java b/src/main/java/page/clab/api/global/common/file/dto/response/UploadFileResponseDto.java deleted file mode 100644 index d797e2f5f..000000000 --- a/src/main/java/page/clab/api/global/common/file/dto/response/UploadFileResponseDto.java +++ /dev/null @@ -1,45 +0,0 @@ -package page.clab.api.global.common.file.dto.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import page.clab.api.global.common.file.domain.UploadedFile; -import page.clab.api.global.util.ModelMapperUtil; - -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class UploadFileResponseDto { - - private Long id; - - private String uploaderId; - - private String uploaderName; - - private String originalFileName; - - private String saveFileName; - - private String savePath; - - private Long fileSize; - - private String contentType; - - private String category; - - public static UploadFileResponseDto of(UploadedFile uploadedFile) { - UploadFileResponseDto uploadFileResponseDto = ModelMapperUtil.getModelMapper().map(uploadedFile, UploadFileResponseDto.class); - if (uploadedFile.getUploader() != null) { - uploadFileResponseDto.setUploaderId(uploadedFile.getUploader().getId()); - uploadFileResponseDto.setUploaderName(uploadedFile.getUploader().getName()); - } - return uploadFileResponseDto; - } - -} diff --git a/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java b/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java index 9da99dea1..2224279f3 100644 --- a/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java +++ b/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java @@ -1,18 +1,13 @@ package page.clab.api.global.common.file.dto.response; -import java.time.LocalDateTime; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; import page.clab.api.global.common.file.domain.UploadedFile; -import page.clab.api.global.util.ModelMapperUtil; + +import java.time.LocalDateTime; +import java.util.List; @Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor @Builder public class UploadedFileResponseDto { @@ -24,8 +19,19 @@ public class UploadedFileResponseDto { private LocalDateTime createdAt; - public static UploadedFileResponseDto of(UploadedFile uploadedFile) { - return ModelMapperUtil.getModelMapper().map(uploadedFile, UploadedFileResponseDto.class); + public static List toDto(List uploadedFiles) { + return uploadedFiles.stream() + .map(UploadedFileResponseDto::toDto) + .toList(); + } + + public static UploadedFileResponseDto toDto(UploadedFile uploadedFile) { + return UploadedFileResponseDto.builder() + .fileUrl(uploadedFile.getUrl()) + .originalFileName(uploadedFile.getOriginalFileName()) + .storagePeriod(uploadedFile.getStoragePeriod()) + .createdAt(uploadedFile.getCreatedAt()) + .build(); } } diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java index a80b04e05..f1380fd56 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java @@ -3,6 +3,8 @@ import com.slack.api.Slack; import com.slack.api.webhook.Payload; import com.slack.api.webhook.WebhookResponse; +import io.ipinfo.api.model.IPResponse; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -30,10 +32,14 @@ public class SlackService { private final Slack slack = Slack.getInstance(); + private final String webhookUrl; - public SlackService(@Value("${slack.webhook.url}") String webhookUrl) { + private final AttributeStrategy attributeStrategy; + + public SlackService(@Value("${slack.webhook.url}") String webhookUrl, AttributeStrategy attributeStrategy) { this.webhookUrl = webhookUrl; + this.attributeStrategy = attributeStrategy; } public CompletableFuture sendServerErrorNotification(HttpServletRequest request, Exception e) { @@ -55,9 +61,11 @@ public CompletableFuture sendSecurityAlertNotification(HttpServletReque String requestUrl = request.getRequestURI(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); + IPResponse ipResponse = attributeStrategy.getAttribute(request); + String location = ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); - String message = String.format(":red_circle: *%s [%s] - %s*\n>*User*: %s\n>*Endpoint*: %s\n>*Details*: `%s`\n>```%s```", - alertType.getTitle(), clientIpAddress, serverTime, username, requestUrl, alertType.getDefaultMessage(), additionalMessage); + String message = String.format(":red_circle: *%s [%s] - %s*\n>*User*: %s\n>*Location*: %s\n>*Endpoint*: %s\n>*Details*: `%s`\n>```%s```", + alertType.getTitle(), clientIpAddress, serverTime, username, location, requestUrl, alertType.getDefaultMessage(), additionalMessage); return sendSlackMessage(message); } diff --git a/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java b/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java new file mode 100644 index 000000000..b6bbd48a4 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java @@ -0,0 +1,48 @@ +package page.clab.api.global.common.verification.application; + +import lombok.RequiredArgsConstructor; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.stereotype.Service; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.verification.dao.VerificationRepository; +import page.clab.api.global.common.verification.domain.Verification; +import page.clab.api.global.common.verification.dto.request.VerificationRequestDto; +import page.clab.api.global.exception.NotFoundException; + +import java.security.SecureRandom; + +@Service +@RequiredArgsConstructor +public class VerificationService { + + private final VerificationRepository verificationRepository; + + public Verification getVerificationCode(String verificationCode) { + return verificationRepository.findByVerificationCode(verificationCode) + .orElseThrow(() -> new NotFoundException("존재하지 않는 인증코드입니다.")); + } + + public void saveVerificationCode(String memberId, String verificationCode) { + Verification code = Verification.create(memberId, verificationCode); + verificationRepository.save(code); + } + + public void deleteVerificationCode(String verificationCode) { + verificationRepository.findByVerificationCode(verificationCode) + .ifPresent(verificationRepository::delete); + } + + public Verification validateVerificationCode(VerificationRequestDto verificationRequestDto, Member member) { + Verification verification = getVerificationCode(verificationRequestDto.getVerificationCode()); + verification.validateRequest(member.getId()); + return verification; + } + + public String generateVerificationCode() { + SecureRandom secureRandom = new SecureRandom(); + byte[] codeBytes = new byte[9]; + secureRandom.nextBytes(codeBytes); + return Base64.encodeBase64URLSafeString(codeBytes); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/verification/dao/VerificationRepository.java b/src/main/java/page/clab/api/global/common/verification/dao/VerificationRepository.java new file mode 100644 index 000000000..eea285656 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/verification/dao/VerificationRepository.java @@ -0,0 +1,14 @@ +package page.clab.api.global.common.verification.dao; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; +import page.clab.api.global.common.verification.domain.Verification; + +import java.util.Optional; + +@Repository +public interface VerificationRepository extends CrudRepository { + + Optional findByVerificationCode(String verificationCode); + +} diff --git a/src/main/java/page/clab/api/global/common/verificationCode/domain/VerificationCode.java b/src/main/java/page/clab/api/global/common/verification/domain/Verification.java similarity index 56% rename from src/main/java/page/clab/api/global/common/verificationCode/domain/VerificationCode.java rename to src/main/java/page/clab/api/global/common/verification/domain/Verification.java index f22cb00d4..0703e77cd 100644 --- a/src/main/java/page/clab/api/global/common/verificationCode/domain/VerificationCode.java +++ b/src/main/java/page/clab/api/global/common/verification/domain/Verification.java @@ -1,7 +1,9 @@ -package page.clab.api.global.common.verificationCode.domain; +package page.clab.api.global.common.verification.domain; import jakarta.persistence.Column; import jakarta.persistence.Id; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -12,20 +14,23 @@ @Getter @Builder -@AllArgsConstructor -@NoArgsConstructor -@RedisHash(value = "verification-code", timeToLive = 60*3) -public class VerificationCode { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@RedisHash(value = "verification", timeToLive = 60*3) +public class Verification { @Id - @Column(name = "member_id") + @Column(name = "member_id", nullable = false) + @Size(min = 9, max = 9, message = "{size.verificationCode.memberId}") private String id; @Indexed + @Size(min = 12, max = 12, message = "{size.verificationCode.verificationCode}") + @Column(nullable = false) private String verificationCode; - public static VerificationCode create(String memberId, String verificationCode) { - return VerificationCode.builder() + public static Verification create(String memberId, String verificationCode) { + return Verification.builder() .id(memberId) .verificationCode(verificationCode) .build(); diff --git a/src/main/java/page/clab/api/global/common/verificationCode/dto/request/VerificationCodeRequestDto.java b/src/main/java/page/clab/api/global/common/verification/dto/request/VerificationRequestDto.java similarity index 53% rename from src/main/java/page/clab/api/global/common/verificationCode/dto/request/VerificationCodeRequestDto.java rename to src/main/java/page/clab/api/global/common/verification/dto/request/VerificationRequestDto.java index 25209c0b4..f00e1595c 100644 --- a/src/main/java/page/clab/api/global/common/verificationCode/dto/request/VerificationCodeRequestDto.java +++ b/src/main/java/page/clab/api/global/common/verification/dto/request/VerificationRequestDto.java @@ -1,28 +1,19 @@ -package page.clab.api.global.common.verificationCode.dto.request; +package page.clab.api.global.common.verification.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class VerificationCodeRequestDto { +public class VerificationRequestDto { @NotNull(message = "{notNull.verificationCode.memberId}") - @Size(min = 9, max = 9, message = "{size.verificationCode.memberId}") @Schema(description = "학번", example = "202310000", required = true) private String memberId; @NotNull(message = "{notNull.verificationCode.verificationCode}") - @Size(min = 12, max = 12, message = "{size.verificationCode.verificationCode}") @Schema(description = "인증 코드", example = "123456789012", required = true) private String verificationCode; diff --git a/src/main/java/page/clab/api/global/common/verificationCode/application/VerificationCodeService.java b/src/main/java/page/clab/api/global/common/verificationCode/application/VerificationCodeService.java deleted file mode 100644 index 2bab579fd..000000000 --- a/src/main/java/page/clab/api/global/common/verificationCode/application/VerificationCodeService.java +++ /dev/null @@ -1,48 +0,0 @@ -package page.clab.api.global.common.verificationCode.application; - -import lombok.RequiredArgsConstructor; -import org.apache.tomcat.util.codec.binary.Base64; -import org.springframework.stereotype.Service; -import page.clab.api.domain.member.domain.Member; -import page.clab.api.global.common.verificationCode.dao.VerificationCodeRepository; -import page.clab.api.global.common.verificationCode.domain.VerificationCode; -import page.clab.api.global.common.verificationCode.dto.request.VerificationCodeRequestDto; -import page.clab.api.global.exception.NotFoundException; - -import java.security.SecureRandom; - -@Service -@RequiredArgsConstructor -public class VerificationCodeService { - - private final VerificationCodeRepository verificationCodeRepository; - - public VerificationCode getVerificationCode(String verificationCode) { - return verificationCodeRepository.findByVerificationCode(verificationCode) - .orElseThrow(() -> new NotFoundException("존재하지 않는 인증코드입니다.")); - } - - public void saveVerificationCode(String memberId, String verificationCode) { - VerificationCode code = VerificationCode.create(memberId, verificationCode); - verificationCodeRepository.save(code); - } - - public void deleteVerificationCode(String verificationCode) { - verificationCodeRepository.findByVerificationCode(verificationCode) - .ifPresent(verificationCodeRepository::delete); - } - - public VerificationCode validateVerificationCode(VerificationCodeRequestDto verificationCodeRequestDto, Member member) { - VerificationCode verificationCode = getVerificationCode(verificationCodeRequestDto.getVerificationCode()); - verificationCode.validateRequest(member.getId()); - return verificationCode; - } - - public String generateVerificationCode() { - SecureRandom secureRandom = new SecureRandom(); - byte[] codeBytes = new byte[9]; - secureRandom.nextBytes(codeBytes); - return Base64.encodeBase64URLSafeString(codeBytes); - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/verificationCode/dao/VerificationCodeRepository.java b/src/main/java/page/clab/api/global/common/verificationCode/dao/VerificationCodeRepository.java deleted file mode 100644 index 5eeafb129..000000000 --- a/src/main/java/page/clab/api/global/common/verificationCode/dao/VerificationCodeRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package page.clab.api.global.common.verificationCode.dao; - -import java.util.Optional; -import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Repository; -import page.clab.api.global.common.verificationCode.domain.VerificationCode; - -@Repository -public interface VerificationCodeRepository extends CrudRepository { - - Optional findByVerificationCode(String verificationCode); - -} diff --git a/src/main/java/page/clab/api/global/config/AesConfig.java b/src/main/java/page/clab/api/global/config/AesConfig.java index 782f02f31..06246aa9c 100644 --- a/src/main/java/page/clab/api/global/config/AesConfig.java +++ b/src/main/java/page/clab/api/global/config/AesConfig.java @@ -2,7 +2,9 @@ import lombok.Getter; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; +import page.clab.api.global.util.EncryptionUtil; @Getter @Component @@ -11,8 +13,13 @@ public class AesConfig { @Value("${security.aes-key}") private String secretKey; - private int ivLengthBytes = 12; + private final int ivLengthBytes = 12; - private int gcmTagLengthBits = 128; + private final int gcmTagLengthBits = 128; + + @Bean + public EncryptionUtil encryptionUtil(AesConfig aesConfig) { + return EncryptionUtil.create(aesConfig); + } } diff --git a/src/main/java/page/clab/api/global/config/IPInfoConfig.java b/src/main/java/page/clab/api/global/config/IPInfoConfig.java new file mode 100644 index 000000000..425f51f32 --- /dev/null +++ b/src/main/java/page/clab/api/global/config/IPInfoConfig.java @@ -0,0 +1,56 @@ +package page.clab.api.global.config; + +import io.ipinfo.api.IPinfo; +import io.ipinfo.spring.IPinfoSpring; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; +import io.ipinfo.spring.strategies.attribute.SessionAttributeStrategy; +import io.ipinfo.spring.strategies.interceptor.BotInterceptorStrategy; +import io.ipinfo.spring.strategies.interceptor.InterceptorStrategy; +import io.ipinfo.spring.strategies.ip.IPStrategy; +import io.ipinfo.spring.strategies.ip.XForwardedForIPStrategy; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Getter +@Configuration +public class IPInfoConfig { + + private final RestClient restClient = RestClient.create("https://ipinfo.io/"); + + @Value("${ipinfo.access-token}") + private String accessToken; + + @Bean + public IPinfoSpring ipinfoSpring() { + return new IPinfoSpring.Builder() + .setIPinfo(ipInfo()) + .interceptorStrategy(interceptorStrategy()) + .ipStrategy(ipStrategy()) + .attributeStrategy(attributeStrategy()) + .build(); + } + + @Bean + public IPinfo ipInfo() { + return new IPinfo.Builder().setToken(accessToken).build(); + } + + @Bean + public InterceptorStrategy interceptorStrategy() { + return new BotInterceptorStrategy(); + } + + @Bean + public IPStrategy ipStrategy() { + return new XForwardedForIPStrategy(); + } + + @Bean + public AttributeStrategy attributeStrategy() { + return new SessionAttributeStrategy(); + } + +} diff --git a/src/main/java/page/clab/api/global/config/MapperConfig.java b/src/main/java/page/clab/api/global/config/MapperConfig.java deleted file mode 100644 index 415461e9e..000000000 --- a/src/main/java/page/clab/api/global/config/MapperConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package page.clab.api.global.config; - -import org.modelmapper.ModelMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class MapperConfig { - - @Bean - public ModelMapper modelMapper() { - ModelMapper modelMapper = new ModelMapper(); - modelMapper.getConfiguration() - .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE) - .setFieldMatchingEnabled(true); - return modelMapper; - } - -} diff --git a/src/main/java/page/clab/api/global/config/SecurityConfig.java b/src/main/java/page/clab/api/global/config/SecurityConfig.java index d2959a57a..9f8748bbd 100644 --- a/src/main/java/page/clab/api/global/config/SecurityConfig.java +++ b/src/main/java/page/clab/api/global/config/SecurityConfig.java @@ -62,6 +62,8 @@ public class SecurityConfig { private final OpenApiPatternsProperties OpenApiPatternsProperties; + private final IPInfoConfig ipInfoConfig; + @Value("${resource.file.url}") String fileURL; @@ -79,7 +81,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeRequests(this::configureRequests) .authenticationProvider(authenticationConfig.authenticationProvider()) .addFilterBefore( - new IpAuthenticationFilter(), + new IpAuthenticationFilter(ipInfoConfig), UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( diff --git a/src/main/java/page/clab/api/global/config/SecurityConstants.java b/src/main/java/page/clab/api/global/config/SecurityConstants.java index 133d80e9a..ccab29bc6 100644 --- a/src/main/java/page/clab/api/global/config/SecurityConstants.java +++ b/src/main/java/page/clab/api/global/config/SecurityConstants.java @@ -3,8 +3,9 @@ public class SecurityConstants { public static final String[] PERMIT_ALL = { - "/login/**", + "/api/v1/login/**", "/static/**", + "/actuator/health", "/resources/files/**", "/configuration/ui", "/configuration/security", @@ -14,23 +15,23 @@ public class SecurityConstants { }; public static final String[] PERMIT_ALL_API_ENDPOINTS_GET = { - "/applications/{studentId}", - "/recruitments", - "/news", "/news/**", - "/blogs", "/blogs/**", - "/executives", "/executives/**", - "/awards", "/awards/**", - "/activity-group", "/activity-group/**", - "/work-experiences", "/work-experiences/**", - "/products", "/products/**", - "/reviews", "/reviews/**", - "/activity-photos", "/activity-photos/**" + "/api/v1/applications/{studentId}", + "/api/v1/recruitments", + "/api/v1/news", "/api/v1/news/**", + "/api/v1/blogs", "/api/v1/blogs/**", + "/api/v1/positions", "/api/v1/positions/**", + "/api/v1/awards", "/api/v1/awards/**", + "/api/v1/activity-group", "/api/v1/activity-group/**", + "/api/v1/work-experiences", "/api/v1/work-experiences/**", + "/api/v1/products", "/api/v1/products/**", + "/api/v1/reviews", "/api/v1/reviews/**", + "/api/v1/activity-photos", "/api/v1/activity-photos/**" }; public static final String[] PERMIT_ALL_API_ENDPOINTS_POST = { - "/applications", - "/members/password-reset-requests", - "/members/password-reset-verifications", + "/api/v1/applications", + "/api/v1/members/password-reset-requests", + "/api/v1/members/password-reset-verifications", }; } diff --git a/src/main/java/page/clab/api/global/config/SuspiciousPatterns.java b/src/main/java/page/clab/api/global/config/SuspiciousPatterns.java index 3be3ac7cc..6778ad081 100644 --- a/src/main/java/page/clab/api/global/config/SuspiciousPatterns.java +++ b/src/main/java/page/clab/api/global/config/SuspiciousPatterns.java @@ -26,7 +26,7 @@ public class SuspiciousPatterns { Pattern.compile(".*\\/wp-content\\/.*"), // 관리자 페이지 및 도구들 - Pattern.compile(".*\\/(phpmyadmin|pma|admin|dbadmin|mysql|myadmin|phpMyAdmin).*"), + Pattern.compile(".*\\/(phpmyadmin|pma|dbadmin|mysql|myadmin|phpMyAdmin).*"), // 개발 관련 파일과 디렉토리 Pattern.compile(".*\\/(\\.git|\\.svn|\\.hg|\\.env|\\.idea|\\.vscode|\\.vs|\\.DS_Store).*"), diff --git a/src/main/java/page/clab/api/global/config/ThymeleafConfig.java b/src/main/java/page/clab/api/global/config/ThymeleafConfig.java new file mode 100644 index 000000000..ef47f933c --- /dev/null +++ b/src/main/java/page/clab/api/global/config/ThymeleafConfig.java @@ -0,0 +1,21 @@ +package page.clab.api.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver; +import org.thymeleaf.templatemode.TemplateMode; + +@Configuration +public class ThymeleafConfig { + + @Bean + public SpringResourceTemplateResolver springResourceTemplateResolver() { + SpringResourceTemplateResolver springResourceTemplateResolver = new SpringResourceTemplateResolver(); + springResourceTemplateResolver.setPrefix(getClass().getResource("/templates/").toString()); + springResourceTemplateResolver.setCharacterEncoding("UTF-8"); + springResourceTemplateResolver.setTemplateMode(TemplateMode.HTML); + springResourceTemplateResolver.setCacheable(false); + return springResourceTemplateResolver; + } + +} diff --git a/src/main/java/page/clab/api/global/exception/SearchResultNotExistException.java b/src/main/java/page/clab/api/global/exception/SearchResultNotExistException.java deleted file mode 100644 index 74a3667f6..000000000 --- a/src/main/java/page/clab/api/global/exception/SearchResultNotExistException.java +++ /dev/null @@ -1,15 +0,0 @@ -package page.clab.api.global.exception; - -public class SearchResultNotExistException extends NullPointerException { - - private static final String DEFAULT_MESSAGE = "검색 결과가 존재하지 않습니다."; - - public SearchResultNotExistException() { - super(DEFAULT_MESSAGE); - } - - public SearchResultNotExistException(String s) { - super(s); - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/exception/SecretKeyCreationException.java b/src/main/java/page/clab/api/global/exception/SecretKeyCreationException.java deleted file mode 100644 index f6e67609a..000000000 --- a/src/main/java/page/clab/api/global/exception/SecretKeyCreationException.java +++ /dev/null @@ -1,15 +0,0 @@ -package page.clab.api.global.exception; - -public class SecretKeyCreationException extends RuntimeException { - - private static final String DEFAULT_MESSAGE = "SecretKey 생성 중 오류가 발생했습니다."; - - public SecretKeyCreationException() { - super(DEFAULT_MESSAGE); - } - - public SecretKeyCreationException(String s) { - super(s); - } - -} diff --git a/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java b/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java index 2cfccfa4f..05df9d8bc 100644 --- a/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java @@ -1,27 +1,23 @@ package page.clab.api.global.handler; import com.google.gson.stream.MalformedJsonException; -import com.maxmind.geoip2.exception.AddressNotFoundException; -import com.maxmind.geoip2.exception.GeoIp2Exception; import jakarta.mail.MessagingException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.ConstraintViolationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.MessageSource; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.transaction.TransactionSystemException; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.reactive.function.client.WebClientRequestException; -import page.clab.api.domain.accuse.exception.AccuseSearchArgumentLackException; import page.clab.api.domain.accuse.exception.AccuseTargetTypeIncorrectException; import page.clab.api.domain.activityGroup.exception.ActivityGroupNotFinishedException; import page.clab.api.domain.activityGroup.exception.ActivityGroupNotProgressingException; @@ -32,20 +28,20 @@ import page.clab.api.domain.activityGroup.exception.InvalidCategoryException; import page.clab.api.domain.activityGroup.exception.InvalidParentBoardException; import page.clab.api.domain.activityGroup.exception.LeaderStatusChangeNotAllowedException; -import page.clab.api.domain.activityGroup.exception.NotSubmitCategoryBoardException; import page.clab.api.domain.application.exception.NotApprovedApplicationException; +import page.clab.api.domain.book.exception.BookAlreadyAppliedForLoanException; import page.clab.api.domain.book.exception.BookAlreadyBorrowedException; +import page.clab.api.domain.book.exception.BookAlreadyReturnedException; import page.clab.api.domain.book.exception.InvalidBorrowerException; +import page.clab.api.domain.book.exception.LoanNotPendingException; import page.clab.api.domain.book.exception.LoanSuspensionException; +import page.clab.api.domain.book.exception.MaxBorrowLimitExceededException; import page.clab.api.domain.book.exception.OverdueException; -import page.clab.api.domain.donation.exception.DonationSearchArgumentLackException; -import page.clab.api.domain.login.exception.DuplicateLoginException; import page.clab.api.domain.login.exception.LoginFaliedException; import page.clab.api.domain.login.exception.MemberLockedException; import page.clab.api.domain.member.exception.AssociatedAccountExistsException; import page.clab.api.domain.review.exception.AlreadyReviewedException; import page.clab.api.domain.sharedAccount.exception.InvalidUsageTimeException; -import page.clab.api.domain.sharedAccount.exception.SharedAccountInUseException; import page.clab.api.domain.sharedAccount.exception.SharedAccountUsageStateException; import page.clab.api.global.auth.exception.AuthenticationInfoNotFoundException; import page.clab.api.global.auth.exception.TokenForgeryException; @@ -53,7 +49,8 @@ import page.clab.api.global.auth.exception.TokenNotFoundException; import page.clab.api.global.auth.exception.TokenValidateException; import page.clab.api.global.auth.exception.UnAuthorizeException; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.dto.ErrorResponse; import page.clab.api.global.common.file.exception.AssignmentFileUploadFailException; import page.clab.api.global.common.file.exception.CloudStorageNotEnoughException; import page.clab.api.global.common.file.exception.FileUploadFailException; @@ -64,13 +61,11 @@ import page.clab.api.global.exception.InvalidInformationException; import page.clab.api.global.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; -import page.clab.api.global.exception.SearchResultNotExistException; -import page.clab.api.global.exception.SecretKeyCreationException; -import page.clab.api.global.util.ResponseUtil; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; @RestControllerAdvice(basePackages = "page.clab.api") @@ -80,32 +75,20 @@ public class GlobalExceptionHandler { private final SlackService slackService; - private final MessageSource messageSource; - @ExceptionHandler({ - ActivityGroupNotFinishedException.class, InvalidInformationException.class, InvalidParentBoardException.class, InvalidCategoryException.class, - OverdueException.class, StringIndexOutOfBoundsException.class, MissingServletRequestParameterException.class, MalformedJsonException.class, HttpMessageNotReadableException.class, MethodArgumentTypeMismatchException.class, IllegalAccessException.class, - ActivityGroupNotProgressingException.class, - LeaderStatusChangeNotAllowedException.class, - CloudStorageNotEnoughException.class, - NotSubmitCategoryBoardException.class, - AccuseTargetTypeIncorrectException.class, - AccuseSearchArgumentLackException.class, - NotApprovedApplicationException.class, - DonationSearchArgumentLackException.class }) - public ResponseModel badRequestException(HttpServletResponse response, Exception e){ + public ApiResponse badRequestException(HttpServletResponse response, Exception e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return ResponseUtil.createErrorResponse(false, null); + return ApiResponse.failure(); } @ExceptionHandler({ @@ -121,100 +104,94 @@ public ResponseModel badRequestException(HttpServletResponse response, Exception TokenForgeryException.class, MessagingException.class, }) - public ResponseModel unAuthorizeException(HttpServletResponse response, Exception e){ + public ApiResponse unAuthorizeException(HttpServletResponse response, Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return ResponseUtil.createErrorResponse(false, null); + return ApiResponse.failure(); } @ExceptionHandler({ PermissionDeniedException.class, - LoanSuspensionException.class, InvalidBorrowerException.class, }) - public ResponseModel deniedException(HttpServletResponse response, Exception e){ + public ApiResponse deniedException(HttpServletResponse response, Exception e) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); - return ResponseUtil.createErrorResponse(false, null); + return ApiResponse.failure(); } @ExceptionHandler({ NullPointerException.class, - SearchResultNotExistException.class, NotFoundException.class, NoSuchElementException.class, FileNotFoundException.class, - AddressNotFoundException.class, }) - public ResponseModel notFoundException(HttpServletResponse response, Exception e){ + public ApiResponse> notFoundException(HttpServletResponse response, Exception e) { response.setStatus(HttpServletResponse.SC_OK); - return ResponseUtil.createErrorResponse(true, new ArrayList<>()); + return ApiResponse.success(new ArrayList<>()); } @ExceptionHandler({ AssignmentFileUploadFailException.class }) - public ResponseModel notFoundException(HttpServletResponse response, AssignmentFileUploadFailException e) { + public ApiResponse notFoundException(HttpServletResponse response, AssignmentFileUploadFailException e) { response.setStatus(HttpServletResponse.SC_OK); - return ResponseUtil.createErrorResponse(false, null); + return ApiResponse.failure(); } @ExceptionHandler({ + AccuseTargetTypeIncorrectException.class, + NotApprovedApplicationException.class, AssociatedAccountExistsException.class, - BookAlreadyBorrowedException.class, - DuplicateLoginException.class, - AlreadyReviewedException.class, + CloudStorageNotEnoughException.class, + ActivityGroupNotFinishedException.class, + ActivityGroupNotProgressingException.class, + LeaderStatusChangeNotAllowedException.class, AlreadyAppliedException.class, + DuplicateReportException.class, DuplicateAttendanceException.class, DuplicateAbsentExcuseException.class, - DuplicateReportException.class, + AlreadyReviewedException.class, + BookAlreadyBorrowedException.class, + BookAlreadyReturnedException.class, + BookAlreadyAppliedForLoanException.class, + MaxBorrowLimitExceededException.class, + OverdueException.class, + LoanSuspensionException.class, + LoanNotPendingException.class, + SharedAccountUsageStateException.class, InvalidUsageTimeException.class, - SharedAccountInUseException.class, - SharedAccountUsageStateException.class }) - public ResponseModel conflictException(HttpServletResponse response, Exception e){ - response.setStatus(HttpServletResponse.SC_CONFLICT); - return ResponseUtil.createErrorResponse(false, null); + public ErrorResponse conflictException(HttpServletResponse response, Exception e) { + response.setStatus(HttpServletResponse.SC_OK); + return ErrorResponse.failure(e); } @ExceptionHandler({ IllegalStateException.class, FileUploadFailException.class, DataIntegrityViolationException.class, - GeoIp2Exception.class, IOException.class, WebClientRequestException.class, TransactionSystemException.class, SecurityException.class, CustomOptimisticLockingFailureException.class, - SecretKeyCreationException.class, EncryptionException.class, DecryptionException.class, Exception.class }) - public ResponseModel serverException(HttpServletRequest request, HttpServletResponse response, Exception e){ + public ApiResponse serverException(HttpServletRequest request, HttpServletResponse response, Exception e) { slackService.sendServerErrorNotification(request, e); log.warn(e.getMessage()); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return ResponseUtil.createErrorResponse(false, null); + return ApiResponse.failure(); } @ExceptionHandler({ - MethodArgumentNotValidException.class + MethodArgumentNotValidException.class, + ConstraintViolationException.class }) - public ResponseModel handleValidationException(HttpServletResponse response, MethodArgumentNotValidException ex) { - BindingResult result = ex.getBindingResult(); - StringBuilder logMessage = new StringBuilder("Validation errors: "); - - result.getFieldErrors().forEach(fieldError -> { - logMessage.append("["); - logMessage.append(fieldError.getField()); - logMessage.append(": "); - logMessage.append(fieldError.getDefaultMessage()); - logMessage.append("] "); - }); - log.info(logMessage.toString());; - + public ApiResponse handleValidationException(HttpServletResponse response, Exception e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return ResponseUtil.createErrorResponse(false, null); + return ApiResponse.failure(); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/util/EncryptionUtil.java b/src/main/java/page/clab/api/global/util/EncryptionUtil.java index a1f093d2e..f7bac3f46 100644 --- a/src/main/java/page/clab/api/global/util/EncryptionUtil.java +++ b/src/main/java/page/clab/api/global/util/EncryptionUtil.java @@ -5,8 +5,6 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; import page.clab.api.global.config.AesConfig; import page.clab.api.global.exception.DecryptionException; import page.clab.api.global.exception.EncryptionException; @@ -17,18 +15,28 @@ import java.util.Arrays; import java.util.Base64; -@Component -@RequiredArgsConstructor public class EncryptionUtil { - private final AesConfig aesConfig; + private final String secretKey; + private final int ivLengthBytes; + private final int gcmTagLengthBits; + + private EncryptionUtil(String secretKey, int ivLengthBytes, int gcmTagLengthBits) { + this.secretKey = secretKey; + this.ivLengthBytes = ivLengthBytes; + this.gcmTagLengthBits = gcmTagLengthBits; + } + + public static EncryptionUtil create(AesConfig aesConfig) { + return new EncryptionUtil(aesConfig.getSecretKey(), aesConfig.getIvLengthBytes(), aesConfig.getGcmTagLengthBits()); + } public String encrypt(String strToEncrypt) { try { - byte[] iv = generateRandomIV(aesConfig.getIvLengthBytes()); - SecretKeySpec keySpec = new SecretKeySpec(aesConfig.getSecretKey().getBytes(StandardCharsets.UTF_8), "AES"); + byte[] iv = generateRandomIV(this.ivLengthBytes); + SecretKeySpec keySpec = new SecretKeySpec(this.secretKey.getBytes(StandardCharsets.UTF_8), "AES"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - GCMParameterSpec gcmSpec = new GCMParameterSpec(aesConfig.getGcmTagLengthBits(), iv); + GCMParameterSpec gcmSpec = new GCMParameterSpec(this.gcmTagLengthBits, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); byte[] cipherText = cipher.doFinal(strToEncrypt.getBytes(StandardCharsets.UTF_8)); byte[] combined = concat(iv, cipherText); @@ -47,11 +55,11 @@ public String encrypt(String strToEncrypt) { public String decrypt(String strToDecrypt) { try { byte[] combined = Base64.getDecoder().decode(strToDecrypt); - byte[] iv = Arrays.copyOfRange(combined, 0, aesConfig.getIvLengthBytes()); - byte[] cipherText = Arrays.copyOfRange(combined, aesConfig.getIvLengthBytes(), combined.length); - SecretKeySpec keySpec = new SecretKeySpec(aesConfig.getSecretKey().getBytes(StandardCharsets.UTF_8), "AES"); + byte[] iv = Arrays.copyOfRange(combined, 0, this.ivLengthBytes); + byte[] cipherText = Arrays.copyOfRange(combined, this.ivLengthBytes, combined.length); + SecretKeySpec keySpec = new SecretKeySpec(this.secretKey.getBytes(StandardCharsets.UTF_8), "AES"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - GCMParameterSpec gcmSpec = new GCMParameterSpec(aesConfig.getGcmTagLengthBits(), iv); + GCMParameterSpec gcmSpec = new GCMParameterSpec(this.gcmTagLengthBits, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); byte[] decryptedText = cipher.doFinal(cipherText); return new String(decryptedText, StandardCharsets.UTF_8); diff --git a/src/main/java/page/clab/api/global/util/FileSystemUtil.java b/src/main/java/page/clab/api/global/util/FileSystemUtil.java index 0db6dd0fc..77dba51f7 100644 --- a/src/main/java/page/clab/api/global/util/FileSystemUtil.java +++ b/src/main/java/page/clab/api/global/util/FileSystemUtil.java @@ -1,13 +1,10 @@ package page.clab.api.global.util; -import org.springframework.stereotype.Component; - import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -@Component public class FileSystemUtil { public static long calculateDirectorySize(File directory) { diff --git a/src/main/java/page/clab/api/global/util/GeoIpUtil.java b/src/main/java/page/clab/api/global/util/GeoIpUtil.java deleted file mode 100644 index 613d43b8c..000000000 --- a/src/main/java/page/clab/api/global/util/GeoIpUtil.java +++ /dev/null @@ -1,57 +0,0 @@ -package page.clab.api.global.util; - -import com.maxmind.geoip2.DatabaseReader; -import com.maxmind.geoip2.exception.GeoIp2Exception; -import com.maxmind.geoip2.model.CityResponse; -import com.maxmind.geoip2.record.City; -import com.maxmind.geoip2.record.Country; -import com.maxmind.geoip2.record.Location; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ResourceLoader; -import org.springframework.stereotype.Component; -import page.clab.api.domain.login.domain.GeoIpInfo; - -@Component -public class GeoIpUtil { - - private static DatabaseReader databaseReader; - - public GeoIpUtil(ResourceLoader resourceLoader, @Value("${geoip2.database.path}") String databasePath) throws IOException { - try (InputStream inputStream = resourceLoader.getResource(databasePath).getInputStream()) { - databaseReader = new DatabaseReader.Builder(inputStream).build(); - } catch (IOException e) { - throw new RuntimeException("Error initializing GeoIpUtil", e); - } - } - - public static GeoIpInfo getInfoByIp(String ipAddress) { - GeoIpInfo geoIpInfo = new GeoIpInfo(); - try { - InetAddress ip = InetAddress.getByName(ipAddress); - CityResponse response = databaseReader.city(ip); - - City city = response.getCity(); - Country country = response.getCountry(); - Location location = response.getLocation(); - - geoIpInfo.setLocation(city.getName() + " " + country.getName()); - geoIpInfo.setCity(city.getName()); - geoIpInfo.setCountry(country.getName()); - geoIpInfo.setLatitude(location.getLatitude()); - geoIpInfo.setLongitude(location.getLongitude()); - } catch (IOException | GeoIp2Exception e) { - return GeoIpInfo.builder() - .location("Unknown") - .city(null) - .country(null) - .latitude(null) - .longitude(null) - .build(); - } - return geoIpInfo; - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/util/HttpReqResUtil.java b/src/main/java/page/clab/api/global/util/HttpReqResUtil.java index 30ca8af8f..1269288b6 100644 --- a/src/main/java/page/clab/api/global/util/HttpReqResUtil.java +++ b/src/main/java/page/clab/api/global/util/HttpReqResUtil.java @@ -4,8 +4,12 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import java.util.Set; + public class HttpReqResUtil { + private static final Set localIpSet = Set.of("0:0:0:0:0:0:0:1", "127.0.0.1", "192.168.0.1"); + private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", @@ -27,12 +31,15 @@ public static String getClientIpAddressIfServletRequestExist() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); for (String header : IP_HEADER_CANDIDATES) { String ipList = request.getHeader(header); - if (ipList != null && ipList.length() != 0 && !"unknown".equalsIgnoreCase(ipList)) { - String ip = ipList.split(",")[0]; - return ip; + if (ipList != null && !ipList.isEmpty() && !"unknown".equalsIgnoreCase(ipList)) { + return ipList.split(",")[0]; } } return request.getRemoteAddr(); } + public static boolean isLocalRequest(String ipAddress) { + return localIpSet.contains(ipAddress); + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/util/IPInfoUtil.java b/src/main/java/page/clab/api/global/util/IPInfoUtil.java new file mode 100644 index 000000000..35ec1a43d --- /dev/null +++ b/src/main/java/page/clab/api/global/util/IPInfoUtil.java @@ -0,0 +1,51 @@ +package page.clab.api.global.util; + +import io.ipinfo.api.model.IPResponse; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import page.clab.api.global.common.dto.IPInfoResponse; +import page.clab.api.global.config.IPInfoConfig; + +@Slf4j +@Component +public class IPInfoUtil { + + private static RestClient restClient; + + private static String accessToken; + + private static AttributeStrategy attributeStrategy; + + public IPInfoUtil(IPInfoConfig ipInfoConfig) { + restClient = ipInfoConfig.getRestClient(); + accessToken = ipInfoConfig.getAccessToken(); + IPInfoUtil.attributeStrategy = ipInfoConfig.attributeStrategy(); + } + + public static IPInfoResponse getIpInfo(String ipAddress) { + return restClient.get() + .uri(ipAddress) + .header("Authorization", "Bearer " + accessToken) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> { + log.warn("4xx error occurred while getting ip info. Status code: {}", response.getStatusCode()); + })) + .body(IPInfoResponse.class); + } + + public static IPResponse getIpInfo(HttpServletRequest request) { + IPResponse ipResponse = attributeStrategy.getAttribute(request); + if (ipResponse == null) { + log.warn("Failed to get geolocation information from IPInfo. IPResponse is null."); + return null; + } + return ipResponse; + } + +} diff --git a/src/main/java/page/clab/api/global/util/ImageCompressionUtil.java b/src/main/java/page/clab/api/global/util/ImageCompressionUtil.java index bc3512b6d..5134dbf1d 100644 --- a/src/main/java/page/clab/api/global/util/ImageCompressionUtil.java +++ b/src/main/java/page/clab/api/global/util/ImageCompressionUtil.java @@ -1,20 +1,17 @@ package page.clab.api.global.util; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.util.Iterator; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FilenameUtils; -import org.springframework.stereotype.Component; import page.clab.api.global.exception.ImageCompressionException; -@Component -@Slf4j +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.Iterator; + public class ImageCompressionUtil { public static void compressImage(String filePath, float quality) { diff --git a/src/main/java/page/clab/api/global/util/ModelMapperUtil.java b/src/main/java/page/clab/api/global/util/ModelMapperUtil.java deleted file mode 100644 index a302de810..000000000 --- a/src/main/java/page/clab/api/global/util/ModelMapperUtil.java +++ /dev/null @@ -1,15 +0,0 @@ -package page.clab.api.global.util; - -import org.modelmapper.ModelMapper; -import org.springframework.stereotype.Component; - -@Component -public class ModelMapperUtil { - - private static final ModelMapper modelMapper = new ModelMapper(); - - public static ModelMapper getModelMapper() { - return modelMapper; - } - -} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/util/QRCodeUtil.java b/src/main/java/page/clab/api/global/util/QRCodeUtil.java index 02088aa41..576ec6f23 100644 --- a/src/main/java/page/clab/api/global/util/QRCodeUtil.java +++ b/src/main/java/page/clab/api/global/util/QRCodeUtil.java @@ -5,11 +5,10 @@ import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; + import java.io.ByteArrayOutputStream; import java.io.IOException; -import org.springframework.stereotype.Component; -@Component public class QRCodeUtil { private static final int SIZE = 200; diff --git a/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java b/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java index 931ee1e35..22cf935f7 100644 --- a/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java +++ b/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java @@ -1,11 +1,8 @@ package page.clab.api.global.util; -import org.springframework.stereotype.Component; - import java.util.Arrays; import java.util.List; -@Component public class RandomNicknameUtil { private static final int ADJECTIVE_SIZE = 15; @@ -14,7 +11,7 @@ public class RandomNicknameUtil { private static final int NOUN_SIZE = 25; - public static String makeRandomNickname(){ + public static String makeRandomNickname() { List adjectiveArray = Arrays.asList( "행복한", "기쁜", "배고픈", "졸린", "신난", "잠자는", "코딩하는", "밥먹는", "책읽는", "알바하는", diff --git a/src/main/java/page/clab/api/global/util/ResponseUtil.java b/src/main/java/page/clab/api/global/util/ResponseUtil.java index e0489691b..871b17d78 100644 --- a/src/main/java/page/clab/api/global/util/ResponseUtil.java +++ b/src/main/java/page/clab/api/global/util/ResponseUtil.java @@ -1,26 +1,16 @@ package page.clab.api.global.util; import jakarta.servlet.http.HttpServletResponse; -import page.clab.api.global.common.dto.ResponseModel; +import page.clab.api.global.common.dto.ApiResponse; import java.io.IOException; public class ResponseUtil { public static void sendErrorResponse(HttpServletResponse response, int status) throws IOException { - ResponseModel responseModel = ResponseModel.builder() - .success(false) - .build(); - response.getWriter().write(responseModel.toJson()); + response.getWriter().write(ApiResponse.failure().toJson()); response.setContentType("application/json"); response.setStatus(status); } - public static ResponseModel createErrorResponse(boolean success, Object message) { - return ResponseModel.builder() - .success(success) - .data(message) - .build(); - } - } diff --git a/src/main/java/page/clab/api/global/util/StringJsonConverter.java b/src/main/java/page/clab/api/global/util/StringJsonConverter.java new file mode 100644 index 000000000..d8cd0a54e --- /dev/null +++ b/src/main/java/page/clab/api/global/util/StringJsonConverter.java @@ -0,0 +1,35 @@ +package page.clab.api.global.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class StringJsonConverter implements AttributeConverter, String> { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List attribute) { + try { + return objectMapper.writeValueAsString(attribute); + } catch (Exception e) { + log.error("Could not convert list to JSON string", e); + return null; + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + try { + return objectMapper.readValue(dbData, new TypeReference>(){}); + } catch (Exception e) { + log.error("Could not convert JSON string to list", e); + return null; + } + } + +} diff --git a/src/main/java/page/clab/api/global/validation/ValidationService.java b/src/main/java/page/clab/api/global/validation/ValidationService.java new file mode 100644 index 000000000..79272ffb7 --- /dev/null +++ b/src/main/java/page/clab/api/global/validation/ValidationService.java @@ -0,0 +1,15 @@ +package page.clab.api.global.validation; + +import jakarta.validation.Valid; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +public class ValidationService { + + public void checkValid(@Valid T validationTarget) { + + } + +} diff --git a/src/main/resources/GeoLite2-City.mmdb b/src/main/resources/GeoLite2-City.mmdb deleted file mode 100644 index 2bc1a2bfb..000000000 Binary files a/src/main/resources/GeoLite2-City.mmdb and /dev/null differ diff --git a/src/main/resources/continent.json b/src/main/resources/continent.json new file mode 100644 index 000000000..516fde24c --- /dev/null +++ b/src/main/resources/continent.json @@ -0,0 +1,252 @@ +{ + "BD": {"code": "AS", "name": "Asia"}, + "BE": {"code": "EU", "name": "Europe"}, + "BF": {"code": "AF", "name": "Africa"}, + "BG": {"code": "EU", "name": "Europe"}, + "BA": {"code": "EU", "name": "Europe"}, + "BB": {"code": "NA", "name": "North America"}, + "WF": {"code": "OC", "name": "Oceania"}, + "BL": {"code": "NA", "name": "North America"}, + "BM": {"code": "NA", "name": "North America"}, + "BN": {"code": "AS", "name": "Asia"}, + "BO": {"code": "SA", "name": "South America"}, + "BH": {"code": "AS", "name": "Asia"}, + "BI": {"code": "AF", "name": "Africa"}, + "BJ": {"code": "AF", "name": "Africa"}, + "BT": {"code": "AS", "name": "Asia"}, + "JM": {"code": "NA", "name": "North America"}, + "BV": {"code": "AN", "name": "Antarctica"}, + "BW": {"code": "AF", "name": "Africa"}, + "WS": {"code": "OC", "name": "Oceania"}, + "BQ": {"code": "NA", "name": "North America"}, + "BR": {"code": "SA", "name": "South America"}, + "BS": {"code": "NA", "name": "North America"}, + "JE": {"code": "EU", "name": "Europe"}, + "BY": {"code": "EU", "name": "Europe"}, + "BZ": {"code": "NA", "name": "North America"}, + "RU": {"code": "EU", "name": "Europe"}, + "RW": {"code": "AF", "name": "Africa"}, + "RS": {"code": "EU", "name": "Europe"}, + "TL": {"code": "OC", "name": "Oceania"}, + "RE": {"code": "AF", "name": "Africa"}, + "TM": {"code": "AS", "name": "Asia"}, + "TJ": {"code": "AS", "name": "Asia"}, + "RO": {"code": "EU", "name": "Europe"}, + "TK": {"code": "OC", "name": "Oceania"}, + "GW": {"code": "AF", "name": "Africa"}, + "GU": {"code": "OC", "name": "Oceania"}, + "GT": {"code": "NA", "name": "North America"}, + "GS": {"code": "AN", "name": "Antarctica"}, + "GR": {"code": "EU", "name": "Europe"}, + "GQ": {"code": "AF", "name": "Africa"}, + "GP": {"code": "NA", "name": "North America"}, + "JP": {"code": "AS", "name": "Asia"}, + "GY": {"code": "SA", "name": "South America"}, + "GG": {"code": "EU", "name": "Europe"}, + "GF": {"code": "SA", "name": "South America"}, + "GE": {"code": "AS", "name": "Asia"}, + "GD": {"code": "NA", "name": "North America"}, + "GB": {"code": "EU", "name": "Europe"}, + "GA": {"code": "AF", "name": "Africa"}, + "SV": {"code": "NA", "name": "North America"}, + "GN": {"code": "AF", "name": "Africa"}, + "GM": {"code": "AF", "name": "Africa"}, + "GL": {"code": "NA", "name": "North America"}, + "GI": {"code": "EU", "name": "Europe"}, + "GH": {"code": "AF", "name": "Africa"}, + "OM": {"code": "AS", "name": "Asia"}, + "TN": {"code": "AF", "name": "Africa"}, + "JO": {"code": "AS", "name": "Asia"}, + "HR": {"code": "EU", "name": "Europe"}, + "HT": {"code": "NA", "name": "North America"}, + "HU": {"code": "EU", "name": "Europe"}, + "HK": {"code": "AS", "name": "Asia"}, + "HN": {"code": "NA", "name": "North America"}, + "HM": {"code": "AN", "name": "Antarctica"}, + "VE": {"code": "SA", "name": "South America"}, + "PR": {"code": "NA", "name": "North America"}, + "PS": {"code": "AS", "name": "Asia"}, + "PW": {"code": "OC", "name": "Oceania"}, + "PT": {"code": "EU", "name": "Europe"}, + "SJ": {"code": "EU", "name": "Europe"}, + "PY": {"code": "SA", "name": "South America"}, + "IQ": {"code": "AS", "name": "Asia"}, + "PA": {"code": "NA", "name": "North America"}, + "PF": {"code": "OC", "name": "Oceania"}, + "PG": {"code": "OC", "name": "Oceania"}, + "PE": {"code": "SA", "name": "South America"}, + "PK": {"code": "AS", "name": "Asia"}, + "PH": {"code": "AS", "name": "Asia"}, + "PN": {"code": "OC", "name": "Oceania"}, + "PL": {"code": "EU", "name": "Europe"}, + "PM": {"code": "NA", "name": "North America"}, + "ZM": {"code": "AF", "name": "Africa"}, + "EH": {"code": "AF", "name": "Africa"}, + "EE": {"code": "EU", "name": "Europe"}, + "EG": {"code": "AF", "name": "Africa"}, + "ZA": {"code": "AF", "name": "Africa"}, + "EC": {"code": "SA", "name": "South America"}, + "IT": {"code": "EU", "name": "Europe"}, + "VN": {"code": "AS", "name": "Asia"}, + "SB": {"code": "OC", "name": "Oceania"}, + "ET": {"code": "AF", "name": "Africa"}, + "SO": {"code": "AF", "name": "Africa"}, + "ZW": {"code": "AF", "name": "Africa"}, + "SA": {"code": "AS", "name": "Asia"}, + "ES": {"code": "EU", "name": "Europe"}, + "ER": {"code": "AF", "name": "Africa"}, + "ME": {"code": "EU", "name": "Europe"}, + "MD": {"code": "EU", "name": "Europe"}, + "MG": {"code": "AF", "name": "Africa"}, + "MF": {"code": "NA", "name": "North America"}, + "MA": {"code": "AF", "name": "Africa"}, + "MC": {"code": "EU", "name": "Europe"}, + "UZ": {"code": "AS", "name": "Asia"}, + "MM": {"code": "AS", "name": "Asia"}, + "ML": {"code": "AF", "name": "Africa"}, + "MO": {"code": "AS", "name": "Asia"}, + "MN": {"code": "AS", "name": "Asia"}, + "MH": {"code": "OC", "name": "Oceania"}, + "MK": {"code": "EU", "name": "Europe"}, + "MU": {"code": "AF", "name": "Africa"}, + "MT": {"code": "EU", "name": "Europe"}, + "MW": {"code": "AF", "name": "Africa"}, + "MV": {"code": "AS", "name": "Asia"}, + "MQ": {"code": "NA", "name": "North America"}, + "MP": {"code": "OC", "name": "Oceania"}, + "MS": {"code": "NA", "name": "North America"}, + "MR": {"code": "AF", "name": "Africa"}, + "IM": {"code": "EU", "name": "Europe"}, + "UG": {"code": "AF", "name": "Africa"}, + "TZ": {"code": "AF", "name": "Africa"}, + "MY": {"code": "AS", "name": "Asia"}, + "MX": {"code": "NA", "name": "North America"}, + "IL": {"code": "AS", "name": "Asia"}, + "FR": {"code": "EU", "name": "Europe"}, + "IO": {"code": "AS", "name": "Asia"}, + "SH": {"code": "AF", "name": "Africa"}, + "FI": {"code": "EU", "name": "Europe"}, + "FJ": {"code": "OC", "name": "Oceania"}, + "FK": {"code": "SA", "name": "South America"}, + "FM": {"code": "OC", "name": "Oceania"}, + "FO": {"code": "EU", "name": "Europe"}, + "NI": {"code": "NA", "name": "North America"}, + "NL": {"code": "EU", "name": "Europe"}, + "NO": {"code": "EU", "name": "Europe"}, + "NA": {"code": "AF", "name": "Africa"}, + "VU": {"code": "OC", "name": "Oceania"}, + "NC": {"code": "OC", "name": "Oceania"}, + "NE": {"code": "AF", "name": "Africa"}, + "NF": {"code": "OC", "name": "Oceania"}, + "NG": {"code": "AF", "name": "Africa"}, + "NZ": {"code": "OC", "name": "Oceania"}, + "NP": {"code": "AS", "name": "Asia"}, + "NR": {"code": "OC", "name": "Oceania"}, + "NU": {"code": "OC", "name": "Oceania"}, + "CK": {"code": "OC", "name": "Oceania"}, + "XK": {"code": "EU", "name": "Europe"}, + "CI": {"code": "AF", "name": "Africa"}, + "CH": {"code": "EU", "name": "Europe"}, + "CO": {"code": "SA", "name": "South America"}, + "CN": {"code": "AS", "name": "Asia"}, + "CM": {"code": "AF", "name": "Africa"}, + "CL": {"code": "SA", "name": "South America"}, + "CC": {"code": "AS", "name": "Asia"}, + "CA": {"code": "NA", "name": "North America"}, + "CG": {"code": "AF", "name": "Africa"}, + "CF": {"code": "AF", "name": "Africa"}, + "CD": {"code": "AF", "name": "Africa"}, + "CZ": {"code": "EU", "name": "Europe"}, + "CY": {"code": "EU", "name": "Europe"}, + "CX": {"code": "AS", "name": "Asia"}, + "CR": {"code": "NA", "name": "North America"}, + "CW": {"code": "NA", "name": "North America"}, + "CV": {"code": "AF", "name": "Africa"}, + "CU": {"code": "NA", "name": "North America"}, + "SZ": {"code": "AF", "name": "Africa"}, + "SY": {"code": "AS", "name": "Asia"}, + "SX": {"code": "NA", "name": "North America"}, + "KG": {"code": "AS", "name": "Asia"}, + "KE": {"code": "AF", "name": "Africa"}, + "SS": {"code": "AF", "name": "Africa"}, + "SR": {"code": "SA", "name": "South America"}, + "KI": {"code": "OC", "name": "Oceania"}, + "KH": {"code": "AS", "name": "Asia"}, + "KN": {"code": "NA", "name": "North America"}, + "KM": {"code": "AF", "name": "Africa"}, + "ST": {"code": "AF", "name": "Africa"}, + "SK": {"code": "EU", "name": "Europe"}, + "KR": {"code": "AS", "name": "Asia"}, + "SI": {"code": "EU", "name": "Europe"}, + "KP": {"code": "AS", "name": "Asia"}, + "KW": {"code": "AS", "name": "Asia"}, + "SN": {"code": "AF", "name": "Africa"}, + "SM": {"code": "EU", "name": "Europe"}, + "SL": {"code": "AF", "name": "Africa"}, + "SC": {"code": "AF", "name": "Africa"}, + "KZ": {"code": "AS", "name": "Asia"}, + "KY": {"code": "NA", "name": "North America"}, + "SG": {"code": "AS", "name": "Asia"}, + "SE": {"code": "EU", "name": "Europe"}, + "SD": {"code": "AF", "name": "Africa"}, + "DO": {"code": "NA", "name": "North America"}, + "DM": {"code": "NA", "name": "North America"}, + "DJ": {"code": "AF", "name": "Africa"}, + "DK": {"code": "EU", "name": "Europe"}, + "VG": {"code": "NA", "name": "North America"}, + "DE": {"code": "EU", "name": "Europe"}, + "YE": {"code": "AS", "name": "Asia"}, + "DZ": {"code": "AF", "name": "Africa"}, + "US": {"code": "NA", "name": "North America"}, + "UY": {"code": "SA", "name": "South America"}, + "YT": {"code": "AF", "name": "Africa"}, + "UM": {"code": "OC", "name": "Oceania"}, + "LB": {"code": "AS", "name": "Asia"}, + "LC": {"code": "NA", "name": "North America"}, + "LA": {"code": "AS", "name": "Asia"}, + "TV": {"code": "OC", "name": "Oceania"}, + "TW": {"code": "AS", "name": "Asia"}, + "TT": {"code": "NA", "name": "North America"}, + "TR": {"code": "AS", "name": "Asia"}, + "LK": {"code": "AS", "name": "Asia"}, + "LI": {"code": "EU", "name": "Europe"}, + "LV": {"code": "EU", "name": "Europe"}, + "TO": {"code": "OC", "name": "Oceania"}, + "LT": {"code": "EU", "name": "Europe"}, + "LU": {"code": "EU", "name": "Europe"}, + "LR": {"code": "AF", "name": "Africa"}, + "LS": {"code": "AF", "name": "Africa"}, + "TH": {"code": "AS", "name": "Asia"}, + "TF": {"code": "AN", "name": "Antarctica"}, + "TG": {"code": "AF", "name": "Africa"}, + "TD": {"code": "AF", "name": "Africa"}, + "TC": {"code": "NA", "name": "North America"}, + "LY": {"code": "AF", "name": "Africa"}, + "VA": {"code": "EU", "name": "Europe"}, + "VC": {"code": "NA", "name": "North America"}, + "AE": {"code": "AS", "name": "Asia"}, + "AD": {"code": "EU", "name": "Europe"}, + "AG": {"code": "NA", "name": "North America"}, + "AF": {"code": "AS", "name": "Asia"}, + "AI": {"code": "NA", "name": "North America"}, + "VI": {"code": "NA", "name": "North America"}, + "IS": {"code": "EU", "name": "Europe"}, + "IR": {"code": "AS", "name": "Asia"}, + "AM": {"code": "AS", "name": "Asia"}, + "AL": {"code": "EU", "name": "Europe"}, + "AO": {"code": "AF", "name": "Africa"}, + "AQ": {"code": "AN", "name": "Antarctica"}, + "AS": {"code": "OC", "name": "Oceania"}, + "AR": {"code": "SA", "name": "South America"}, + "AU": {"code": "OC", "name": "Oceania"}, + "AT": {"code": "EU", "name": "Europe"}, + "AW": {"code": "NA", "name": "North America"}, + "IN": {"code": "AS", "name": "Asia"}, + "AX": {"code": "EU", "name": "Europe"}, + "AZ": {"code": "AS", "name": "Asia"}, + "IE": {"code": "EU", "name": "Europe"}, + "ID": {"code": "AS", "name": "Asia"}, + "UA": {"code": "EU", "name": "Europe"}, + "QA": {"code": "AS", "name": "Asia"}, + "MZ": {"code": "AF", "name": "Africa"} +} diff --git a/src/main/resources/currency.json b/src/main/resources/currency.json new file mode 100644 index 000000000..2aea66d06 --- /dev/null +++ b/src/main/resources/currency.json @@ -0,0 +1,246 @@ +{ + "BD": {"code": "BDT", "symbol": "৳"}, + "BE": {"code": "EUR", "symbol": "€"}, + "BF": {"code": "XOF", "symbol": "CFA"}, + "BG": {"code": "BGN", "symbol": "лв"}, + "BA": {"code": "BAM", "symbol": "KM"}, + "BB": {"code": "BBD", "symbol": "$"}, + "WF": {"code": "XPF", "symbol": "₣"}, + "BL": {"code": "EUR", "symbol": "€"}, + "BM": {"code": "BMD", "symbol": "$"}, + "BN": {"code": "BND", "symbol": "$"}, + "BO": {"code": "BOB", "symbol": "$b"}, + "BH": {"code": "BHD", "symbol": ".د.ب"}, + "BI": {"code": "BIF", "symbol": "FBu"}, + "BJ": {"code": "XOF", "symbol": "CFA"}, + "BT": {"code": "BTN", "symbol": "Nu."}, + "JM": {"code": "JMD", "symbol": "J$"}, + "BV": {"code": "NOK", "symbol": "kr"}, + "BW": {"code": "BWP", "symbol": "P"}, + "WS": {"code": "WST", "symbol": "WS$"}, + "BQ": {"code": "USD", "symbol": "$"}, + "BR": {"code": "BRL", "symbol": "R$"}, + "BS": {"code": "BSD", "symbol": "$"}, + "JE": {"code": "GBP", "symbol": "£"}, + "BY": {"code": "BYN", "symbol": "Br"}, + "BZ": {"code": "BZD", "symbol": "BZ$"}, + "RU": {"code": "RUB", "symbol": "₽"}, + "RW": {"code": "RWF", "symbol": "FRw"}, + "RS": {"code": "RSD", "symbol": "Дин."}, + "TL": {"code": "USD", "symbol": "$"}, + "RE": {"code": "EUR", "symbol": "€"}, + "TM": {"code": "TMT", "symbol": "T"}, + "TJ": {"code": "TJS", "symbol": "SM"}, + "RO": {"code": "RON", "symbol": "lei"}, + "TK": {"code": "NZD", "symbol": "$"}, + "GW": {"code": "XOF", "symbol": "CFA"}, + "GU": {"code": "USD", "symbol": "$"}, + "GT": {"code": "GTQ", "symbol": "Q"}, + "GS": {"code": "GBP", "symbol": "£"}, + "GR": {"code": "EUR", "symbol": "€"}, + "GQ": {"code": "XAF", "symbol": "FCFA"}, + "GP": {"code": "EUR", "symbol": "€"}, + "JP": {"code": "JPY", "symbol": "¥"}, + "GY": {"code": "GYD", "symbol": "$"}, + "GG": {"code": "GBP", "symbol": "£"}, + "GF": {"code": "EUR", "symbol": "€"}, + "GE": {"code": "GEL", "symbol": "ლ"}, + "GD": {"code": "XCD", "symbol": "$"}, + "GB": {"code": "GBP", "symbol": "£"}, + "GA": {"code": "XAF", "symbol": "FCFA"}, + "SV": {"code": "USD", "symbol": "$"}, + "GN": {"code": "GNF", "symbol": "FG"}, + "GM": {"code": "GMD", "symbol": "D"}, + "GL": {"code": "DKK", "symbol": "kr"}, + "GI": {"code": "GIP", "symbol": "£"}, + "GH": {"code": "GHS", "symbol": "GH₵"}, + "OM": {"code": "OMR", "symbol": "﷼"}, + "TN": {"code": "TND", "symbol": "د.ت"}, + "JO": {"code": "JOD", "symbol": "JD"}, + "HR": {"code": "HRK", "symbol": "kn"}, + "HT": {"code": "HTG", "symbol": "G"}, + "HU": {"code": "HUF", "symbol": "Ft"}, + "HK": {"code": "HKD", "symbol": "$"}, + "HN": {"code": "HNL", "symbol": "L"}, + "HM": {"code": "AUD", "symbol": "$"}, + "VE": {"code": "VES", "symbol": "Bs"}, + "PR": {"code": "USD", "symbol": "$"}, + "PS": {"code": "ILS", "symbol": "₪"}, + "PW": {"code": "USD", "symbol": "$"}, + "PT": {"code": "EUR", "symbol": "€"}, + "SJ": {"code": "NOK", "symbol": "kr"}, + "PY": {"code": "PYG", "symbol": "₲"}, + "IQ": {"code": "IQD", "symbol": "ع.د"}, + "PA": {"code": "PAB", "symbol": "B/."}, + "PF": {"code": "XPF", "symbol": "₣"}, + "PG": {"code": "PGK", "symbol": "K"}, + "PE": {"code": "PEN", "symbol": "S/"}, + "PK": {"code": "PKR", "symbol": "₨"}, + "PH": {"code": "PHP", "symbol": "₱"}, + "PN": {"code": "NZD", "symbol": "$"}, + "PL": {"code": "PLN", "symbol": "zł"}, + "PM": {"code": "EUR", "symbol": "€"}, + "ZM": {"code": "ZMW", "symbol": "ZK"}, + "EH": {"code": "MAD", "symbol": "MAD"}, + "EE": {"code": "EUR", "symbol": "€"}, + "EG": {"code": "EGP", "symbol": "£"}, + "ZA": {"code": "ZAR", "symbol": "R"}, + "EC": {"code": "USD", "symbol": "$"}, + "IT": {"code": "EUR", "symbol": "€"}, + "VN": {"code": "VND", "symbol": "₫"}, + "SB": {"code": "SBD", "symbol": "$"}, + "ET": {"code": "ETB", "symbol": "Br"}, + "SO": {"code": "SOS", "symbol": "S"}, + "ZW": {"code": "ZWL", "symbol": "Z$"}, + "SA": {"code": "SAR", "symbol": "﷼"}, + "ES": {"code": "EUR", "symbol": "€"}, + "ER": {"code": "ERN", "symbol": "Nfk"}, + "ME": {"code": "EUR", "symbol": "€"}, + "MD": {"code": "MDL", "symbol": "lei"}, + "MG": {"code": "MGA", "symbol": "Ar"}, + "MF": {"code": "EUR", "symbol": "€"}, + "MA": {"code": "MAD", "symbol": "MAD"}, + "MC": {"code": "EUR", "symbol": "€"}, + "UZ": {"code": "UZS", "symbol": "лв"}, + "MM": {"code": "MMK", "symbol": "K"}, + "ML": {"code": "XOF", "symbol": "CFA"}, + "MO": {"code": "MOP", "symbol": "MOP$"}, + "MN": {"code": "MNT", "symbol": "₮"}, + "MH": {"code": "USD", "symbol": "$"}, + "MK": {"code": "MKD", "symbol": "ден"}, + "MU": {"code": "MUR", "symbol": "₨"}, + "MT": {"code": "EUR", "symbol": "€"}, + "MW": {"code": "MWK", "symbol": "MK"}, + "MQ": {"code": "EUR", "symbol": "€"}, + "MP": {"code": "USD", "symbol": "$"}, + "MS": {"code": "XCD", "symbol": "$"}, + "MR": {"code": "MRO", "symbol": "UM"}, + "IM": {"code": "GBP", "symbol": "£"}, + "UG": {"code": "UGX", "symbol": "USh"}, + "TZ": {"code": "TZS", "symbol": "TSh"}, + "MY": {"code": "MYR", "symbol": "RM"}, + "MX": {"code": "MXN", "symbol": "$"}, + "IL": {"code": "ILS", "symbol": "₪"}, + "FR": {"code": "EUR", "symbol": "€"}, + "IO": {"code": "USD", "symbol": "$"}, + "SH": {"code": "SHP", "symbol": "£"}, + "FI": {"code": "EUR", "symbol": "€"}, + "FJ": {"code": "FJD", "symbol": "$"}, + "FK": {"code": "FKP", "symbol": "£"}, + "FM": {"code": "USD", "symbol": "$"}, + "FO": {"code": "DKK", "symbol": "kr"}, + "NI": {"code": "NIO", "symbol": "C$"}, + "NL": {"code": "EUR", "symbol": "€"}, + "NO": {"code": "NOK", "symbol": "kr"}, + "NA": {"code": "NAD", "symbol": "N$"}, + "VU": {"code": "VUV", "symbol": "VT"}, + "NC": {"code": "XPF", "symbol": "₣"}, + "NE": {"code": "XOF", "symbol": "CFA"}, + "NF": {"code": "AUD", "symbol": "$"}, + "NG": {"code": "NGN", "symbol": "₦"}, + "NZ": {"code": "NZD", "symbol": "$"}, + "NP": {"code": "NPR", "symbol": "₨"}, + "NR": {"code": "AUD", "symbol": "$"}, + "NU": {"code": "NZD", "symbol": "$"}, + "CK": {"code": "NZD", "symbol": "$"}, + "XK": {"code": "EUR", "symbol": "€"}, + "CI": {"code": "XOF", "symbol": "CFA"}, + "CH": {"code": "CHF", "symbol": "CHF"}, + "CO": {"code": "COP", "symbol": "$"}, + "CN": {"code": "CNY", "symbol": "¥"}, + "CM": {"code": "XAF", "symbol": "FCFA"}, + "CL": {"code": "CLP", "symbol": "$"}, + "CC": {"code": "AUD", "symbol": "$"}, + "CA": {"code": "CAD", "symbol": "$"}, + "CG": {"code": "XAF", "symbol": "FCFA"}, + "CF": {"code": "XAF", "symbol": "FCFA"}, + "CD": {"code": "CDF", "symbol": "FC"}, + "CZ": {"code": "CZK", "symbol": "Kč"}, + "CY": {"code": "EUR", "symbol": "€"}, + "CX": {"code": "AUD", "symbol": "$"}, + "CR": {"code": "CRC", "symbol": "₡"}, + "CW": {"code": "ANG", "symbol": "ƒ"}, + "CV": {"code": "CVE", "symbol": "$"}, + "CU": {"code": "CUP", "symbol": "₱"}, + "SZ": {"code": "SZL", "symbol": "L"}, + "SY": {"code": "SYP", "symbol": "£"}, + "SX": {"code": "ANG", "symbol": "ƒ"}, + "KG": {"code": "KGS", "symbol": "лв"}, + "KE": {"code": "KES", "symbol": "KSh"}, + "SS": {"code": "SSP", "symbol": "£"}, + "SR": {"code": "SRD", "symbol": "$"}, + "KI": {"code": "AUD", "symbol": "$"}, + "KH": {"code": "KHR", "symbol": "៛"}, + "KN": {"code": "XCD", "symbol": "$"}, + "KM": {"code": "KMF", "symbol": "CF"}, + "ST": {"code": "STD", "symbol": "Db"}, + "SK": {"code": "EUR", "symbol": "€"}, + "KR": {"code": "KRW", "symbol": "₩"}, + "SI": {"code": "EUR", "symbol": "€"}, + "KP": {"code": "KPW", "symbol": "₩"}, + "KW": {"code": "KWD", "symbol": "KD"}, + "SN": {"code": "XOF", "symbol": "CFA"}, + "SM": {"code": "EUR", "symbol": "€"}, + "SL": {"code": "SLL", "symbol": "Le"}, + "SC": {"code": "SCR", "symbol": "₨"}, + "KZ": {"code": "KZT", "symbol": "₸"}, + "KY": {"code": "KYD", "symbol": "$"}, + "SG": {"code": "SGD", "symbol": "$"}, + "SE": {"code": "SEK", "symbol": "kr"}, + "SD": {"code": "SDG", "symbol": "ج.س."}, + "DO": {"code": "DOP", "symbol": "RD$"}, + "DM": {"code": "XCD", "symbol": "$"}, + "DJ": {"code": "DJF", "symbol": "Fdj"}, + "DK": {"code": "DKK", "symbol": "kr"}, + "VG": {"code": "USD", "symbol": "$"}, + "DE": {"code": "EUR", "symbol": "€"}, + "YE": {"code": "YER", "symbol": "﷼"}, + "DZ": {"code": "DZD", "symbol": "دج"}, + "US": {"code": "USD", "symbol": "$"}, + "UY": {"code": "UYU", "symbol": "$U"}, + "YT": {"code": "EUR", "symbol": "€"}, + "UM": {"code": "USD", "symbol": "$"}, + "LB": {"code": "LBP", "symbol": "£"}, + "LC": {"code": "XCD", "symbol": "$"}, + "LA": {"code": "LAK", "symbol": "₭"}, + "TV": {"code": "AUD", "symbol": "$"}, + "TW": {"code": "TWD", "symbol": "NT$"}, + "TT": {"code": "TTD", "symbol": "TT$"}, + "TR": {"code": "TRY", "symbol": "₺"}, + "LK": {"code": "LKR", "symbol": "₨"}, + "LI": {"code": "CHF", "symbol": "CHF"}, + "LV": {"code": "EUR", "symbol": "€"}, + "TO": {"code": "TOP", "symbol": "T$"}, + "LT": {"code": "EUR", "symbol": "€"}, + "LU": {"code": "EUR", "symbol": "€"}, + "LR": {"code": "LRD", "symbol": "$"}, + "LS": {"code": "LSL", "symbol": "M"}, + "TH": {"code": "THB", "symbol": "฿"}, + "TF": {"code": "EUR", "symbol": "€"}, + "TG": {"code": "XOF", "symbol": "CFA"}, + "TD": {"code": "XAF", "symbol": "FCFA"}, + "TC": {"code": "USD", "symbol": "$"}, + "LY": {"code": "LYD", "symbol": "LD"}, + "VA": {"code": "EUR", "symbol": "€"}, + "VC": {"code": "XCD", "symbol": "$"}, + "VI": {"code": "USD", "symbol": "$"}, + "IS": {"code": "ISK", "symbol": "kr"}, + "IR": {"code": "IRR", "symbol": "﷼"}, + "AM": {"code": "AMD", "symbol": "֏"}, + "AL": {"code": "ALL", "symbol": "L"}, + "AO": {"code": "AOA", "symbol": "Kz"}, + "AQ": {"code": "USD", "symbol": "$"}, + "AS": {"code": "USD", "symbol": "$"}, + "AR": {"code": "ARS", "symbol": "$"}, + "AU": {"code": "AUD", "symbol": "$"}, + "AT": {"code": "EUR", "symbol": "€"}, + "AW": {"code": "AWG", "symbol": "ƒ"}, + "IN": {"code": "INR", "symbol": "₹"}, + "AX": {"code": "EUR", "symbol": "€"}, + "AZ": {"code": "AZN", "symbol": "₼"}, + "IE": {"code": "EUR", "symbol": "€"}, + "ID": {"code": "IDR", "symbol": "Rp"}, + "UA": {"code": "UAH", "symbol": "₴"}, + "QA": {"code": "QAR", "symbol": "﷼"}, + "MZ": {"code": "MZN", "symbol": "MT"} +} diff --git a/src/main/resources/en_US.json b/src/main/resources/en_US.json new file mode 100644 index 000000000..db5315c48 --- /dev/null +++ b/src/main/resources/en_US.json @@ -0,0 +1,252 @@ +{ + "BD": "Bangladesh", + "BE": "Belgium", + "BF": "Burkina Faso", + "BG": "Bulgaria", + "BA": "Bosnia and Herzegovina", + "BB": "Barbados", + "WF": "Wallis and Futuna", + "BL": "Saint Barthelemy", + "BM": "Bermuda", + "BN": "Brunei", + "BO": "Bolivia", + "BH": "Bahrain", + "BI": "Burundi", + "BJ": "Benin", + "BT": "Bhutan", + "JM": "Jamaica", + "BV": "Bouvet Island", + "BW": "Botswana", + "WS": "Samoa", + "BQ": "Bonaire, Saint Eustatius and Saba", + "BR": "Brazil", + "BS": "Bahamas", + "JE": "Jersey", + "BY": "Belarus", + "BZ": "Belize", + "RU": "Russia", + "RW": "Rwanda", + "RS": "Serbia", + "TL": "East Timor", + "RE": "Reunion", + "TM": "Turkmenistan", + "TJ": "Tajikistan", + "RO": "Romania", + "TK": "Tokelau", + "GW": "Guinea-Bissau", + "GU": "Guam", + "GT": "Guatemala", + "GS": "South Georgia and the South Sandwich Islands", + "GR": "Greece", + "GQ": "Equatorial Guinea", + "GP": "Guadeloupe", + "JP": "Japan", + "GY": "Guyana", + "GG": "Guernsey", + "GF": "French Guiana", + "GE": "Georgia", + "GD": "Grenada", + "GB": "United Kingdom", + "GA": "Gabon", + "SV": "El Salvador", + "GN": "Guinea", + "GM": "Gambia", + "GL": "Greenland", + "GI": "Gibraltar", + "GH": "Ghana", + "OM": "Oman", + "TN": "Tunisia", + "JO": "Jordan", + "HR": "Croatia", + "HT": "Haiti", + "HU": "Hungary", + "HK": "Hong Kong", + "HN": "Honduras", + "HM": "Heard Island and McDonald Islands", + "VE": "Venezuela", + "PR": "Puerto Rico", + "PS": "Palestinian Territory", + "PW": "Palau", + "PT": "Portugal", + "SJ": "Svalbard and Jan Mayen", + "PY": "Paraguay", + "IQ": "Iraq", + "PA": "Panama", + "PF": "French Polynesia", + "PG": "Papua New Guinea", + "PE": "Peru", + "PK": "Pakistan", + "PH": "Philippines", + "PN": "Pitcairn", + "PL": "Poland", + "PM": "Saint Pierre and Miquelon", + "ZM": "Zambia", + "EH": "Western Sahara", + "EE": "Estonia", + "EG": "Egypt", + "ZA": "South Africa", + "EC": "Ecuador", + "IT": "Italy", + "VN": "Vietnam", + "SB": "Solomon Islands", + "ET": "Ethiopia", + "SO": "Somalia", + "ZW": "Zimbabwe", + "SA": "Saudi Arabia", + "ES": "Spain", + "ER": "Eritrea", + "ME": "Montenegro", + "MD": "Moldova", + "MG": "Madagascar", + "MF": "Saint Martin", + "MA": "Morocco", + "MC": "Monaco", + "UZ": "Uzbekistan", + "MM": "Myanmar", + "ML": "Mali", + "MO": "Macao", + "MN": "Mongolia", + "MH": "Marshall Islands", + "MK": "Macedonia", + "MU": "Mauritius", + "MT": "Malta", + "MW": "Malawi", + "MV": "Maldives", + "MQ": "Martinique", + "MP": "Northern Mariana Islands", + "MS": "Montserrat", + "MR": "Mauritania", + "IM": "Isle of Man", + "UG": "Uganda", + "TZ": "Tanzania", + "MY": "Malaysia", + "MX": "Mexico", + "IL": "Israel", + "FR": "France", + "IO": "British Indian Ocean Territory", + "SH": "Saint Helena", + "FI": "Finland", + "FJ": "Fiji", + "FK": "Falkland Islands", + "FM": "Micronesia", + "FO": "Faroe Islands", + "NI": "Nicaragua", + "NL": "Netherlands", + "NO": "Norway", + "NA": "Namibia", + "VU": "Vanuatu", + "NC": "New Caledonia", + "NE": "Niger", + "NF": "Norfolk Island", + "NG": "Nigeria", + "NZ": "New Zealand", + "NP": "Nepal", + "NR": "Nauru", + "NU": "Niue", + "CK": "Cook Islands", + "XK": "Kosovo", + "CI": "Ivory Coast", + "CH": "Switzerland", + "CO": "Colombia", + "CN": "China", + "CM": "Cameroon", + "CL": "Chile", + "CC": "Cocos Islands", + "CA": "Canada", + "CG": "Republic of the Congo", + "CF": "Central African Republic", + "CD": "Democratic Republic of the Congo", + "CZ": "Czech Republic", + "CY": "Cyprus", + "CX": "Christmas Island", + "CR": "Costa Rica", + "CW": "Curacao", + "CV": "Cape Verde", + "CU": "Cuba", + "SZ": "Swaziland", + "SY": "Syria", + "SX": "Sint Maarten", + "KG": "Kyrgyzstan", + "KE": "Kenya", + "SS": "South Sudan", + "SR": "Suriname", + "KI": "Kiribati", + "KH": "Cambodia", + "KN": "Saint Kitts and Nevis", + "KM": "Comoros", + "ST": "Sao Tome and Principe", + "SK": "Slovakia", + "KR": "South Korea", + "SI": "Slovenia", + "KP": "North Korea", + "KW": "Kuwait", + "SN": "Senegal", + "SM": "San Marino", + "SL": "Sierra Leone", + "SC": "Seychelles", + "KZ": "Kazakhstan", + "KY": "Cayman Islands", + "SG": "Singapore", + "SE": "Sweden", + "SD": "Sudan", + "DO": "Dominican Republic", + "DM": "Dominica", + "DJ": "Djibouti", + "DK": "Denmark", + "VG": "British Virgin Islands", + "DE": "Germany", + "YE": "Yemen", + "DZ": "Algeria", + "US": "United States", + "UY": "Uruguay", + "YT": "Mayotte", + "UM": "United States Minor Outlying Islands", + "LB": "Lebanon", + "LC": "Saint Lucia", + "LA": "Laos", + "TV": "Tuvalu", + "TW": "Taiwan", + "TT": "Trinidad and Tobago", + "TR": "Turkey", + "LK": "Sri Lanka", + "LI": "Liechtenstein", + "LV": "Latvia", + "TO": "Tonga", + "LT": "Lithuania", + "LU": "Luxembourg", + "LR": "Liberia", + "LS": "Lesotho", + "TH": "Thailand", + "TF": "French Southern Territories", + "TG": "Togo", + "TD": "Chad", + "TC": "Turks and Caicos Islands", + "LY": "Libya", + "VA": "Vatican", + "VC": "Saint Vincent and the Grenadines", + "AE": "United Arab Emirates", + "AD": "Andorra", + "AG": "Antigua and Barbuda", + "AF": "Afghanistan", + "AI": "Anguilla", + "VI": "U.S. Virgin Islands", + "IS": "Iceland", + "IR": "Iran", + "AM": "Armenia", + "AL": "Albania", + "AO": "Angola", + "AQ": "Antarctica", + "AS": "American Samoa", + "AR": "Argentina", + "AU": "Australia", + "AT": "Austria", + "AW": "Aruba", + "IN": "India", + "AX": "Aland Islands", + "AZ": "Azerbaijan", + "IE": "Ireland", + "ID": "Indonesia", + "UA": "Ukraine", + "QA": "Qatar", + "MZ": "Mozambique" +} diff --git a/src/main/resources/eu.json b/src/main/resources/eu.json new file mode 100644 index 000000000..1a2543025 --- /dev/null +++ b/src/main/resources/eu.json @@ -0,0 +1,29 @@ +[ + "IE", + "AT", + "LT", + "LU", + "LV", + "DE", + "DK", + "SE", + "SI", + "SK", + "CZ", + "CY", + "NL", + "FI", + "FR", + "MT", + "ES", + "IT", + "EE", + "PL", + "PT", + "HU", + "HR", + "GR", + "RO", + "BG", + "BE" +] diff --git a/src/main/resources/flags.json b/src/main/resources/flags.json new file mode 100644 index 000000000..4c7719918 --- /dev/null +++ b/src/main/resources/flags.json @@ -0,0 +1,252 @@ +{ + "BD": {"emoji": "🇧🇩", "unicode": "U+1F1E7 U+1F1E9"}, + "BE": {"emoji": "🇧🇪", "unicode": "U+1F1E7 U+1F1EA"}, + "BF": {"emoji": "🇧🇫", "unicode": "U+1F1E7 U+1F1EB"}, + "BG": {"emoji": "🇧🇬", "unicode": "U+1F1E7 U+1F1EC"}, + "BA": {"emoji": "🇧🇦", "unicode": "U+1F1E7 U+1F1E6"}, + "BB": {"emoji": "🇧🇧", "unicode": "U+1F1E7 U+1F1E7"}, + "WF": {"emoji": "🇼🇫", "unicode": "U+1F1FC U+1F1EB"}, + "BL": {"emoji": "🇧🇱", "unicode": "U+1F1E7 U+1F1F1"}, + "BM": {"emoji": "🇧🇲", "unicode": "U+1F1E7 U+1F1F2"}, + "BN": {"emoji": "🇧🇳", "unicode": "U+1F1E7 U+1F1F3"}, + "BO": {"emoji": "🇧🇴", "unicode": "U+1F1E7 U+1F1F4"}, + "BH": {"emoji": "🇧🇭", "unicode": "U+1F1E7 U+1F1ED"}, + "BI": {"emoji": "🇧🇮", "unicode": "U+1F1E7 U+1F1EE"}, + "BJ": {"emoji": "🇧🇯", "unicode": "U+1F1E7 U+1F1EF"}, + "BT": {"emoji": "🇧🇹", "unicode": "U+1F1E7 U+1F1F9"}, + "JM": {"emoji": "🇯🇲", "unicode": "U+1F1EF U+1F1F2"}, + "BV": {"emoji": "🇧🇻", "unicode": "U+1F1E7 U+1F1FB"}, + "BW": {"emoji": "🇧🇼", "unicode": "U+1F1E7 U+1F1FC"}, + "WS": {"emoji": "🇼🇸", "unicode": "U+1F1FC U+1F1F8"}, + "BQ": {"emoji": "🇧🇶", "unicode": "U+1F1E7 U+1F1F6"}, + "BR": {"emoji": "🇧🇷", "unicode": "U+1F1E7 U+1F1F7"}, + "BS": {"emoji": "🇧🇸", "unicode": "U+1F1E7 U+1F1F8"}, + "JE": {"emoji": "🇯🇪", "unicode": "U+1F1EF U+1F1EA"}, + "BY": {"emoji": "🇧🇾", "unicode": "U+1F1E7 U+1F1FE"}, + "BZ": {"emoji": "🇧🇿", "unicode": "U+1F1E7 U+1F1FF"}, + "RU": {"emoji": "🇷🇺", "unicode": "U+1F1F7 U+1F1FA"}, + "RW": {"emoji": "🇷🇼", "unicode": "U+1F1F7 U+1F1FC"}, + "RS": {"emoji": "🇷🇸", "unicode": "U+1F1F7 U+1F1F8"}, + "TL": {"emoji": "🇹🇱", "unicode": "U+1F1F9 U+1F1F1"}, + "RE": {"emoji": "🇷🇪", "unicode": "U+1F1F7 U+1F1EA"}, + "TM": {"emoji": "🇹🇲", "unicode": "U+1F1F9 U+1F1F2"}, + "TJ": {"emoji": "🇹🇯", "unicode": "U+1F1F9 U+1F1EF"}, + "RO": {"emoji": "🇷🇴", "unicode": "U+1F1F7 U+1F1F4"}, + "TK": {"emoji": "🇹🇰", "unicode": "U+1F1F9 U+1F1F0"}, + "GW": {"emoji": "🇬🇼", "unicode": "U+1F1EC U+1F1FC"}, + "GU": {"emoji": "🇬🇺", "unicode": "U+1F1EC U+1F1FA"}, + "GT": {"emoji": "🇬🇹", "unicode": "U+1F1EC U+1F1F9"}, + "GS": {"emoji": "🇬🇸", "unicode": "U+1F1EC U+1F1F8"}, + "GR": {"emoji": "🇬🇷", "unicode": "U+1F1EC U+1F1F7"}, + "GQ": {"emoji": "🇬🇶", "unicode": "U+1F1EC U+1F1F6"}, + "GP": {"emoji": "🇬🇵", "unicode": "U+1F1EC U+1F1F5"}, + "JP": {"emoji": "🇯🇵", "unicode": "U+1F1EF U+1F1F5"}, + "GY": {"emoji": "🇬🇾", "unicode": "U+1F1EC U+1F1FE"}, + "GG": {"emoji": "🇬🇬", "unicode": "U+1F1EC U+1F1EC"}, + "GF": {"emoji": "🇬🇫", "unicode": "U+1F1EC U+1F1EB"}, + "GE": {"emoji": "🇬🇪", "unicode": "U+1F1EC U+1F1EA"}, + "GD": {"emoji": "🇬🇩", "unicode": "U+1F1EC U+1F1E9"}, + "GB": {"emoji": "🇬🇧", "unicode": "U+1F1EC U+1F1E7"}, + "GA": {"emoji": "🇬🇦", "unicode": "U+1F1EC U+1F1E6"}, + "SV": {"emoji": "🇸🇻", "unicode": "U+1F1F8 U+1F1FB"}, + "GN": {"emoji": "🇬🇳", "unicode": "U+1F1EC U+1F1F3"}, + "GM": {"emoji": "🇬🇲", "unicode": "U+1F1EC U+1F1F2"}, + "GL": {"emoji": "🇬🇱", "unicode": "U+1F1EC U+1F1F1"}, + "GI": {"emoji": "🇬🇮", "unicode": "U+1F1EC U+1F1EE"}, + "GH": {"emoji": "🇬🇭", "unicode": "U+1F1EC U+1F1ED"}, + "OM": {"emoji": "🇴🇲", "unicode": "U+1F1F4 U+1F1F2"}, + "TN": {"emoji": "🇹🇳", "unicode": "U+1F1F9 U+1F1F3"}, + "JO": {"emoji": "🇯🇴", "unicode": "U+1F1EF U+1F1F4"}, + "HR": {"emoji": "🇭🇷", "unicode": "U+1F1ED U+1F1F7"}, + "HT": {"emoji": "🇭🇹", "unicode": "U+1F1ED U+1F1F9"}, + "HU": {"emoji": "🇭🇺", "unicode": "U+1F1ED U+1F1FA"}, + "HK": {"emoji": "🇭🇰", "unicode": "U+1F1ED U+1F1F0"}, + "HN": {"emoji": "🇭🇳", "unicode": "U+1F1ED U+1F1F3"}, + "HM": {"emoji": "🇭🇲", "unicode": "U+1F1ED U+1F1F2"}, + "VE": {"emoji": "🇻🇪", "unicode": "U+1F1FB U+1F1EA"}, + "PR": {"emoji": "🇵🇷", "unicode": "U+1F1F5 U+1F1F7"}, + "PS": {"emoji": "🇵🇸", "unicode": "U+1F1F5 U+1F1F8"}, + "PW": {"emoji": "🇵🇼", "unicode": "U+1F1F5 U+1F1FC"}, + "PT": {"emoji": "🇵🇹", "unicode": "U+1F1F5 U+1F1F9"}, + "SJ": {"emoji": "🇸🇯", "unicode": "U+1F1F8 U+1F1EF"}, + "PY": {"emoji": "🇵🇾", "unicode": "U+1F1F5 U+1F1FE"}, + "IQ": {"emoji": "🇮🇶", "unicode": "U+1F1EE U+1F1F6"}, + "PA": {"emoji": "🇵🇦", "unicode": "U+1F1F5 U+1F1E6"}, + "PF": {"emoji": "🇵🇫", "unicode": "U+1F1F5 U+1F1EB"}, + "PG": {"emoji": "🇵🇬", "unicode": "U+1F1F5 U+1F1EC"}, + "PE": {"emoji": "🇵🇪", "unicode": "U+1F1F5 U+1F1EA"}, + "PK": {"emoji": "🇵🇰", "unicode": "U+1F1F5 U+1F1F0"}, + "PH": {"emoji": "🇵🇭", "unicode": "U+1F1F5 U+1F1ED"}, + "PN": {"emoji": "🇵🇳", "unicode": "U+1F1F5 U+1F1F3"}, + "PL": {"emoji": "🇵🇱", "unicode": "U+1F1F5 U+1F1F1"}, + "PM": {"emoji": "🇵🇲", "unicode": "U+1F1F5 U+1F1F2"}, + "ZM": {"emoji": "🇿🇲", "unicode": "U+1F1FF U+1F1F2"}, + "EH": {"emoji": "🇪🇭", "unicode": "U+1F1EA U+1F1ED"}, + "EE": {"emoji": "🇪🇪", "unicode": "U+1F1EA U+1F1EA"}, + "EG": {"emoji": "🇪🇬", "unicode": "U+1F1EA U+1F1EC"}, + "ZA": {"emoji": "🇿🇦", "unicode": "U+1F1FF U+1F1E6"}, + "EC": {"emoji": "🇪🇨", "unicode": "U+1F1EA U+1F1E8"}, + "IT": {"emoji": "🇮🇹", "unicode": "U+1F1EE U+1F1F9"}, + "VN": {"emoji": "🇻🇳", "unicode": "U+1F1FB U+1F1F3"}, + "SB": {"emoji": "🇸🇧", "unicode": "U+1F1F8 U+1F1E7"}, + "ET": {"emoji": "🇪🇹", "unicode": "U+1F1EA U+1F1F9"}, + "SO": {"emoji": "🇸🇴", "unicode": "U+1F1F8 U+1F1F4"}, + "ZW": {"emoji": "🇿🇼", "unicode": "U+1F1FF U+1F1FC"}, + "SA": {"emoji": "🇸🇦", "unicode": "U+1F1F8 U+1F1E6"}, + "ES": {"emoji": "🇪🇸", "unicode": "U+1F1EA U+1F1F8"}, + "ER": {"emoji": "🇪🇷", "unicode": "U+1F1EA U+1F1F7"}, + "ME": {"emoji": "🇲🇪", "unicode": "U+1F1F2 U+1F1EA"}, + "MD": {"emoji": "🇲🇩", "unicode": "U+1F1F2 U+1F1E9"}, + "MG": {"emoji": "🇲🇬", "unicode": "U+1F1F2 U+1F1EC"}, + "MF": {"emoji": "🇲🇫", "unicode": "U+1F1F2 U+1F1EB"}, + "MA": {"emoji": "🇲🇦", "unicode": "U+1F1F2 U+1F1E6"}, + "MC": {"emoji": "🇲🇨", "unicode": "U+1F1F2 U+1F1E8"}, + "UZ": {"emoji": "🇺🇿", "unicode": "U+1F1FA U+1F1FF"}, + "MM": {"emoji": "🇲🇲", "unicode": "U+1F1F2 U+1F1F2"}, + "ML": {"emoji": "🇲🇱", "unicode": "U+1F1F2 U+1F1F1"}, + "MO": {"emoji": "🇲🇴", "unicode": "U+1F1F2 U+1F1F4"}, + "MN": {"emoji": "🇲🇳", "unicode": "U+1F1F2 U+1F1F3"}, + "MH": {"emoji": "🇲🇭", "unicode": "U+1F1F2 U+1F1ED"}, + "MK": {"emoji": "🇲🇰", "unicode": "U+1F1F2 U+1F1F0"}, + "MU": {"emoji": "🇲🇺", "unicode": "U+1F1F2 U+1F1FA"}, + "MT": {"emoji": "🇲🇹", "unicode": "U+1F1F2 U+1F1F9"}, + "MW": {"emoji": "🇲🇼", "unicode": "U+1F1F2 U+1F1FC"}, + "MV": {"emoji": "🇲🇻", "unicode": "U+1F1F2 U+1F1FB"}, + "MQ": {"emoji": "🇲🇶", "unicode": "U+1F1F2 U+1F1F6"}, + "MP": {"emoji": "🇲🇵", "unicode": "U+1F1F2 U+1F1F5"}, + "MS": {"emoji": "🇲🇸", "unicode": "U+1F1F2 U+1F1F8"}, + "MR": {"emoji": "🇲🇷", "unicode": "U+1F1F2 U+1F1F7"}, + "IM": {"emoji": "🇮🇲", "unicode": "U+1F1EE U+1F1F2"}, + "UG": {"emoji": "🇺🇬", "unicode": "U+1F1FA U+1F1EC"}, + "TZ": {"emoji": "🇹🇿", "unicode": "U+1F1F9 U+1F1FF"}, + "MY": {"emoji": "🇲🇾", "unicode": "U+1F1F2 U+1F1FE"}, + "MX": {"emoji": "🇲🇽", "unicode": "U+1F1F2 U+1F1FD"}, + "IL": {"emoji": "🇮🇱", "unicode": "U+1F1EE U+1F1F1"}, + "FR": {"emoji": "🇫🇷", "unicode": "U+1F1EB U+1F1F7"}, + "IO": {"emoji": "🇮🇴", "unicode": "U+1F1EE U+1F1F4"}, + "SH": {"emoji": "🇸🇭", "unicode": "U+1F1F8 U+1F1ED"}, + "FI": {"emoji": "🇫🇮", "unicode": "U+1F1EB U+1F1EE"}, + "FJ": {"emoji": "🇫🇯", "unicode": "U+1F1EB U+1F1EF"}, + "FK": {"emoji": "🇫🇰", "unicode": "U+1F1EB U+1F1F0"}, + "FM": {"emoji": "🇫🇲", "unicode": "U+1F1EB U+1F1F2"}, + "FO": {"emoji": "🇫🇴", "unicode": "U+1F1EB U+1F1F4"}, + "NI": {"emoji": "🇳🇮", "unicode": "U+1F1F3 U+1F1EE"}, + "NL": {"emoji": "🇳🇱", "unicode": "U+1F1F3 U+1F1F1"}, + "NO": {"emoji": "🇳🇴", "unicode": "U+1F1F3 U+1F1F4"}, + "NA": {"emoji": "🇳🇦", "unicode": "U+1F1F3 U+1F1E6"}, + "VU": {"emoji": "🇻🇺", "unicode": "U+1F1FB U+1F1FA"}, + "NC": {"emoji": "🇳🇨", "unicode": "U+1F1F3 U+1F1E8"}, + "NE": {"emoji": "🇳🇪", "unicode": "U+1F1F3 U+1F1EA"}, + "NF": {"emoji": "🇳🇫", "unicode": "U+1F1F3 U+1F1EB"}, + "NG": {"emoji": "🇳🇬", "unicode": "U+1F1F3 U+1F1EC"}, + "NZ": {"emoji": "🇳🇿", "unicode": "U+1F1F3 U+1F1FF"}, + "NP": {"emoji": "🇳🇵", "unicode": "U+1F1F3 U+1F1F5"}, + "NR": {"emoji": "🇳🇷", "unicode": "U+1F1F3 U+1F1F7"}, + "NU": {"emoji": "🇳🇺", "unicode": "U+1F1F3 U+1F1FA"}, + "CK": {"emoji": "🇨🇰", "unicode": "U+1F1E8 U+1F1F0"}, + "XK": {"emoji": "🇽🇰", "unicode": "U+1F1FD U+1F1F0"}, + "CI": {"emoji": "🇨🇮", "unicode": "U+1F1E8 U+1F1EE"}, + "CH": {"emoji": "🇨🇭", "unicode": "U+1F1E8 U+1F1ED"}, + "CO": {"emoji": "🇨🇴", "unicode": "U+1F1E8 U+1F1F4"}, + "CN": {"emoji": "🇨🇳", "unicode": "U+1F1E8 U+1F1F3"}, + "CM": {"emoji": "🇨🇲", "unicode": "U+1F1E8 U+1F1F2"}, + "CL": {"emoji": "🇨🇱", "unicode": "U+1F1E8 U+1F1F1"}, + "CC": {"emoji": "🇨🇨", "unicode": "U+1F1E8 U+1F1E8"}, + "CA": {"emoji": "🇨🇦", "unicode": "U+1F1E8 U+1F1E6"}, + "CG": {"emoji": "🇨🇬", "unicode": "U+1F1E8 U+1F1EC"}, + "CF": {"emoji": "🇨🇫", "unicode": "U+1F1E8 U+1F1EB"}, + "CD": {"emoji": "🇨🇩", "unicode": "U+1F1E8 U+1F1E9"}, + "CZ": {"emoji": "🇨🇿", "unicode": "U+1F1E8 U+1F1FF"}, + "CY": {"emoji": "🇨🇾", "unicode": "U+1F1E8 U+1F1FE"}, + "CX": {"emoji": "🇨🇽", "unicode": "U+1F1E8 U+1F1FD"}, + "CR": {"emoji": "🇨🇷", "unicode": "U+1F1E8 U+1F1F7"}, + "CW": {"emoji": "🇨🇼", "unicode": "U+1F1E8 U+1F1FC"}, + "CV": {"emoji": "🇨🇻", "unicode": "U+1F1E8 U+1F1FB"}, + "CU": {"emoji": "🇨🇺", "unicode": "U+1F1E8 U+1F1FA"}, + "SZ": {"emoji": "🇸🇿", "unicode": "U+1F1F8 U+1F1FF"}, + "SY": {"emoji": "🇸🇾", "unicode": "U+1F1F8 U+1F1FE"}, + "SX": {"emoji": "🇸🇽", "unicode": "U+1F1F8 U+1F1FD"}, + "KG": {"emoji": "🇰🇬", "unicode": "U+1F1F0 U+1F1EC"}, + "KE": {"emoji": "🇰🇪", "unicode": "U+1F1F0 U+1F1EA"}, + "SS": {"emoji": "🇸🇸", "unicode": "U+1F1F8 U+1F1F8"}, + "SR": {"emoji": "🇸🇷", "unicode": "U+1F1F8 U+1F1F7"}, + "KI": {"emoji": "🇰🇮", "unicode": "U+1F1F0 U+1F1EE"}, + "KH": {"emoji": "🇰🇭", "unicode": "U+1F1F0 U+1F1ED"}, + "KN": {"emoji": "🇰🇳", "unicode": "U+1F1F0 U+1F1F3"}, + "KM": {"emoji": "🇰🇲", "unicode": "U+1F1F0 U+1F1F2"}, + "ST": {"emoji": "🇸🇹", "unicode": "U+1F1F8 U+1F1F9"}, + "SK": {"emoji": "🇸🇰", "unicode": "U+1F1F8 U+1F1F0"}, + "KR": {"emoji": "🇰🇷", "unicode": "U+1F1F0 U+1F1F7"}, + "SI": {"emoji": "🇸🇮", "unicode": "U+1F1F8 U+1F1EE"}, + "KP": {"emoji": "🇰🇵", "unicode": "U+1F1F0 U+1F1F5"}, + "KW": {"emoji": "🇰🇼", "unicode": "U+1F1F0 U+1F1FC"}, + "SN": {"emoji": "🇸🇳", "unicode": "U+1F1F8 U+1F1F3"}, + "SM": {"emoji": "🇸🇲", "unicode": "U+1F1F8 U+1F1F2"}, + "SL": {"emoji": "🇸🇱", "unicode": "U+1F1F8 U+1F1F1"}, + "SC": {"emoji": "🇸🇨", "unicode": "U+1F1F8 U+1F1E8"}, + "KZ": {"emoji": "🇰🇿", "unicode": "U+1F1F0 U+1F1FF"}, + "KY": {"emoji": "🇰🇾", "unicode": "U+1F1F0 U+1F1FE"}, + "SG": {"emoji": "🇸🇬", "unicode": "U+1F1F8 U+1F1EC"}, + "SE": {"emoji": "🇸🇪", "unicode": "U+1F1F8 U+1F1EA"}, + "SD": {"emoji": "🇸🇩", "unicode": "U+1F1F8 U+1F1E9"}, + "DO": {"emoji": "🇩🇴", "unicode": "U+1F1E9 U+1F1F4"}, + "DM": {"emoji": "🇩🇲", "unicode": "U+1F1E9 U+1F1F2"}, + "DJ": {"emoji": "🇩🇯", "unicode": "U+1F1E9 U+1F1EF"}, + "DK": {"emoji": "🇩🇰", "unicode": "U+1F1E9 U+1F1F0"}, + "VG": {"emoji": "🇻🇬", "unicode": "U+1F1FB U+1F1EC"}, + "DE": {"emoji": "🇩🇪", "unicode": "U+1F1E9 U+1F1EA"}, + "YE": {"emoji": "🇾🇪", "unicode": "U+1F1FE U+1F1EA"}, + "DZ": {"emoji": "🇩🇿", "unicode": "U+1F1E9 U+1F1FF"}, + "US": {"emoji": "🇺🇸", "unicode": "U+1F1FA U+1F1F8"}, + "UY": {"emoji": "🇺🇾", "unicode": "U+1F1FA U+1F1FE"}, + "YT": {"emoji": "🇾🇹", "unicode": "U+1F1FE U+1F1F9"}, + "UM": {"emoji": "🇺🇲", "unicode": "U+1F1FA U+1F1F2"}, + "LB": {"emoji": "🇱🇧", "unicode": "U+1F1F1 U+1F1E7"}, + "LC": {"emoji": "🇱🇨", "unicode": "U+1F1F1 U+1F1E8"}, + "LA": {"emoji": "🇱🇦", "unicode": "U+1F1F1 U+1F1E6"}, + "TV": {"emoji": "🇹🇻", "unicode": "U+1F1F9 U+1F1FB"}, + "TW": {"emoji": "🇹🇼", "unicode": "U+1F1F9 U+1F1FC"}, + "TT": {"emoji": "🇹🇹", "unicode": "U+1F1F9 U+1F1F9"}, + "TR": {"emoji": "🇹🇷", "unicode": "U+1F1F9 U+1F1F7"}, + "LK": {"emoji": "🇱🇰", "unicode": "U+1F1F1 U+1F1F0"}, + "LI": {"emoji": "🇱🇮", "unicode": "U+1F1F1 U+1F1EE"}, + "LV": {"emoji": "🇱🇻", "unicode": "U+1F1F1 U+1F1FB"}, + "TO": {"emoji": "🇹🇴", "unicode": "U+1F1F9 U+1F1F4"}, + "LT": {"emoji": "🇱🇹", "unicode": "U+1F1F1 U+1F1F9"}, + "LU": {"emoji": "🇱🇺", "unicode": "U+1F1F1 U+1F1FA"}, + "LR": {"emoji": "🇱🇷", "unicode": "U+1F1F1 U+1F1F7"}, + "LS": {"emoji": "🇱🇸", "unicode": "U+1F1F1 U+1F1F8"}, + "TH": {"emoji": "🇹🇭", "unicode": "U+1F1F9 U+1F1ED"}, + "TF": {"emoji": "🇹🇫", "unicode": "U+1F1F9 U+1F1EB"}, + "TG": {"emoji": "🇹🇬", "unicode": "U+1F1F9 U+1F1EC"}, + "TD": {"emoji": "🇹🇩", "unicode": "U+1F1F9 U+1F1E9"}, + "TC": {"emoji": "🇹🇨", "unicode": "U+1F1F9 U+1F1E8"}, + "LY": {"emoji": "🇱🇾", "unicode": "U+1F1F1 U+1F1FE"}, + "VA": {"emoji": "🇻🇦", "unicode": "U+1F1FB U+1F1E6"}, + "VC": {"emoji": "🇻🇨", "unicode": "U+1F1FB U+1F1E8"}, + "AE": {"emoji": "🇦🇪", "unicode": "U+1F1E6 U+1F1EA"}, + "AD": {"emoji": "🇦🇩", "unicode": "U+1F1E6 U+1F1E9"}, + "AG": {"emoji": "🇦🇬", "unicode": "U+1F1E6 U+1F1EC"}, + "AF": {"emoji": "🇦🇫", "unicode": "U+1F1E6 U+1F1EB"}, + "AI": {"emoji": "🇦🇮", "unicode": "U+1F1E6 U+1F1EE"}, + "VI": {"emoji": "🇻🇮", "unicode": "U+1F1FB U+1F1EE"}, + "IS": {"emoji": "🇮🇸", "unicode": "U+1F1EE U+1F1F8"}, + "IR": {"emoji": "🇮🇷", "unicode": "U+1F1EE U+1F1F7"}, + "AM": {"emoji": "🇦🇲", "unicode": "U+1F1E6 U+1F1F2"}, + "AL": {"emoji": "🇦🇱", "unicode": "U+1F1E6 U+1F1F1"}, + "AO": {"emoji": "🇦🇴", "unicode": "U+1F1E6 U+1F1F4"}, + "AQ": {"emoji": "🇦🇶", "unicode": "U+1F1E6 U+1F1F6"}, + "AS": {"emoji": "🇦🇸", "unicode": "U+1F1E6 U+1F1F8"}, + "AR": {"emoji": "🇦🇷", "unicode": "U+1F1E6 U+1F1F7"}, + "AU": {"emoji": "🇦🇺", "unicode": "U+1F1E6 U+1F1FA"}, + "AT": {"emoji": "🇦🇹", "unicode": "U+1F1E6 U+1F1F9"}, + "AW": {"emoji": "🇦🇼", "unicode": "U+1F1E6 U+1F1FC"}, + "IN": {"emoji": "🇮🇳", "unicode": "U+1F1EE U+1F1F3"}, + "AX": {"emoji": "🇦🇽", "unicode": "U+1F1E6 U+1F1FD"}, + "AZ": {"emoji": "🇦🇿", "unicode": "U+1F1E6 U+1F1FF"}, + "IE": {"emoji": "🇮🇪", "unicode": "U+1F1EE U+1F1EA"}, + "ID": {"emoji": "🇮🇩", "unicode": "U+1F1EE U+1F1E9"}, + "UA": {"emoji": "🇺🇦", "unicode": "U+1F1FA U+1F1E6"}, + "QA": {"emoji": "🇶🇦", "unicode": "U+1F1F6 U+1F1E6"}, + "MZ": {"emoji": "🇲🇿", "unicode": "U+1F1F2 U+1F1FF"} +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index ed22671b1..d42cc8b92 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -15,7 +15,6 @@ size.award.awardName=수상명은 {min}자 이상 {max}자 이하로 입력하 size.blog.title=제목은 {min}자 이상 {max}자 이하로 입력하세요. size.blog.subTitle=부제목은 {min}자 이상 {max}자 이하로 입력하세요. size.blog.content=내용은 {min}자 이상 {max}자 이하로 입력하세요. -size.board.category=카테고리는 {min}자 이상 {max}자 이하로 입력하세요. size.board.title=제목은 {min}자 이상 {max}자 이하로 입력하세요. size.board.content=내용은 {min}자 이상 {max}자 이하로 입력하세요. size.comment.content=댓글 내용은 {min}자 이상 {max}자 이하로 입력하세요. @@ -23,6 +22,7 @@ size.donation.message=메시지는 {min}자 이상 {max}자 이하로 입력하 size.position.year=연도는 {min}자 이상 {max}자 이하로 입력하세요. size.jobPosting.title=공고명은 {min}자 이상 {max}자 이하로 입력하세요. size.jobPosting.companyName=기업명은 {min}자 이상 입력하세요. +size.jobPosting.jobPostingUrl=채용 공고 URL은 {max}자 이하 입력하세요. size.login.id=아이디를 {min}자 이상 입력하세요. size.login.password=비밀번호를 {min}자 이상 입력하세요. size.token.token=토큰을 {min}자 이상 입력하세요. @@ -49,8 +49,8 @@ size.review.content=내용은 {min}자 이상 {max}자 이하여야 합니다. size.sharedAccount.username=유저명은 {min}자 이상이어야 합니다. size.sharedAccount.password=비밀번호는 {min}자 이상이어야 합니다. size.sharedAccount.platformName=플랫폼 이름은 {min}자 이상이어야 합니다. -size.verificationCode.memberId=학번은 {min}자 이상 {max}자 이하로 입력하세요. -size.verificationCode.verificationCode=인증 코드는 {min}자 이상 {max}자 이하로 입력하세요. +size.verification.memberId=학번은 {min}자 이상 {max}자 이하로 입력하세요. +size.verification.verification=인증 코드는 {min}자 이상 {max}자 이하로 입력하세요. size.workExperience.companyName=회사명은 최소 {min}글자 이상이어야 합니다. size.workExperience.position=직책은 최소 {min}글자 이상이어야 합니다. range.activityGroup.progress=진행도는 0에서 100 사이의 값이어야 합니다. @@ -181,8 +181,8 @@ notNull.sharedAccount.platformName=플랫폼 이름은 필수 입력 항목입 notNull.sharedAccount.platformUrl=플랫폼 URL은 필수 입력 항목입니다. notNull.sharedAccountUsage.sharedAccountId=공유 계정 식별 ID는 필수 입력 항목입니다. notNull.sharedAccountUsage.endTime=계정 이용 종료 시간은 필수 입력 항목입니다. -notNull.verificationCode.memberId=학번은 필수 입력 항목입니다. -notNull.verificationCode.verificationCode=인증 코드는 필수 입력 항목입니다. +notNull.verification.memberId=학번은 필수 입력 항목입니다. +notNull.verification.verification=인증 코드는 필수 입력 항목입니다. notNull.workExperience.companyName=회사명은 필수 입력 항목입니다. notNull.workExperience.position=직책은 필수 입력 항목입니다. notNull.workExperience.startDate=시작일은 필수 입력 항목입니다.