Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : Members 탭 설계 및 페이징을 이용한 필터링 기능 구현(SJB-STEP3) #4

Open
wants to merge 5 commits into
base: develop-SJB
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
Expand Down
35 changes: 21 additions & 14 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.11'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id 'java'
id 'org.springframework.boot' version '2.7.11'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id 'jacoco'
}

group = 'org.poolc'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-collections4:4.4'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.apache.commons:commons-collections4:4.4'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
29 changes: 29 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# PoolC-Spring-Practice

풀씨 백엔드와 유사하지만, 일부 기능들을 개선한 서버를 만들어봅시다. 물론 풀씨 페이지는 규모가 상당히 크기에, 모든 컴포넌트를 만들 수 없으니 기본적인 것들만 구현해 봅시다.

진행 방식
---
- Spring 프로그래밍 연습 스터디는, 하나의 큰 과제를 수행해야 합니다.
- Java 스터디에 비해 자유도가 조금 높으며, 요구 사항이 다소 추상적입니다.
- 직접적인 main 브랜치로의 커밋은 금지 되며, 반드시 Step 수행 이후 Pull Request 요청을 통해 확인이 진행됩니다.
- 본인의 닉네임/이름에 해당하는 브랜치를 만들고, 각 Step 에 대한 브랜치를 만들어서 PR을 진행해 주세요.
- ex) KBC 브랜치를 만들고, Step 1에 대한 결과물은 KBC-step1 로 만들어 주세요.
- 그 이후, PR은 KBC-step1 -> KBC 꼴로 요청해 주세요.
- 각 커밋의 단위는 최소화 해야하며, 다음과 같은 커밋 메시지 양식을 준수해 주세요.
- https://vsfe.notion.site/Git-Convention-84e1df4868974a58a1609b052e815095

요구 사항 (공통)
---
- 해당 과제는 여러 Step으로 구성되어 있으며, 앞 Step에 대한 PR 및 리뷰가 완료 되어야 뒤 Step을 진행할 수 있습니다.
- 포함된 라이브러리는 기본적인 라이브러리만 포함되어 있으며, 필요에 따라 추가 라이브러리를 사용해도 됩니다.
- 모든 Java 코드는 반드시 Java 코드 컨벤션 가이드를 준수해야 합니다.
- 작성한 메서드에 대한 테스트 코드 작성이 진행되어야 합니다.
- Jacoco 기준, Test Coverage 및 Branch Coverage가 80% 이상이어야 합니다.
- 통합 테스트/단위 테스트 여부는 자유롭게 설정하셔도 됩니다.
- 하지만, 통합 테스트 수행 시, 실제 DB에 전혀 영향이 가지 않아야 합니다.
- 사용하는 DB는 제한이 없습니다.

요구 사항 (단계)
---
- 특정 Step 을 마치지 못했다면, 그 다음 Step의 요구 사항을 보지 않는 것을 권장합니다. https://vsfe.notion.site/Spring-PoolC-Backend-Reborn-281c69c2eaf543459fedace987868ea4
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.poolc.springpractice;
package org.poolc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/org/poolc/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.poolc.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.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@EnableWebSecurity
public class SecurityConfig {


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


http
.csrf().disable() // post 방식으로 값을 전송할 때 token을 사용해야하는 보안 설정을 해제
.authorizeRequests()
.antMatchers("/", "/members/**", "/login", "/loginedMembers/**", "/role", "/admin/**").permitAll()
// .antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
return http.build();
}
}
37 changes: 37 additions & 0 deletions src/main/java/org/poolc/config/SpringConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.poolc.config;


import org.poolc.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.Filter;


@Configuration
public class SpringConfig {

@PersistenceContext
private EntityManager em;

@Bean
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}


@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");

return filterRegistrationBean;
}
}
33 changes: 33 additions & 0 deletions src/main/java/org/poolc/controller/HomeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.poolc.controller;

import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.poolc.controller.session.SessionConst;
import org.poolc.controller.session.SessionManager;
import org.poolc.domain.Member;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttribute;

@Controller
@RequiredArgsConstructor
public class HomeController {

private final SessionManager sessionManager;

@GetMapping("/")
public String homeLoginV2(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model) {

//로그인 여부에 따라 유저에게 다른 홈화면 제공
if (Optional.ofNullable(loginMember).isEmpty()) {
return "home";
}

//세션이 유지되면 회원 홈(로그인 모드 홈)으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
}
73 changes: 73 additions & 0 deletions src/main/java/org/poolc/controller/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.poolc.controller;

import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.poolc.controller.form.LoginFormController;
import org.poolc.controller.session.SessionConst;
import org.poolc.domain.Member;
import org.poolc.service.LoginService;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;


@Controller
@RequiredArgsConstructor
public class LoginController {

private final LoginService loginService;

// 로그인 페이지로 이동
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginFormController form) {
return "members/loginMemberForm";
}

@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginForm") LoginFormController form,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
// valid한 입력이 아닐경우(채우지 않은 칸이 존재), 로그인 화면으로 다시 redirection
if (bindingResult.hasErrors()) {
return "members/loginMemberForm";
}
Optional<Member> loginMember = loginService.login(form.getLoginId(), form.getPassword());
// 가입하지 않은 아이디일 경우, 로그인 화면으로 다시 redirection
if (loginMember.isEmpty()) {
return "members/loginMemberForm";
}
//로그인 성공 -> 세션이 있으면 해당 세션 반환, 없으면 신규세션 생성(default=true) false면 세션 없으면 null을 반환
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관.
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember.get());

