diff --git a/.github/workflows/aws-cicd-dev.yml b/.github/workflows/aws-cicd-dev.yml index 37627514..5a08d95f 100644 --- a/.github/workflows/aws-cicd-dev.yml +++ b/.github/workflows/aws-cicd-dev.yml @@ -4,7 +4,9 @@ on: push: branches: - develop - - feat/LA-20_3 + pull_request: + branches: + - develop env: REGISTRY: "docker.io" @@ -127,6 +129,7 @@ jobs: name: Deploy needs: [ build, setup ] runs-on: ubuntu-latest + if: github.event_name != 'pull_request' env: DEPLOY_TARGET: ${{ needs.setup.outputs.deploy_target }} diff --git a/.github/workflows/aws-cicd-prod.yml b/.github/workflows/aws-cicd-prod.yml index 33aa5669..249c4645 100644 --- a/.github/workflows/aws-cicd-prod.yml +++ b/.github/workflows/aws-cicd-prod.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main env: REGISTRY: "docker.io" @@ -127,6 +130,7 @@ jobs: name: Deploy needs: [ build, setup ] runs-on: ubuntu-latest + if: github.event_name != 'pull_request' env: DEPLOY_TARGET: ${{ needs.setup.outputs.deploy_target }} diff --git a/.gitignore b/.gitignore index 4e76a4c9..a84fdccb 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,7 @@ out/ #*.properties -*.sql - +layer-api/src/main/resources/data.sql layer-domain/src/main/generated @@ -58,4 +57,8 @@ credentials.json layer-api/src/main/resources/tokens/StoredCredential layer-batch/src/main/resources/application-secret.properties -layer-admin/src/main/resources/application-secret.properties \ No newline at end of file +layer-admin/src/main/resources/application-secret.properties +layer-admin/src/main/resources/application.yml +layer-admin/src/main/resources/data.sql + + diff --git a/build.gradle b/build.gradle index ac8dbd9e..f43b9470 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,7 @@ project(":layer-api") { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' //== jwt ==// implementation 'io.jsonwebtoken:jjwt-api:0.12.5' @@ -201,6 +202,7 @@ project(":layer-admin") { dependencies { implementation project(path: ':layer-domain') + implementation project(path: ':layer-common') implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -211,6 +213,9 @@ project(":layer-admin") { // Security implementation 'org.springframework.boot:spring-boot-starter-security' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } } \ No newline at end of file diff --git a/layer-admin/src/main/java/org/layer/common/exception/AdminException.java b/layer-admin/src/main/java/org/layer/common/exception/AdminException.java new file mode 100644 index 00000000..4fbb87fa --- /dev/null +++ b/layer-admin/src/main/java/org/layer/common/exception/AdminException.java @@ -0,0 +1,7 @@ +package org.layer.common.exception; + +public class AdminException extends BaseCustomException { + public AdminException(ExceptionType exceptionType) { + super(exceptionType); + } +} diff --git a/layer-admin/src/main/java/org/layer/config/RedisConfig.java b/layer-admin/src/main/java/org/layer/config/RedisConfig.java new file mode 100644 index 00000000..508ff4d7 --- /dev/null +++ b/layer-admin/src/main/java/org/layer/config/RedisConfig.java @@ -0,0 +1,68 @@ +package org.layer.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + + // prod에서 최근 서비스 이용 시점 기록 - 1번 데이터베이스 + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(port); + redisStandaloneConfiguration.setDatabase(1); // 1번 데이터베이스 + + return new LettuceConnectionFactory(redisStandaloneConfiguration); + + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory()); + + PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator + .builder() + .allowIfSubType(Object.class) + .build(); + + ObjectMapper objectMapper = new ObjectMapper() + .findAndRegisterModules() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) + .activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL) + .registerModule(new JavaTimeModule()); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + + return template; + } + +} \ No newline at end of file diff --git a/layer-admin/src/main/java/org/layer/member/controller/dto/GetMemberActivityResponse.java b/layer-admin/src/main/java/org/layer/member/controller/dto/GetMemberActivityResponse.java index 3a957e66..be97eb31 100644 --- a/layer-admin/src/main/java/org/layer/member/controller/dto/GetMemberActivityResponse.java +++ b/layer-admin/src/main/java/org/layer/member/controller/dto/GetMemberActivityResponse.java @@ -1,17 +1,17 @@ package org.layer.member.controller.dto; -import java.time.LocalDateTime; - import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; + @Schema(name = "GetMemberActivityResponse", description = "회원 활동 Dto") public record GetMemberActivityResponse( @NotNull @Schema(description = "회원 이름", example = "홍길동") String name, @NotNull - @Schema(description = "최근 활동 날짜", example = "2024-11-30T16:21:47.031Z") + @Schema(description = "최근 활동 날짜, 최근 6개월 동안 접속 없을 시 null", example = "2024-11-30T16:21:47.031Z") LocalDateTime recentActivityDate, @NotNull @Schema(description = "소속된 스페이스 수", example = "7") diff --git a/layer-admin/src/main/java/org/layer/member/service/AdminMemberService.java b/layer-admin/src/main/java/org/layer/member/service/AdminMemberService.java index d761a368..cce764d6 100644 --- a/layer-admin/src/main/java/org/layer/member/service/AdminMemberService.java +++ b/layer-admin/src/main/java/org/layer/member/service/AdminMemberService.java @@ -1,7 +1,8 @@ package org.layer.member.service; -import java.util.List; +import lombok.RequiredArgsConstructor; +import org.layer.common.dto.RecentActivityDto; import org.layer.domain.answer.repository.AdminAnswerRepository; import org.layer.domain.member.entity.Member; import org.layer.domain.member.repository.AdminMemberRepository; @@ -11,10 +12,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; +import java.util.List; @Service @RequiredArgsConstructor @@ -23,6 +25,7 @@ public class AdminMemberService { private final AdminMemberRepository adminMemberRepository; private final AdminMemberSpaceRelationRepository adminMemberSpaceRelationRepository; private final AdminAnswerRepository adminAnswerRepository; + private final RedisTemplate redisTemplate; @Value("${admin.password}") private String password; @@ -43,8 +46,12 @@ public GetMembersActivitiesResponse getMemberActivities(String password, int pag Long spaceCount = adminMemberSpaceRelationRepository.countAllByMemberId(member.getId()); Long retrospectAnswerCount = adminAnswerRepository.countAllByMemberId(member.getId()); - return new GetMemberActivityResponse(member.getName(), null, spaceCount, retrospectAnswerCount, - member.getCreatedAt(), member.getSocialType().name()); + RecentActivityDto recentActivityDto = (RecentActivityDto)redisTemplate.opsForValue() + .get(Long.toString(member.getId())); + + return new GetMemberActivityResponse(member.getName(), + recentActivityDto == null ? null : recentActivityDto.getRecentActivityDate(), + spaceCount, retrospectAnswerCount, member.getCreatedAt(), member.getSocialType().name()); }).toList(); return new GetMembersActivitiesResponse(responses); diff --git a/layer-admin/src/main/java/org/layer/retrospect/controller/AdminRetrospectApi.java b/layer-admin/src/main/java/org/layer/retrospect/controller/AdminRetrospectApi.java new file mode 100644 index 00000000..528b6e8c --- /dev/null +++ b/layer-admin/src/main/java/org/layer/retrospect/controller/AdminRetrospectApi.java @@ -0,0 +1,45 @@ +package org.layer.retrospect.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.layer.domain.retrospect.dto.AdminRetrospectCountGroupBySpaceGetResponse; +import org.layer.retrospect.controller.dto.AdminRetrospectCountGetResponse; +import org.layer.retrospect.controller.dto.AdminRetrospectsGetResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; + +import java.time.LocalDateTime; +import java.util.List; + +@Tag(name = "[ADMIN] 회고 데이터", description = "회고 관련 api") +public interface AdminRetrospectApi { + + + @Operation(summary = "회고 관련 데이터 조회", description = "") + @Parameters({ + @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) + ResponseEntity getRetrospectData(@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); + + @Operation(summary = "회고 개수 조회", description = "특정 기간내에 시작된 회고 개수를 조회합니다. (우리 팀원이 만든 스페이스에서 진행된 회고는 제외)") + @Parameters({ + @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) + ResponseEntity getRetrospectCount(@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); + + @Operation(summary = "특정 기간 내 회고 개수 스페이스 별로 보기", description = "특정 기간내에 시작된 회고 개수를 스페이스 별로 조회합니다. (우리 팀원이 만든 스페이스는 제외)") + @Parameters({ + @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) + ResponseEntity> getRetrospectCountGroupBySpace (@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @RequestParam("password") String password); +} diff --git a/layer-admin/src/main/java/org/layer/retrospect/controller/AdminRetrospectController.java b/layer-admin/src/main/java/org/layer/retrospect/controller/AdminRetrospectController.java new file mode 100644 index 00000000..a166a543 --- /dev/null +++ b/layer-admin/src/main/java/org/layer/retrospect/controller/AdminRetrospectController.java @@ -0,0 +1,46 @@ +package org.layer.retrospect.controller; + +import lombok.RequiredArgsConstructor; +import org.layer.domain.retrospect.dto.AdminRetrospectCountGroupBySpaceGetResponse; +import org.layer.retrospect.controller.dto.AdminRetrospectCountGetResponse; +import org.layer.retrospect.controller.dto.AdminRetrospectsGetResponse; +import org.layer.retrospect.service.AdminRetrospectService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RequestMapping("/retrospect") +@RequiredArgsConstructor +@RestController +public class AdminRetrospectController implements AdminRetrospectApi { + private final AdminRetrospectService adminRetrospectService; + + @Override + @GetMapping + public ResponseEntity getRetrospectData( + @RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @RequestParam("password") String password) { + + return ResponseEntity.ok(adminRetrospectService.getRetrospectData(startDate, endDate, password)); + } + + @Override + @GetMapping("/count/user-only") + public ResponseEntity getRetrospectCount( + @RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @RequestParam("password") String password) { + + return ResponseEntity.ok(adminRetrospectService.getRetrospectCount(startDate, endDate, password)); + } + + @Override + @GetMapping("/count/group-by-space") + public ResponseEntity> getRetrospectCountGroupBySpace(LocalDateTime startDate, LocalDateTime endDate, String password) { + return ResponseEntity.ok(adminRetrospectService.getRetrospectCountGroupSpace(startDate, endDate, password)); + } + +} diff --git a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminRetrospectCountGetResponse.java b/layer-admin/src/main/java/org/layer/retrospect/controller/dto/AdminRetrospectCountGetResponse.java similarity index 89% rename from layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminRetrospectCountGetResponse.java rename to layer-admin/src/main/java/org/layer/retrospect/controller/dto/AdminRetrospectCountGetResponse.java index d746e8d1..e3fc485e 100644 --- a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminRetrospectCountGetResponse.java +++ b/layer-admin/src/main/java/org/layer/retrospect/controller/dto/AdminRetrospectCountGetResponse.java @@ -1,4 +1,4 @@ -package org.layer.domain.admin.controller.dto; +package org.layer.retrospect.controller.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminRetrospectsGetResponse.java b/layer-admin/src/main/java/org/layer/retrospect/controller/dto/AdminRetrospectsGetResponse.java similarity index 87% rename from layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminRetrospectsGetResponse.java rename to layer-admin/src/main/java/org/layer/retrospect/controller/dto/AdminRetrospectsGetResponse.java index fc3049ea..5e26038d 100644 --- a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminRetrospectsGetResponse.java +++ b/layer-admin/src/main/java/org/layer/retrospect/controller/dto/AdminRetrospectsGetResponse.java @@ -1,4 +1,4 @@ -package org.layer.domain.admin.controller.dto; +package org.layer.retrospect.controller.dto; import java.util.List; diff --git a/layer-admin/src/main/java/org/layer/retrospect/service/AdminRetrospectService.java b/layer-admin/src/main/java/org/layer/retrospect/service/AdminRetrospectService.java new file mode 100644 index 00000000..20c0dd2b --- /dev/null +++ b/layer-admin/src/main/java/org/layer/retrospect/service/AdminRetrospectService.java @@ -0,0 +1,52 @@ +package org.layer.retrospect.service; + +import lombok.RequiredArgsConstructor; +import org.layer.domain.retrospect.dto.AdminRetrospectCountGroupBySpaceGetResponse; +import org.layer.domain.retrospect.dto.AdminRetrospectGetResponse; +import org.layer.domain.retrospect.repository.RetrospectAdminRepository; +import org.layer.retrospect.controller.dto.AdminRetrospectCountGetResponse; +import org.layer.retrospect.controller.dto.AdminRetrospectsGetResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminRetrospectService { + private final RetrospectAdminRepository retrospectAdminRepository; + + @Value("${admin.password}") + private String password; + + public AdminRetrospectsGetResponse getRetrospectData(LocalDateTime startDate, LocalDateTime endDate, String requestPassword){ + + if(!requestPassword.equals(password)){ + throw new IllegalArgumentException("비밀번호가 틀렸습니다."); + } + + List retrospects = retrospectAdminRepository.findAllByCreatedAtAfterAndCreatedAtBefore(startDate, endDate); + + return new AdminRetrospectsGetResponse(retrospects, retrospects.size()); + } + + public AdminRetrospectCountGetResponse getRetrospectCount(LocalDateTime startDate, LocalDateTime endDate, String requestPassword) { + if(!requestPassword.equals(password)) { + throw new IllegalArgumentException("비밀번호가 틀렸습니다."); + } + + Long count = retrospectAdminRepository.countRetrospectsExceptForAdminSpace(startDate, endDate); + return new AdminRetrospectCountGetResponse(count); + } + + public List getRetrospectCountGroupSpace(LocalDateTime startDate, LocalDateTime endDate, String requestPassword) { + if(!requestPassword.equals(password)) { + throw new IllegalArgumentException("비밀번호가 틀렸습니다."); + } + + return retrospectAdminRepository.countRetrospectsGroupBySpace(startDate, endDate); + } +} diff --git a/layer-admin/src/main/java/org/layer/space/controller/AdminSpaceApi.java b/layer-admin/src/main/java/org/layer/space/controller/AdminSpaceApi.java new file mode 100644 index 00000000..1864efc2 --- /dev/null +++ b/layer-admin/src/main/java/org/layer/space/controller/AdminSpaceApi.java @@ -0,0 +1,47 @@ +package org.layer.space.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.layer.retrospect.controller.dto.AdminRetrospectCountGetResponse; +import org.layer.space.controller.dto.AdminSpaceCountGetResponse; +import org.layer.space.controller.dto.AdminSpacesGetResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.time.LocalDateTime; + +@Tag(name = "[ADMIN] 스페이스 데이터", description = "스페이스 관련 api") +public interface AdminSpaceApi { + @Operation(summary = "스페이스 관련 데이터 조회", description = "") + @Parameters({ + @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true), + @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true), + @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true) + }) + ResponseEntity getSpaceData(@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); + + @Operation(summary = "스페이스 개수 조회", description = "특정 기간내에 만들어진 스페이스 개수를 조회합니다. (우리 팀원이 만든 스페이스는 제외)") + @Parameters({ + @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) + ResponseEntity getSpaceCount(@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); + + + @Operation(summary = "특정 스페이스 내 회고 개수 조회", description = "특정 기간내에 특정 스페이스 안에서 시작된 회고 개수를 조회합니다.") + @Parameters({ + @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), + @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) + ResponseEntity getRetrospectCountInSpace(@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @PathVariable("spaceId") Long spaceId, + @RequestParam("password") String password); + +} diff --git a/layer-admin/src/main/java/org/layer/space/controller/AdminSpaceController.java b/layer-admin/src/main/java/org/layer/space/controller/AdminSpaceController.java new file mode 100644 index 00000000..b71f2add --- /dev/null +++ b/layer-admin/src/main/java/org/layer/space/controller/AdminSpaceController.java @@ -0,0 +1,47 @@ +package org.layer.space.controller; + +import lombok.RequiredArgsConstructor; +import org.layer.retrospect.controller.dto.AdminRetrospectCountGetResponse; +import org.layer.space.controller.dto.AdminSpaceCountGetResponse; +import org.layer.space.controller.dto.AdminSpacesGetResponse; +import org.layer.space.service.AdminSpaceService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; + +@RequestMapping("/space") +@RequiredArgsConstructor +@RestController +public class AdminSpaceController implements AdminSpaceApi { + + private final AdminSpaceService adminSpaceService; + @Override + @GetMapping + public ResponseEntity getSpaceData( + @RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @RequestParam("password") String password) { + + return ResponseEntity.ok(adminSpaceService.getSpaceData(startDate, endDate, password)); + } + + @Override + @GetMapping("/count/user-only") + public ResponseEntity getSpaceCount( + @RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @RequestParam("password") String password) { + return ResponseEntity.ok(adminSpaceService.getSpaceCount(startDate, endDate, password)); + } + + @Override + @GetMapping("/{spaceId}/retrospect/count") + public ResponseEntity getRetrospectCountInSpace(@RequestParam("startDate") LocalDateTime startDate, + @RequestParam("endDate") LocalDateTime endDate, + @PathVariable("spaceId") Long spaceId, + @RequestParam("password") String password) { + return ResponseEntity.ok(adminSpaceService.getRetrospectCountInSpace(startDate, endDate, spaceId, password)); + } + +} diff --git a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminSpaceCountGetResponse.java b/layer-admin/src/main/java/org/layer/space/controller/dto/AdminSpaceCountGetResponse.java similarity index 89% rename from layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminSpaceCountGetResponse.java rename to layer-admin/src/main/java/org/layer/space/controller/dto/AdminSpaceCountGetResponse.java index 3f25b895..31188424 100644 --- a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminSpaceCountGetResponse.java +++ b/layer-admin/src/main/java/org/layer/space/controller/dto/AdminSpaceCountGetResponse.java @@ -1,4 +1,4 @@ -package org.layer.domain.admin.controller.dto; +package org.layer.space.controller.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminSpacesGetResponse.java b/layer-admin/src/main/java/org/layer/space/controller/dto/AdminSpacesGetResponse.java similarity index 92% rename from layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminSpacesGetResponse.java rename to layer-admin/src/main/java/org/layer/space/controller/dto/AdminSpacesGetResponse.java index 3029aabf..48e317f5 100644 --- a/layer-api/src/main/java/org/layer/domain/admin/controller/dto/AdminSpacesGetResponse.java +++ b/layer-admin/src/main/java/org/layer/space/controller/dto/AdminSpacesGetResponse.java @@ -1,4 +1,4 @@ -package org.layer.domain.admin.controller.dto; +package org.layer.space.controller.dto; import java.util.List; diff --git a/layer-admin/src/main/java/org/layer/space/service/AdminSpaceService.java b/layer-admin/src/main/java/org/layer/space/service/AdminSpaceService.java new file mode 100644 index 00000000..d5d61b89 --- /dev/null +++ b/layer-admin/src/main/java/org/layer/space/service/AdminSpaceService.java @@ -0,0 +1,56 @@ +package org.layer.space.service; + +import lombok.RequiredArgsConstructor; +import org.layer.domain.retrospect.repository.RetrospectAdminRepository; +import org.layer.domain.space.dto.AdminSpaceGetResponse; +import org.layer.domain.space.repository.SpaceAdminRepository; +import org.layer.retrospect.controller.dto.AdminRetrospectCountGetResponse; +import org.layer.space.controller.dto.AdminSpaceCountGetResponse; +import org.layer.space.controller.dto.AdminSpacesGetResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminSpaceService { + + private final SpaceAdminRepository spaceAdminRepository; + private final RetrospectAdminRepository retrospectAdminRepository; + + @Value("${admin.password}") + private String password; + + public AdminSpacesGetResponse getSpaceData(LocalDateTime startDate, LocalDateTime endDate, String requestPassword){ + + if(!requestPassword.equals(password)){ + throw new IllegalArgumentException("비밀번호가 틀렸습니다."); + } + + List spaces = spaceAdminRepository.findAllByCreatedAtAfterAndCreatedAtBefore(startDate, endDate); + + return new AdminSpacesGetResponse(spaces, spaces.size()); + } + + public AdminSpaceCountGetResponse getSpaceCount(LocalDateTime startDate, LocalDateTime endDate, String requestPassword) { + if(!requestPassword.equals(password)) { + throw new IllegalArgumentException("비밀번호가 틀렸습니다."); + } + + Long count = spaceAdminRepository.countSpacesExceptForAdminSpace(startDate, endDate); + return new AdminSpaceCountGetResponse(count); + } + + public AdminRetrospectCountGetResponse getRetrospectCountInSpace(LocalDateTime startDate, LocalDateTime endDate, Long spaceId, String requestPassword) { + if(!requestPassword.equals(password)) { + throw new IllegalArgumentException("비밀번호가 틀렸습니다."); + } + + Long count = retrospectAdminRepository.countRetrospectsBySpaceId(spaceId, startDate, endDate); + return new AdminRetrospectCountGetResponse(count); + } +} diff --git a/layer-admin/src/main/resources/application-dev.yml b/layer-admin/src/main/resources/application-dev.yml index b68b603a..e99506f2 100644 --- a/layer-admin/src/main/resources/application-dev.yml +++ b/layer-admin/src/main/resources/application-dev.yml @@ -21,6 +21,11 @@ spring: show_sql: true open-in-view: false database: mysql + data: + redis: + host: ${DEV_REDIS_HOST} + port: ${DEV_REDIS_PORT} + password: ${DEV_REDIS_PASSWORD} admin: password: ${ADMIN_PASSWORD} \ No newline at end of file diff --git a/layer-admin/src/main/resources/application-local.yml b/layer-admin/src/main/resources/application-local.yml new file mode 100644 index 00000000..11f26409 --- /dev/null +++ b/layer-admin/src/main/resources/application-local.yml @@ -0,0 +1,34 @@ +server: + port: 3000 + +spring: + config: + import: application-secret.properties + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:layer-local-db;DATABASE_TO_UPPER=FALSE;mode=mysql # H2 접속 정보 (전부 소문자로 지정) + username: sa + password: + h2: + console: + enabled: true + path: /h2-console + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + show_sql: true + open-in-view: false + defer-datasource-initialization: true + + data: + redis: + host: localhost + port: 6379 + password: + +admin: + password: ${ADMIN_PASSWORD} \ No newline at end of file diff --git a/layer-admin/src/main/resources/application-prod.yml b/layer-admin/src/main/resources/application-prod.yml index 54120ad1..3c0a4f07 100644 --- a/layer-admin/src/main/resources/application-prod.yml +++ b/layer-admin/src/main/resources/application-prod.yml @@ -22,5 +22,11 @@ spring: open-in-view: false database: mysql + data: + redis: + host: ${DEV_REDIS_HOST} + port: ${DEV_REDIS_PORT} + password: ${DEV_REDIS_PASSWORD} + admin: password: ${ADMIN_PASSWORD} \ No newline at end of file diff --git a/layer-api/infra/development/Dockerfile-redis b/layer-api/infra/development/Dockerfile-redis new file mode 100644 index 00000000..f1ba86b8 --- /dev/null +++ b/layer-api/infra/development/Dockerfile-redis @@ -0,0 +1,8 @@ +# Base image +FROM redis:latest + +# Copy custom Redis configuration +COPY redis.conf /usr/local/etc/redis/redis.conf + +# Command to run Redis with the custom configuration +CMD ["redis-server", "/usr/local/etc/redis/redis.conf"] \ No newline at end of file diff --git a/layer-api/infra/development/docker-compose.yaml b/layer-api/infra/development/docker-compose.yaml index b0ea5bbe..340313bd 100644 --- a/layer-api/infra/development/docker-compose.yaml +++ b/layer-api/infra/development/docker-compose.yaml @@ -1,4 +1,16 @@ services: + redis: + build: + context: . + dockerfile: Dockerfile-redis + ports: + - "6379:6379" + volumes: + - redis-data:/data # Persistent data storage + restart: always + networks: + - app-network + java-app: image: docker.io/clean01/layer-server_layer-api:latest container_name: layer-api @@ -9,10 +21,14 @@ services: - SPRING_PROFILES_ACTIVE=dev volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" batch-job: image: docker.io/clean01/layer-server_layer-batch:latest # @@ -21,13 +37,17 @@ services: - TZ=Asia/Seoul volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network depends_on: - java-app restart: always + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" admin-app: image: docker.io/clean01/layer-server_layer-admin:latest # @@ -39,13 +59,17 @@ services: - SPRING_PROFILES_ACTIVE=dev volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network depends_on: - java-app restart: always + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" nginx: image: nginx:latest @@ -60,4 +84,7 @@ services: - app-network networks: - app-network: \ No newline at end of file + app-network: + +volumes: + redis-data: \ No newline at end of file diff --git a/layer-api/infra/development/nginx.conf b/layer-api/infra/development/nginx.conf index fabe06a6..530c5113 100644 --- a/layer-api/infra/development/nginx.conf +++ b/layer-api/infra/development/nginx.conf @@ -12,7 +12,7 @@ http { # api.layerapp.io에 대한 서버 블록 server { listen 80; - server_name api.layerapp.io; + server_name stgapi.layerapp.io; location / { proxy_pass http://layer-api; diff --git a/layer-api/infra/development/redis.conf b/layer-api/infra/development/redis.conf new file mode 100644 index 00000000..21c696c5 --- /dev/null +++ b/layer-api/infra/development/redis.conf @@ -0,0 +1,20 @@ +# Save the DB snapshot every 180 seconds if at least 1 key changes +save 180 1 + +# Specify the filename for the RDB file +dbfilename dump.rdb + +# Directory where the RDB snapshot will be saved +dir /data + +# Enable RDB snapshot logging (optional for debugging) +loglevel notice + +# Disable AOF (if you want only RDB persistence) +appendonly no + +# Compression for RDB files (enabled by default) +rdbcompression yes + +# Checksum verification for RDB files (enabled by default) +rdbchecksum yes diff --git a/layer-api/infra/production/Dockerfile-redis b/layer-api/infra/production/Dockerfile-redis new file mode 100644 index 00000000..f1ba86b8 --- /dev/null +++ b/layer-api/infra/production/Dockerfile-redis @@ -0,0 +1,8 @@ +# Base image +FROM redis:latest + +# Copy custom Redis configuration +COPY redis.conf /usr/local/etc/redis/redis.conf + +# Command to run Redis with the custom configuration +CMD ["redis-server", "/usr/local/etc/redis/redis.conf"] \ No newline at end of file diff --git a/layer-api/infra/production/docker-compose-blue.yaml b/layer-api/infra/production/docker-compose-blue.yaml index 1f7311d0..ee46871e 100644 --- a/layer-api/infra/production/docker-compose-blue.yaml +++ b/layer-api/infra/production/docker-compose-blue.yaml @@ -1,4 +1,16 @@ services: + redis: + build: + context: . + dockerfile: Dockerfile-redis + ports: + - "6379:6379" + volumes: + - redis-data:/data # Persistent data storage + restart: always + networks: + - app-network + layer-api-blue: image: docker.io/clean01/layer-server_layer-api:latest container_name: layer-api-blue @@ -9,10 +21,14 @@ services: - SPRING_PROFILES_ACTIVE=prod volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" batch-job-blue: image: docker.io/clean01/layer-server_layer-batch:latest @@ -21,13 +37,17 @@ services: - TZ=Asia/Seoul volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network depends_on: - layer-api-blue restart: always + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" admin-app-blue: image: docker.io/clean01/layer-server_layer-admin:latest # @@ -39,13 +59,20 @@ services: - SPRING_PROFILES_ACTIVE=prod volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network depends_on: - layer-api-blue restart: always + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - app-network: \ No newline at end of file + app-network: + +volumes: + redis-data: \ No newline at end of file diff --git a/layer-api/infra/production/docker-compose-green.yaml b/layer-api/infra/production/docker-compose-green.yaml index 9df97019..7e4b7aa0 100644 --- a/layer-api/infra/production/docker-compose-green.yaml +++ b/layer-api/infra/production/docker-compose-green.yaml @@ -1,4 +1,16 @@ services: + redis: + build: + context: . + dockerfile: Dockerfile-redis + ports: + - "6379:6379" + volumes: + - redis-data:/data # Persistent data storage + restart: always + networks: + - app-network + layer-api-green: image: docker.io/clean01/layer-server_layer-api:latest container_name: layer-api-green @@ -9,10 +21,14 @@ services: - SPRING_PROFILES_ACTIVE=prod volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" batch-job-green: image: docker.io/clean01/layer-server_layer-batch:latest @@ -21,13 +37,17 @@ services: - TZ=Asia/Seoul volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network depends_on: - layer-api-green restart: always + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" admin-app-green: image: docker.io/clean01/layer-server_layer-admin:latest # @@ -39,13 +59,20 @@ services: - SPRING_PROFILES_ACTIVE=prod volumes: - ./application-secret.properties:/config/application-secret.properties - - ./log:/log - ./tokens:/config/tokens networks: - app-network depends_on: - layer-api-green restart: always + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" networks: - app-network: \ No newline at end of file + app-network: + +volumes: + redis-data: \ No newline at end of file diff --git a/layer-api/infra/production/redis.conf b/layer-api/infra/production/redis.conf new file mode 100644 index 00000000..21c696c5 --- /dev/null +++ b/layer-api/infra/production/redis.conf @@ -0,0 +1,20 @@ +# Save the DB snapshot every 180 seconds if at least 1 key changes +save 180 1 + +# Specify the filename for the RDB file +dbfilename dump.rdb + +# Directory where the RDB snapshot will be saved +dir /data + +# Enable RDB snapshot logging (optional for debugging) +loglevel notice + +# Disable AOF (if you want only RDB persistence) +appendonly no + +# Compression for RDB files (enabled by default) +rdbcompression yes + +# Checksum verification for RDB files (enabled by default) +rdbchecksum yes diff --git a/layer-api/src/main/java/org/layer/aop/RecentAccessHistoryAop.java b/layer-api/src/main/java/org/layer/aop/RecentAccessHistoryAop.java new file mode 100644 index 00000000..4771d402 --- /dev/null +++ b/layer-api/src/main/java/org/layer/aop/RecentAccessHistoryAop.java @@ -0,0 +1,52 @@ +package org.layer.aop; + + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.layer.common.dto.RecentActivityDto; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; + +@RequiredArgsConstructor +@Aspect +@Component +public class RecentAccessHistoryAop { + @Qualifier("recentActivityDate") + private final RedisTemplate redisTemplate; + + // 모든 layer-api 모듈 내의 controller package에 존재하는 클래스 + @Around("execution(* org.layer.domain..controller..*(..))") + public Object recordRecentAccessHistory(ProceedingJoinPoint pjp) throws Throwable { + Long memberId = getCurrentMemberId(); + + if(memberId != null) { // 멤버 아이디가 있다면 현재 시간을 저장. + setRecentTime(Long.toString(memberId), LocalDateTime.now()); + } + Object result = pjp.proceed(); + return result; + } + + private Long getCurrentMemberId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + try { + return Long.parseLong(authentication.getName()); + } catch(Exception e) { + return null; + } + } + + + private void setRecentTime(String memberId, LocalDateTime recentTime) { + Duration ttl = Duration.ofDays(30 * 6); // 6개월 + redisTemplate.opsForValue().set(memberId, new RecentActivityDto(recentTime), ttl); + } +} \ No newline at end of file diff --git a/layer-api/src/main/java/org/layer/config/RedisConfig.java b/layer-api/src/main/java/org/layer/config/RedisConfig.java index e3372349..619baf92 100644 --- a/layer-api/src/main/java/org/layer/config/RedisConfig.java +++ b/layer-api/src/main/java/org/layer/config/RedisConfig.java @@ -1,6 +1,12 @@ package org.layer.config; -import org.layer.domain.member.entity.Member; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,7 +15,7 @@ import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @@ -34,8 +40,8 @@ public RedisConnectionFactory redisConnectionFactory() { } @Bean - RedisTemplate redisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); + RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); @@ -43,4 +49,41 @@ RedisTemplate redisTemplate() { return redisTemplate; } -} + + // 최근 서비스 이용 시점 기록 - 1번 데이터베이스 + @Bean + @Qualifier("recentActivityDateConnectionFactory") + public RedisConnectionFactory recentActivityDateRedisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(port); + redisStandaloneConfiguration.setDatabase(1); // 1번 데이터베이스 + + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean + @Qualifier("recentActivityDate") + public RedisTemplate recentActivityDateRedisTemplate(@Qualifier("recentActivityDateConnectionFactory") RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(redisConnectionFactory); + + PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator + .builder() + .allowIfSubType(Object.class) + .build(); + + ObjectMapper objectMapper = new ObjectMapper() + .findAndRegisterModules() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) + .activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL) + .registerModule(new JavaTimeModule()); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + + return template; + } + +} \ No newline at end of file diff --git a/layer-api/src/main/java/org/layer/domain/admin/controller/AdminApi.java b/layer-api/src/main/java/org/layer/domain/admin/controller/AdminApi.java deleted file mode 100644 index 213e8949..00000000 --- a/layer-api/src/main/java/org/layer/domain/admin/controller/AdminApi.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.layer.domain.admin.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.layer.domain.admin.controller.dto.AdminRetrospectCountGetResponse; -import org.layer.domain.admin.controller.dto.AdminRetrospectsGetResponse; -import org.layer.domain.admin.controller.dto.AdminSpaceCountGetResponse; -import org.layer.domain.admin.controller.dto.AdminSpacesGetResponse; -import org.layer.domain.retrospect.dto.AdminRetrospectCountGroupBySpaceGetResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestParam; - -import java.time.LocalDateTime; -import java.util.List; - -@Tag(name = "어드민", description = "어드민 관련 API") -public interface AdminApi { - @Operation(summary = "스페이스 관련 데이터 조회", description = "") - @Parameters({ - @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true), - @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true), - @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true) - }) - ResponseEntity getSpaceData(@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); - - @Operation(summary = "회고 관련 데이터 조회", description = "") - @Parameters({ - @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) - ResponseEntity getRetrospectData(@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); - - @Operation(summary = "스페이스 개수 조회", description = "특정 기간내에 만들어진 스페이스 개수를 조회합니다. (우리 팀원이 만든 스페이스는 제외)") - @Parameters({ - @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) - ResponseEntity getSpaceCount(@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); - - @Operation(summary = "회고 개수 조회", description = "특정 기간내에 시작된 회고 개수를 조회합니다. (우리 팀원이 만든 스페이스에서 진행된 회고는 제외)") - @Parameters({ - @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) - ResponseEntity getRetrospectCount(@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, @RequestParam("password") String password); - - - @Operation(summary = "특정 스페이스 내 회고 개수 조회", description = "특정 기간내에 특정 스페이스 안에서 시작된 회고 개수를 조회합니다.") - @Parameters({ - @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) - ResponseEntity getRetrospectCountInSpace(@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @RequestParam("spaceId") Long spaceId, - @RequestParam("password") String password); - - - @Operation(summary = "특정 기간 내 회고 개수 스페이스 별로 보기", description = "특정 기간내에 시작된 회고 개수를 스페이스 별로 조회합니다. (우리 팀원이 만든 스페이스는 제외)") - @Parameters({ - @Parameter(name = "startDate", description = "검색 시작 시간", example = "2024-09-05T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "endDate", description = "검색 종료 시간", example = "2024-09-13T15:30:45", required = true, schema = @Schema(type = "string")), - @Parameter(name = "password", description = "비밀번호 [카톡방으로 공유]", example = "[카톡방으로 공유]", required = true, schema = @Schema(type = "string", format = "string"))}) - ResponseEntity> getRetrospectCountGroupBySpace (@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @RequestParam("password") String password); -} diff --git a/layer-api/src/main/java/org/layer/domain/admin/controller/AdminController.java b/layer-api/src/main/java/org/layer/domain/admin/controller/AdminController.java deleted file mode 100644 index 3a3dd4d8..00000000 --- a/layer-api/src/main/java/org/layer/domain/admin/controller/AdminController.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.layer.domain.admin.controller; - -import lombok.RequiredArgsConstructor; -import org.layer.domain.admin.controller.dto.AdminRetrospectCountGetResponse; -import org.layer.domain.admin.controller.dto.AdminRetrospectsGetResponse; -import org.layer.domain.admin.controller.dto.AdminSpaceCountGetResponse; -import org.layer.domain.admin.controller.dto.AdminSpacesGetResponse; -import org.layer.domain.admin.service.AdminService; -import org.layer.domain.retrospect.dto.AdminRetrospectCountGroupBySpaceGetResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/admin") -public class AdminController implements AdminApi { - private final AdminService adminService; - - @Override - @GetMapping("/space") - public ResponseEntity getSpaceData( - @RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @RequestParam("password") String password) { - - return ResponseEntity.ok(adminService.getSpaceData(startDate, endDate, password)); - } - - @Override - @GetMapping("/retrospect") - public ResponseEntity getRetrospectData( - @RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @RequestParam("password") String password) { - - return ResponseEntity.ok(adminService.getRetrospectData(startDate, endDate, password)); - } - - @Override - @GetMapping("/space/count/user-only") - public ResponseEntity getSpaceCount( - @RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @RequestParam("password") String password) { - return ResponseEntity.ok(adminService.getSpaceCount(startDate, endDate, password)); - } - - @Override - @GetMapping("/retrospect/count/user-only") - public ResponseEntity getRetrospectCount( - @RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @RequestParam("password") String password) { - - return ResponseEntity.ok(adminService.getRetrospectCount(startDate, endDate, password)); - } - - @Override - @GetMapping("space/{spaceId}/retrospect/count") - public ResponseEntity getRetrospectCountInSpace(@RequestParam("startDate") LocalDateTime startDate, - @RequestParam("endDate") LocalDateTime endDate, - @PathVariable("spaceId") Long spaceId, - @RequestParam("password") String password) { - return ResponseEntity.ok(adminService.getRetrospectCountInSpace(startDate, endDate, spaceId, password)); - } - - @Override - @GetMapping("/retrospect/count/group-by-space") - public ResponseEntity> getRetrospectCountGroupBySpace(LocalDateTime startDate, LocalDateTime endDate, String password) { - return ResponseEntity.ok(adminService.getRetrospectCountGroupSpace(startDate, endDate, password)); - } - -} diff --git a/layer-api/src/main/java/org/layer/domain/admin/service/AdminService.java b/layer-api/src/main/java/org/layer/domain/admin/service/AdminService.java deleted file mode 100644 index d2e563bd..00000000 --- a/layer-api/src/main/java/org/layer/domain/admin/service/AdminService.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.layer.domain.admin.service; - -import lombok.RequiredArgsConstructor; -import org.layer.domain.admin.controller.dto.AdminRetrospectCountGetResponse; -import org.layer.domain.admin.controller.dto.AdminRetrospectsGetResponse; -import org.layer.domain.admin.controller.dto.AdminSpaceCountGetResponse; -import org.layer.domain.admin.controller.dto.AdminSpacesGetResponse; -import org.layer.domain.retrospect.dto.AdminRetrospectCountGroupBySpaceGetResponse; -import org.layer.domain.retrospect.dto.AdminRetrospectGetResponse; -import org.layer.domain.retrospect.repository.RetrospectAdminRepository; -import org.layer.domain.space.dto.AdminSpaceGetResponse; -import org.layer.domain.space.repository.SpaceAdminRepository; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AdminService { - - private final SpaceAdminRepository spaceAdminRepository; - private final RetrospectAdminRepository retrospectAdminRepository; - - @Value("${admin.password}") - private String password; - - public AdminSpacesGetResponse getSpaceData(LocalDateTime startDate, LocalDateTime endDate, String requestPassword){ - - if(!requestPassword.equals(password)){ - throw new IllegalArgumentException("비밀번호가 틀렸습니다."); - } - - List spaces = spaceAdminRepository.findAllByCreatedAtAfterAndCreatedAtBefore(startDate, endDate); - - return new AdminSpacesGetResponse(spaces, spaces.size()); - } - - public AdminRetrospectsGetResponse getRetrospectData(LocalDateTime startDate, LocalDateTime endDate, String requestPassword){ - - if(!requestPassword.equals(password)){ - throw new IllegalArgumentException("비밀번호가 틀렸습니다."); - } - - List retrospects = retrospectAdminRepository.findAllByCreatedAtAfterAndCreatedAtBefore(startDate, endDate); - - return new AdminRetrospectsGetResponse(retrospects, retrospects.size()); - } - - public AdminSpaceCountGetResponse getSpaceCount(LocalDateTime startDate, LocalDateTime endDate, String requestPassword) { - if(!requestPassword.equals(password)) { - throw new IllegalArgumentException("비밀번호가 틀렸습니다."); - } - - Long count = spaceAdminRepository.countSpacesExceptForAdminSpace(startDate, endDate); - return new AdminSpaceCountGetResponse(count); - } - - public AdminRetrospectCountGetResponse getRetrospectCount(LocalDateTime startDate, LocalDateTime endDate, String requestPassword) { - if(!requestPassword.equals(password)) { - throw new IllegalArgumentException("비밀번호가 틀렸습니다."); - } - - Long count = retrospectAdminRepository.countRetrospectsExceptForAdminSpace(startDate, endDate); - return new AdminRetrospectCountGetResponse(count); - } - - public AdminRetrospectCountGetResponse getRetrospectCountInSpace(LocalDateTime startDate, LocalDateTime endDate, Long spaceId, String requestPassword) { - if(!requestPassword.equals(password)) { - throw new IllegalArgumentException("비밀번호가 틀렸습니다."); - } - - Long count = retrospectAdminRepository.countRetrospectsBySpaceId(spaceId, startDate, endDate); - return new AdminRetrospectCountGetResponse(count); - } - - public List getRetrospectCountGroupSpace(LocalDateTime startDate, LocalDateTime endDate, String requestPassword) { - if(!requestPassword.equals(password)) { - throw new IllegalArgumentException("비밀번호가 틀렸습니다."); - } - - return retrospectAdminRepository.countRetrospectsGroupBySpace(startDate, endDate); - } -} diff --git a/layer-api/src/main/java/org/layer/domain/answer/controller/AnswerController.java b/layer-api/src/main/java/org/layer/domain/answer/controller/AnswerController.java index c8e01c4a..5374d26a 100644 --- a/layer-api/src/main/java/org/layer/domain/answer/controller/AnswerController.java +++ b/layer-api/src/main/java/org/layer/domain/answer/controller/AnswerController.java @@ -48,7 +48,7 @@ public ResponseEntity getTemporaryAnswer(@PathVaria @GetMapping("/analyze") public ResponseEntity getAnalyzeAnswer(@PathVariable("spaceId") Long spaceId, @PathVariable("retrospectId") Long retrospectId, @MemberId Long memberId) { - AnswerListGetResponse response = answerService.getAnalyzeAnswer(spaceId, retrospectId, memberId); + AnswerListGetResponse response = answerService.getAnalyzeAnswers(spaceId, retrospectId, memberId); return ResponseEntity.ok().body(response); } diff --git a/layer-api/src/main/java/org/layer/domain/answer/service/AnswerService.java b/layer-api/src/main/java/org/layer/domain/answer/service/AnswerService.java index 5e0e5f2e..ee84172d 100644 --- a/layer-api/src/main/java/org/layer/domain/answer/service/AnswerService.java +++ b/layer-api/src/main/java/org/layer/domain/answer/service/AnswerService.java @@ -184,7 +184,7 @@ public TemporaryAnswerListResponse getTemporaryAnswer(Long spaceId, Long retrosp return TemporaryAnswerListResponse.of(temporaryAnswers); } - public AnswerListGetResponse getAnalyzeAnswer(Long spaceId, Long retrospectId, Long memberId) { + public AnswerListGetResponse getAnalyzeAnswers(Long spaceId, Long retrospectId, Long memberId) { // 해당 스페이스 팀원인지 검증 Team team = new Team(memberSpaceRelationRepository.findAllBySpaceId(spaceId)); team.validateTeamMembership(memberId); @@ -192,7 +192,6 @@ public AnswerListGetResponse getAnalyzeAnswer(Long spaceId, Long retrospectId, L // 완료된 answer 뽑기 Answers answers = new Answers( answerRepository.findAllByRetrospectIdAndAnswerStatus(retrospectId, AnswerStatus.DONE)); - answers.validateIsWriteDone(memberId, retrospectId); List questionIds = answers.getAnswers().stream().map(Answer::getQuestionId).toList(); List memberIds = answers.getAnswers().stream().map(Answer::getMemberId).toList(); diff --git a/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java b/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java index b12eeb55..3d28afb4 100644 --- a/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java +++ b/layer-api/src/main/java/org/layer/domain/auth/service/AuthService.java @@ -14,9 +14,7 @@ import org.layer.domain.member.service.MemberService; import org.layer.external.discord.event.SignUpEvent; import org.layer.oauth.dto.service.MemberInfoServiceResponse; -import org.layer.oauth.service.GoogleService; -import org.layer.oauth.service.KakaoService; -import org.layer.oauth.service.apple.AppleService; +import org.layer.oauth.service.OAuthService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,9 +24,9 @@ @RequiredArgsConstructor @Service public class AuthService { - private final KakaoService kakaoService; - private final GoogleService googleService; - private final AppleService appleService; + private final OAuthService kakaoService; + private final OAuthService googleService; + private final OAuthService appleService; private final JwtService jwtService; private final MemberService memberService; diff --git a/layer-api/src/main/java/org/layer/domain/jwt/service/JwtService.java b/layer-api/src/main/java/org/layer/domain/jwt/service/JwtService.java index 66a5cca0..ff6d07be 100644 --- a/layer-api/src/main/java/org/layer/domain/jwt/service/JwtService.java +++ b/layer-api/src/main/java/org/layer/domain/jwt/service/JwtService.java @@ -27,7 +27,7 @@ public class JwtService { private final JwtProvider jwtProvider; private final JwtValidator jwtValidator; - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; public JwtToken issueToken(Long memberId, MemberRole memberRole) { String accessToken = jwtProvider.createToken(MemberAuthentication.create(memberId, memberRole), ACCESS_TOKEN_EXPIRATION_TIME); diff --git a/layer-api/src/main/resources/application-test.yml b/layer-api/src/main/resources/application-test.yml index ce3122f4..d28a3b11 100644 --- a/layer-api/src/main/resources/application-test.yml +++ b/layer-api/src/main/resources/application-test.yml @@ -19,6 +19,9 @@ spring: format_sql: true show_sql: true open-in-view: false + sql: + init: + mode: never data: redis: @@ -84,4 +87,10 @@ openai: maxTokens: ${OPENAI_MAX_TOKENS} admin: - password: ${ADMIN_PASSWORD} \ No newline at end of file + password: ${ADMIN_PASSWORD} + +discord: + webhook: + retrospect-url: ${DISCORD_RETROSPECT_URL} + space-url: ${DISCORD_SPACE_URL} + member-url: ${DISCORD_MEMBER_URL} \ No newline at end of file diff --git a/layer-api/src/main/resources/console-appender.xml b/layer-api/src/main/resources/console-appender.xml index f22ddee8..8f23ce17 100644 --- a/layer-api/src/main/resources/console-appender.xml +++ b/layer-api/src/main/resources/console-appender.xml @@ -5,22 +5,4 @@ ${LOG_PATTERN} - - - ./log/layer-api.log - - - ${application.home:-.}/log/layer-api.%d{yyyy-MM-dd}.%i.log - - - 50MB - - 10 - - - - %-4relative [%thread] %-5level %logger{35} - %msg%n - - diff --git a/layer-api/src/main/resources/logback-spring.xml b/layer-api/src/main/resources/logback-spring.xml index f9ec6edb..4152e0ef 100644 --- a/layer-api/src/main/resources/logback-spring.xml +++ b/layer-api/src/main/resources/logback-spring.xml @@ -16,7 +16,6 @@ - @@ -25,7 +24,15 @@ - + + + + + + + + + diff --git a/layer-api/src/test/java/org/layer/LayerApplicationTests.java b/layer-api/src/test/java/org/layer/LayerApplicationTests.java index fa718484..46d15763 100644 --- a/layer-api/src/test/java/org/layer/LayerApplicationTests.java +++ b/layer-api/src/test/java/org/layer/LayerApplicationTests.java @@ -6,9 +6,9 @@ @SpringBootTest @ActiveProfiles("test") -public class LayerApplicationTests { - // @Test - // void init() { - // - // } +class LayerApplicationTests { + @Test + void init() { + + } } diff --git a/layer-api/src/test/java/org/layer/domain/answer/service/AnswerServiceTest.java b/layer-api/src/test/java/org/layer/domain/answer/service/AnswerServiceTest.java new file mode 100644 index 00000000..a6f04d25 --- /dev/null +++ b/layer-api/src/test/java/org/layer/domain/answer/service/AnswerServiceTest.java @@ -0,0 +1,58 @@ +package org.layer.domain.answer.service; + +import static org.assertj.core.api.Assertions.*; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.layer.domain.answer.controller.dto.response.AnswerListGetResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class AnswerServiceTest { + + @Autowired + private AnswerService answerService; + + @Nested + @SqlGroup({ + @Sql(value = "/sql/answer-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-test-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + + }) + class 회고_답변_조회 { + + @Test + @DisplayName("특정 회고에 답변하지 않은 유저도 완료된 해당 회고를 조회할 수 있다.") + void getAnalyzeAnswersTest_1() { + // given + Long spaceId = 1L; + Long retrospectId = 1L; + Long memberId = 2L; + + // when + AnswerListGetResponse analyzeAnswers = answerService.getAnalyzeAnswers(spaceId, retrospectId, memberId); + + // then + assertThat(analyzeAnswers).isNotNull(); + assertThat(analyzeAnswers.questions()).hasSize(2); + assertThat(analyzeAnswers.questions().get(0).questionContent()).isEqualTo("질문1"); + assertThat(analyzeAnswers.questions().get(0).answers().get(0).answerContent()).isEqualTo("회고답변 1"); + assertThat(analyzeAnswers.questions().get(1).answers().get(0).answerContent()).isEqualTo("회고답변 2"); + + assertThat(analyzeAnswers.individuals()).hasSize(1); + assertThat(analyzeAnswers.individuals().get(0).name()).isEqualTo("홍길동"); + assertThat(analyzeAnswers.individuals().get(0).answers().get(0).answerContent()).isEqualTo("회고답변 1"); + assertThat(analyzeAnswers.individuals().get(0).answers().get(1).answerContent()).isEqualTo("회고답변 2"); + } + } + +} diff --git a/layer-api/src/test/java/org/layer/domain/retrospect/service/RetrospectServiceTest.java b/layer-api/src/test/java/org/layer/domain/retrospect/service/RetrospectServiceTest.java index 30cca50c..c0c4334d 100644 --- a/layer-api/src/test/java/org/layer/domain/retrospect/service/RetrospectServiceTest.java +++ b/layer-api/src/test/java/org/layer/domain/retrospect/service/RetrospectServiceTest.java @@ -1,8 +1,27 @@ package org.layer.domain.retrospect.service; +import static org.layer.domain.space.entity.SpaceField.*; +import java.time.LocalDateTime; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.layer.domain.member.entity.Member; +import org.layer.domain.member.entity.MemberRole; +import org.layer.domain.member.entity.SocialType; import org.layer.domain.member.repository.MemberRepository; +import org.layer.domain.question.enums.QuestionType; +import org.layer.domain.retrospect.controller.dto.request.QuestionCreateRequest; +import org.layer.domain.retrospect.controller.dto.request.RetrospectCreateRequest; +import org.layer.domain.retrospect.entity.Retrospect; +import org.layer.domain.retrospect.entity.RetrospectStatus; import org.layer.domain.retrospect.repository.RetrospectRepository; +import org.layer.domain.space.entity.MemberSpaceRelation; +import org.layer.domain.space.entity.Space; +import org.layer.domain.space.entity.SpaceCategory; import org.layer.domain.space.repository.MemberSpaceRelationRepository; import org.layer.domain.space.repository.SpaceRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -11,53 +30,59 @@ @SpringBootTest @ActiveProfiles("test") -public class RetrospectServiceTest { - - @Autowired - private RetrospectService retrospectService; - - @Autowired - private RetrospectRepository retrospectRepository; - - @Autowired - private MemberRepository memberRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private MemberSpaceRelationRepository memberSpaceRelationRepository; -// -// @Test -// void 정상입력의_경우_회고생성에_성공한다() { -// // given -// // TODO : 이 부분은 이렇게 처리할지 or data.sql로 처리할지 고민. -// Member member = Member.builder() -// .name("홍길동") -// .email("qwer@naver.com") -// .memberRole(MemberRole.USER) -// .socialType(SocialType.GOOGLE) -// .socialId("123456789") -// .build(); -// Member member1 = memberRepository.saveAndFlush(member); -// -// Space space = new Space(null, SpaceCategory.TEAM, List.of(DEVELOPMENT), "회고스페이스이름입니다", "회고스페이스 설명입니다", 1L, 1L); -// spaceRepository.saveAndFlush(space); -// memberSpaceRelationRepository.saveAndFlush(new MemberSpaceRelation(member1.getId(), space)); -// -// RetrospectCreateRequest request = new RetrospectCreateRequest("회고제목입니다", "회고소개입니다", -// List.of(new QuestionCreateRequest("질문1", QuestionType.PLAIN_TEXT.getStyle()), -// new QuestionCreateRequest("질문2", QuestionType.PLAIN_TEXT.getStyle()), -// new QuestionCreateRequest("질문3", QuestionType.PLAIN_TEXT.getStyle())), -// LocalDateTime.of(2024, 8, 4, 3, 5), -// false, null, null, null); -// // when -// Long savedId = retrospectService.createRetrospect(request, 1L, 1L); -// -// // then -// Retrospect retrospect = retrospectRepository.findByIdOrThrow(savedId); -// Assertions.assertThat(retrospect) -// .extracting("title", "introduction", "retrospectStatus") -// .containsExactly("회고제목입니다", "회고소개입니다", RetrospectStatus.PROCEEDING); -// } +class RetrospectServiceTest { + + @Autowired + private RetrospectService retrospectService; + + @Autowired + private RetrospectRepository retrospectRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private MemberSpaceRelationRepository memberSpaceRelationRepository; + + @Nested + class 회고_생성 { + @Test + @DisplayName("정상 입력의 경우 회고 생성에 성공한다.") + void createRetrospectTest_1() { + // given + Member member = Member.builder() + .name("홍길동") + .email("qwer@naver.com") + .memberRole(MemberRole.USER) + .socialType(SocialType.GOOGLE) + .socialId("123456789") + .build(); + Member member1 = memberRepository.saveAndFlush(member); + + Space space = new Space("banner-url", SpaceCategory.TEAM, List.of(DEVELOPMENT), "회고스페이스 이름", "회고스페이스 설명", + 1L, 1L); + spaceRepository.saveAndFlush(space); + memberSpaceRelationRepository.saveAndFlush(new MemberSpaceRelation(member1.getId(), space)); + + RetrospectCreateRequest request = new RetrospectCreateRequest("회고제목입니다", "회고소개입니다", + List.of(new QuestionCreateRequest("질문1", QuestionType.PLAIN_TEXT.getStyle()), + new QuestionCreateRequest("질문2", QuestionType.PLAIN_TEXT.getStyle()), + new QuestionCreateRequest("질문3", QuestionType.PLAIN_TEXT.getStyle())), + LocalDateTime.of(2024, 8, 4, 3, 5), + false, null, null, null, false); + + // when + Long savedId = retrospectService.createRetrospect(request, 1L, 1L); + + // then + Retrospect retrospect = retrospectRepository.findByIdOrThrow(savedId); + Assertions.assertThat(retrospect) + .extracting("title", "introduction", "retrospectStatus") + .containsExactly("회고제목입니다", "회고소개입니다", RetrospectStatus.PROCEEDING); + } + } + } diff --git a/layer-api/src/test/resources/sql/answer-service-test-data.sql b/layer-api/src/test/resources/sql/answer-service-test-data.sql new file mode 100644 index 00000000..31432989 --- /dev/null +++ b/layer-api/src/test/resources/sql/answer-service-test-data.sql @@ -0,0 +1,25 @@ +INSERT INTO member (id, created_at, updated_at, email, member_role, name, profile_image_url, social_id, social_type, deleted_at) +VALUES + (1, '2024-12-27 00:00:00', '2024-12-27 00:00:00', 'user1@example.com', 'USER', '홍길동', 'https://example.com/image1.png', 'social_id_1', 'KAKAO', NULL), + (2, '2024-12-27 00:00:00', '2024-12-27 00:00:00', 'user2@example.com', 'USER', '김철수', 'https://example.com/image2.png', 'social_id_2', 'KAKAO', NULL); + +INSERT INTO space (created_at, updated_at, banner_url, category, field_list, form_id, introduction, leader_id, name) +VALUES + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', 'https://example.com/banner1.jpg', 'INDIVIDUAL', 'EDUCATION,DEVELOPMENT', NULL, '개인 프로젝트를 위한 공간입니다.', 1, '개인 공간 1'); + +INSERT INTO member_space_relation (created_at, updated_at, member_id, space_id) +VALUES + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', 1, 1), + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', 2, 1); + +INSERT INTO question (created_at, updated_at, content, form_id, question_order, question_owner, question_type, retrospect_id) +VALUES + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', '질문1', 1, 1, 'INDIVIDUAL', 'PLAIN_TEXT', 1), + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', '질문2', 1, 2, 'TEAM', 'PLAIN_TEXT', 1); + +INSERT INTO answer (created_at, updated_at, answer_status, content, member_id, question_id, retrospect_id) +VALUES + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', 'DONE', '회고답변 1', 1, 1, 1), + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', 'TEMPORARY', '회고임시답변', 1, 2, 1), + ('2024-12-27 00:00:00', '2024-12-27 00:00:00', 'DONE', '회고답변 2', 1, 2, 1); + diff --git a/layer-api/src/test/resources/sql/delete-all-test-data.sql b/layer-api/src/test/resources/sql/delete-all-test-data.sql new file mode 100644 index 00000000..70ce4c91 --- /dev/null +++ b/layer-api/src/test/resources/sql/delete-all-test-data.sql @@ -0,0 +1,38 @@ +-- 모든 테이블의 데이터 삭제 (외래키 제약 조건 무시) +SET FOREIGN_KEY_CHECKS = 0; + +DELETE FROM action_item; +DELETE FROM `analyze`; +DELETE FROM analyze_detail; +DELETE FROM answer; +DELETE FROM form; +DELETE FROM member; +DELETE FROM question; +DELETE FROM question_description; +DELETE FROM question_option; +DELETE FROM retrospect; +DELETE FROM space; +DELETE FROM member_space_relation; +DELETE FROM template_metadata; +DELETE FROM template_purpose; + +-- AUTO_INCREMENT 초기화 +ALTER TABLE action_item AUTO_INCREMENT = 1; +ALTER TABLE `analyze` AUTO_INCREMENT = 1; +ALTER TABLE analyze_detail AUTO_INCREMENT = 1; +ALTER TABLE answer AUTO_INCREMENT = 1; +ALTER TABLE form AUTO_INCREMENT = 1; +ALTER TABLE member AUTO_INCREMENT = 1; +ALTER TABLE question AUTO_INCREMENT = 1; +ALTER TABLE question_description AUTO_INCREMENT = 1; +ALTER TABLE question_option AUTO_INCREMENT = 1; +ALTER TABLE retrospect AUTO_INCREMENT = 1; +ALTER TABLE space AUTO_INCREMENT = 1; +ALTER TABLE member_space_relation AUTO_INCREMENT = 1; +ALTER TABLE template_metadata AUTO_INCREMENT = 1; +ALTER TABLE template_purpose AUTO_INCREMENT = 1; + +-- 외래키 제약 조건 복구 +SET FOREIGN_KEY_CHECKS = 1; + +-- 테이블 초기화 완료 \ No newline at end of file diff --git a/layer-common/src/main/java/org/layer/common/dto/RecentActivityDto.java b/layer-common/src/main/java/org/layer/common/dto/RecentActivityDto.java new file mode 100644 index 00000000..9491e4ae --- /dev/null +++ b/layer-common/src/main/java/org/layer/common/dto/RecentActivityDto.java @@ -0,0 +1,19 @@ +package org.layer.common.dto; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@Getter +public class RecentActivityDto { + private final LocalDateTime recentActivityDate; + + @JsonCreator + public RecentActivityDto(LocalDateTime recentActivityDate) { + this.recentActivityDate = recentActivityDate; + } +} diff --git a/layer-common/src/main/java/org/layer/common/exception/AdminExceptionType.java b/layer-common/src/main/java/org/layer/common/exception/AdminExceptionType.java new file mode 100644 index 00000000..2a04b377 --- /dev/null +++ b/layer-common/src/main/java/org/layer/common/exception/AdminExceptionType.java @@ -0,0 +1,23 @@ +package org.layer.common.exception; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum AdminExceptionType implements ExceptionType{ + IllegalDateTime(HttpStatus.INTERNAL_SERVER_ERROR, "유저의 최근 접속 시간이 올바르지 않습니다."); + + + private final HttpStatus status; + private final String message; + + @Override + public HttpStatus httpStatus() { + return status; + } + + @Override + public String message() { + return message; + } +} \ No newline at end of file diff --git a/layer-domain/src/main/java/org/layer/domain/answer/entity/Answers.java b/layer-domain/src/main/java/org/layer/domain/answer/entity/Answers.java index 1f801ecd..c7d20288 100644 --- a/layer-domain/src/main/java/org/layer/domain/answer/entity/Answers.java +++ b/layer-domain/src/main/java/org/layer/domain/answer/entity/Answers.java @@ -42,14 +42,6 @@ public boolean hasRetrospectAnswer(Long memberId, Long retrospectId) { .anyMatch(answer -> answer.getMemberId().equals(memberId)); } - public void validateIsWriteDone(Long memberId, Long retrospectId) { - WriteStatus writeStatus = getWriteStatus(memberId, retrospectId); - - if (!writeStatus.equals(WriteStatus.DONE)) { - throw new AnswerException(NOT_ANSWERED); - } - } - public WriteStatus getWriteStatus(Long memberId, Long retrospectId) { boolean isDoneWrite = answers.stream() .filter(answer -> answer.getRetrospectId().equals(retrospectId)) diff --git a/layer-domain/src/test/java/layer/domain/retrospect/entity/RetrospectTest.java b/layer-domain/src/test/java/layer/domain/retrospect/entity/RetrospectTest.java index 9ed01529..5599be54 100644 --- a/layer-domain/src/test/java/layer/domain/retrospect/entity/RetrospectTest.java +++ b/layer-domain/src/test/java/layer/domain/retrospect/entity/RetrospectTest.java @@ -8,7 +8,7 @@ import org.layer.domain.retrospect.entity.Retrospect; import org.layer.domain.retrospect.entity.RetrospectStatus; -public class RetrospectTest { +class RetrospectTest { @Test void 진행중인_회고는_진행여부로직에서_예외를_발생시키지_않는다() { diff --git a/layer-external/src/main/java/org/layer/oauth/service/OAuthService.java b/layer-external/src/main/java/org/layer/oauth/service/OAuthService.java index afc1c9f5..17bc7bb5 100644 --- a/layer-external/src/main/java/org/layer/oauth/service/OAuthService.java +++ b/layer-external/src/main/java/org/layer/oauth/service/OAuthService.java @@ -4,5 +4,4 @@ public interface OAuthService { MemberInfoServiceResponse getMemberInfo(final String accessToken); - }