Skip to content

Commit

Permalink
�feat: admin 모듈 개발 및 회원 목록 조회 구현 (#266)
Browse files Browse the repository at this point in the history
* add: 어드민 모듈 추가

* add: 어드민 모듈 추가

* add: 어드민 모듈 docker 파일 추가

* feat: 어드민 관련 레포지토리 메서드 도메인 내에 구현

* feat: 회원 활동 목록 조회 기능 구현

* add: admin 스프링 서버 환경 세팅

* add: 모든 요청 허용하는 시큐리티 설정 추가

* add: dev yml 추가

* chore: admin 모듈 ci-cd 추가

* chore: admin 관련 컨테이너 및 프록시 추가

* style: 공백 제거

* chore: page 1부터 시작하도록 수정

* chore: 페이지네이션 default value 설정
  • Loading branch information
mikekks authored Dec 2, 2024
1 parent d7f1ec3 commit e912fb0
Show file tree
Hide file tree
Showing 21 changed files with 363 additions and 3 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/aws-cicd-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- develop
- feat/LA-20

env:
REGISTRY: "docker.io"
Expand Down Expand Up @@ -56,6 +57,7 @@ jobs:
run: |
echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-api/src/main/resources/application-secret.properties
echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-batch/src/main/resources/application-secret.properties
echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-admin/src/main/resources/application-secret.properties
- name: Build layer-api module
run: ./gradlew :layer-api:build
Expand All @@ -69,6 +71,12 @@ jobs:
- name: Test layer-batch module
run: ./gradlew :layer-batch:test

- name: Build layer-admin module
run: ./gradlew :layer-admin:build

- name: Test layer-admin module
run: ./gradlew :layer-admin:test

- name: Docker Hub Login
uses: docker/login-action@v1
with:
Expand All @@ -82,6 +90,7 @@ jobs:
images: |
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-api
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-admin
- name: Push layer-api Docker Image
uses: docker/build-push-action@v4
Expand All @@ -104,6 +113,16 @@ jobs:
tags: |
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch:latest
- name: Push layer-admin Docker Image
uses: docker/build-push-action@v4
with:
context: ./layer-admin
file: ./layer-admin/Dockerfile-admin # Dockerfile 이름 지정
platforms: linux/amd64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-admin:latest
deploy:
name: Deploy
needs: [ build, setup ]
Expand All @@ -119,6 +138,7 @@ jobs:
run: |
echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-api/infra/${{ env.DEPLOY_TARGET }}/application-secret.properties
echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-batch/src/main/resources/application-secret.properties
echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-admin/src/main/resources/application-secret.properties
- name: Archive Files
run: |
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/aws-cicd-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
run: |
echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-api/src/main/resources/application-secret.properties
echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-batch/src/main/resources/application-secret.properties
echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-admin/src/main/resources/application-secret.properties
- name: Build layer-api module
run: ./gradlew :layer-api:build
Expand All @@ -70,6 +71,12 @@ jobs:
- name: Test layer-batch module
run: ./gradlew :layer-batch:test

- name: Build layer-admin module
run: ./gradlew :layer-admin:build

- name: Test layer-admin module
run: ./gradlew :layer-admin:test

- name: Docker Hub Login
uses: docker/login-action@v1
with:
Expand All @@ -83,6 +90,7 @@ jobs:
images: |
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-api
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-admin
- name: Push layer-api Docker Image
uses: docker/build-push-action@v4
Expand All @@ -106,6 +114,16 @@ jobs:
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-batch:latest
no-cache: true

- name: Push layer-admin Docker Image
uses: docker/build-push-action@v4
with:
context: ./layer-admin
file: ./layer-admin/Dockerfile-admin # Dockerfile 이름 지정
platforms: linux/amd64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}_layer-admin:latest
deploy:
name: Deploy
needs: [ build, setup ]
Expand All @@ -121,6 +139,7 @@ jobs:
run: |
echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-api/infra/${{ env.DEPLOY_TARGET }}/application-secret.properties
echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-batch/src/main/resources/application-secret.properties
echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./layer-admin/src/main/resources/application-secret.properties
- name: Archive Files
run: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ credentials.json