return "redirect:" + redirectURL;
}

// 로그아웃 page로 이동 -> "/logout"시 query parameter(/login?logout)로 redirect 문제 차후 (Get, POST)분리
@RequestMapping(value = "/loginedMembers/logout", method = {RequestMethod.GET,
RequestMethod.POST})
public String logout(HttpServletRequest request) {
//로그아웃시 해당 세션 삭제 후 홈으로 이동
sessionDelete(request);
return "redirect:/";
}

public void sessionDelete(HttpServletRequest request) {
Optional<HttpSession> session = Optional.ofNullable(request.getSession(false));
if (!session.isEmpty()) {
session.get().invalidate();
}
}


}
125 changes: 125 additions & 0 deletions src/main/java/org/poolc/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.poolc.controller;

import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.poolc.controller.session.SessionConst;
import org.poolc.domain.MEMBER_ROLE;
import org.poolc.domain.Member;
import org.poolc.service.MemberService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;

@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {

private final MemberService memberService;

//회원 가입 페이지
@GetMapping("/new")
public String createForm(@ModelAttribute("member") Member member) {
return "members/createMemberForm";
}

@PostMapping("/new")
public String create(@Valid @ModelAttribute("member") Member member,
BindingResult bindingResult) {

// 회원가입 정보가 valid하지않은 경우(채우지 않은 칸 존재) -> 다시 회원 가입 창으로 redirection
if (bindingResult.hasErrors()) {
return "members/createMemberForm";
}
//기존에 있는 회원 아이디와 동이한지 체크
if (memberService.findByUserId(member.getUserId()).isPresent()) {
return "members/createMemberForm";
}
// 신규회원의 등급은 브론즈로 설정
member.setRole(MEMBER_ROLE.ROLE_BRONZE);
memberService.join(member);

return "redirect:/";
}


// 회원들 리스트 나열
@GetMapping("/list")
public String list(@PageableDefault Pageable pageable, Model model,
@RequestParam(value = "queryFilteredRole", defaultValue = "all") String queryFilteredRole) {

log.debug("filteredRole = " + queryFilteredRole);
Page<Member> members = filterDistributor(queryFilteredRole, pageable);
//지정된 등급외의 잘못된 필터링용 input이 들어왔는지 validation (SRP인가...?)
// 홈 화면으로 다시 리다이렉션 -> 혹시나 필터에서 걸리지 않은 비로그인 사용자 방지
if (members.isEmpty()) {
log.debug("잘못된 queryFilteredRole 쿼리스트링 값입니다.");
return "redirect:/";
}
model.addAttribute("role_filter", queryFilteredRole);
model.addAttribute("members", members);

return "members/memberList";
}

// 회원들 필터링된 리스트 나열
@PostMapping("/list")
public String listAfterFilter(@PageableDefault Pageable pageable, Model model,
@RequestParam("role_filter") String role_filter) {
return "redirect:/members/list?queryFilteredRole=" + role_filter;
}

// 회원정보 수정 창
@GetMapping("/update")
public String UpdateForm(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)
Member loginMember, Model model) {
//회원 업데이트 화면으로 이동
model.addAttribute("member", loginMember);
return "members/updateMemberForm";
}


@PostMapping("/update")
public String update(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
@Valid @ModelAttribute("member") Member member, BindingResult bindingResult) {

if (bindingResult.hasErrors()) {
return "members/updateMemberForm";
}
memberService.update(loginMember.getId(), member);

return "redirect:/loginedMembers/logout";
}

private Page<Member> filterDistributor(String inputRoleFilter, Pageable pageable) {
switch (inputRoleFilter) {
case "all":
return memberService.findAllPageById(pageable);
case "admin":
return memberService.findMemberPageByRole(MEMBER_ROLE.ROLE_ADMIN, pageable);
case "gold":
return memberService.findMemberPageByRole(MEMBER_ROLE.ROLE_GOLD, pageable);
case "silver":
return memberService.findMemberPageByRole(MEMBER_ROLE.ROLE_SILVER, pageable);
case "bronze":
return memberService.findMemberPageByRole(MEMBER_ROLE.ROLE_BRONZE, pageable);
default:
//올바르지 않은 role 감지시 Validation 필요. -> all 변환.
log.debug("올바르지 않은 role 입력입니다.");
return Page.empty();
}
}

}
Loading