layer-api/src/main/resources/tokens/StoredCredential
layer-batch/src/main/resources/application-secret.properties
layer-batch/src/main/resources/application-secret.properties
layer-admin/src/main/resources/application-secret.properties
22 changes: 20 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ project(":layer-api") {
implementation project(path: ':layer-domain')
implementation project(path: ':layer-external')


implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-validation'

Expand All @@ -82,7 +81,6 @@ project(":layer-api") {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
testImplementation 'org.springframework.boot:spring-boot-starter-test'


// openfeign
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2")

Expand Down Expand Up @@ -195,4 +193,24 @@ project(":layer-batch") {
runtimeOnly 'com.mysql:mysql-connector-j'
}

}

project(":layer-admin") {
jar.enabled = false
bootJar.enabled = true

dependencies {
implementation project(path: ':layer-domain')

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'

// swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
}

}
Binary file added config/tokens/StoredCredential.txt
Binary file not shown.
10 changes: 10 additions & 0 deletions layer-admin/Dockerfile-admin
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM openjdk:17

ARG JAR_FILE=./build/libs/*.jar
ARG SPRING_PROFILE

COPY ${JAR_FILE} layer-admin.jar

ENV SPRING_PROFILE=${SPRING_PROFILE}

ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul" ,"-jar" ,"layer-admin.jar"]
14 changes: 14 additions & 0 deletions layer-admin/src/main/java/org/layer/AdminApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.layer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}

}
23 changes: 23 additions & 0 deletions layer-admin/src/main/java/org/layer/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.layer.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 모든 요청 허용
)
.httpBasic(AbstractHttpConfigurer::disable); // HTTP Basic 인증 비활성화

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.layer.member.controller;

import org.layer.member.controller.dto.GetMembersActivitiesResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;

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.tags.Tag;

@Tag(name = "[ADMIN] 회원 서비스", description = "회원 관련 api")
public interface AdminMemberApi {

@Operation(summary = "회원 활동 목록 조회")
@Parameters({
@Parameter(name = "password", description = "패스워드", example = "abcdef", required = true),
@Parameter(name = "page", description = "페이지 수, 최솟값 1", example = "1", required = true),
@Parameter(name = "take", description = "가져올 데이터 수", example = "20", required = true)
})
ResponseEntity<GetMembersActivitiesResponse> getMemberActivities(
@RequestParam String password,
@RequestParam int page,
@RequestParam int take);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.layer.member.controller;

import org.layer.member.controller.dto.GetMembersActivitiesResponse;
import org.layer.member.service.AdminMemberService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RequestMapping("/admin/members")
@RequiredArgsConstructor
@RestController
public class AdminMemberController implements AdminMemberApi {
private final AdminMemberService adminMemberService;

@Override
@GetMapping
public ResponseEntity<GetMembersActivitiesResponse> getMemberActivities(
@RequestParam String password,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int take) {

return ResponseEntity.ok().body(adminMemberService.getMemberActivities(password, page, take));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.layer.member.controller.dto;

import java.time.LocalDateTime;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

@Schema(name = "GetMemberActivityResponse", description = "회원 활동 Dto")
public record GetMemberActivityResponse(
@NotNull
@Schema(description = "회원 이름", example = "홍길동")
String name,
@NotNull
@Schema(description = "최근 활동 날짜", example = "2024-11-30T16:21:47.031Z")
LocalDateTime recentActivityDate,
@NotNull
@Schema(description = "소속된 스페이스 수", example = "7")
long spaceCount,
@NotNull
@Schema(description = "작성한 회고 수", example = "15")
long retrospectAnswerCount,
@NotNull
@Schema(description = "회원가입 날짜", example = "2024-10-30T16:21:47.031Z")
LocalDateTime signUpDate,
@NotNull
@Schema(description = "회원가입 플랫폼", example = "KAKAO")
String socialType
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.layer.member.controller.dto;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

@Schema(name = "GetMembersActivitiesResponse", description = "회원 활동 목록 Dto")
public record GetMembersActivitiesResponse(
@NotNull
@Schema(description = "회원 활동 목록", example = "")
List<GetMemberActivityResponse> responses

) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.layer.member.service;

import java.util.List;

import org.layer.domain.answer.repository.AdminAnswerRepository;
import org.layer.domain.member.entity.Member;
import org.layer.domain.member.repository.AdminMemberRepository;
import org.layer.domain.space.repository.AdminMemberSpaceRelationRepository;
import org.layer.member.controller.dto.GetMemberActivityResponse;
import org.layer.member.controller.dto.GetMembersActivitiesResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AdminMemberService {
private final AdminMemberRepository adminMemberRepository;
private final AdminMemberSpaceRelationRepository adminMemberSpaceRelationRepository;
private final AdminAnswerRepository adminAnswerRepository;

@Value("${admin.password}")
private String password;

public GetMembersActivitiesResponse getMemberActivities(String password, int page, int take) {

// TODO: 검증 로직 필터단으로 옮기기
if (!password.equals(this.password)) {
throw new IllegalArgumentException("비밀번호가 올바르지 않습니다.");
}

PageRequest pageRequest = PageRequest.of(page - 1, take);
Page<Member> members = adminMemberRepository.findAll(pageRequest);

List<GetMemberActivityResponse> responses = members.getContent().stream()
.map(member -> {

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());
}).toList();

return new GetMembersActivitiesResponse(responses);
}
}
23 changes: 23 additions & 0 deletions layer-admin/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
server:
port: 3000

spring:
config:
import: application-secret.properties
datasource:
url: ${AWS_DEV_DB_URL}
username: ${AWS_PROD_DB_NAME}
password: ${AWS_PROD_DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
show_sql: true
open-in-view: false
database: mysql

admin:
password: ${ADMIN_PASSWORD}
Loading

0 comments on commit e912fb0

Please sign in to comment.