From a7cd823588ae6ec2722ecc9541fd30cd2b40ac7f Mon Sep 17 00:00:00 2001 From: gutanbug Date: Sun, 29 Jan 2023 22:45:34 +0900 Subject: [PATCH 01/12] =?UTF-8?q?JWT=EC=99=80=20Security=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EA=B3=BC=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore.swp | Bin 0 -> 12288 bytes build.gradle | 19 ++-- settings.gradle | 2 +- .../cha/carrotApi/CarrotApiApplication.java | 15 ++++ .../controller/MemberController.java | 31 +++++++ .../cha/carrotApi/domain/BaseTimeEntity.java | 22 +++++ .../java/com/cha/carrotApi/domain/Member.java | 85 ++++++++++++++++++ .../java/com/cha/carrotApi/domain/Role.java | 5 ++ .../jwt_security/JwtAuthenticationFilter.java | 33 +++++++ .../jwt_security/JwtTokenProvider.java | 75 ++++++++++++++++ .../jwt_security/SecurityConfig.java | 58 ++++++++++++ .../carrotApi/jwt_security/SecurityUtil.java | 11 +++ .../repository/MemberRepository.java | 10 +++ .../repository/MemberSignUpRequestDto.java | 50 +++++++++++ .../service/CustomUserDetailsService.java | 19 ++++ .../cha/carrotApi/service/MemberService.java | 11 +++ .../carrotApi/service/MemberServiceImpl.java | 57 ++++++++++++ src/main/resources/application.yml | 21 +++++ .../carrotApi/CarrotApiApplicationTests.java | 13 +++ 19 files changed, 530 insertions(+), 7 deletions(-) create mode 100644 .gitignore.swp create mode 100644 src/main/java/com/cha/carrotApi/CarrotApiApplication.java create mode 100644 src/main/java/com/cha/carrotApi/controller/MemberController.java create mode 100644 src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Member.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Role.java create mode 100644 src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java create mode 100644 src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java create mode 100644 src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java create mode 100644 src/main/java/com/cha/carrotApi/repository/MemberRepository.java create mode 100644 src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java create mode 100644 src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java create mode 100644 src/main/java/com/cha/carrotApi/service/MemberService.java create mode 100644 src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java diff --git a/.gitignore.swp b/.gitignore.swp new file mode 100644 index 0000000000000000000000000000000000000000..5380ce24c63fa2446710aa03302936f464c9d614 GIT binary patch literal 12288 zcmeI1F^dyH7>2(nq6nv=ogi$CHj&Jp@xW`(BuLDi26A_WC^EY{x3_V#JIqY(P(d3j zTS5H`TKE^N{0CZCSqXxLSlRk!vuAM{EUt=t3(w4aGrM2jeYz#V;%0ZvTNX{ib&BZ1 z@sHtFxzM$LSd57+=3IDmn%+iEN?Y+PwxUYvBPov(UlPH!BbAwS9n*Z><~18jf|umLu} z2H3#78F0EnPZ0W08T?{7SN6?i$rBr318jf|umLu}2G{@_U;}J`4X^<=FoyV3M!MIERioG8<~18m?wF(7tq zm`BQ|TCL`74!jl$4{IV{WI-b~GD>mkXF-%$hebJMK`>6zs5sFkAF9y#+9W&DmA!5% zcTLI6PMcNhb>EuMcM46g&d2Drw>gqY_ItB9sf$D#!Z=DNIy9em^NB3g07=(3psS(O>TamCxoyA2|6v{bN__^|(NNmZ_<>rBwZkt4yN^7S} zsolL;Wy;`yAbwzJw7qlkV1 D7(2Vz literal 0 HcmV?d00001 diff --git a/build.gradle b/build.gradle index 15b77ef..132cb17 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.0.1' - id 'io.spring.dependency-management' version '1.1.0' + id 'org.springframework.boot' version '2.7.8' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' } -group = 'com.dku' +group = 'com.cha' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' +sourceCompatibility = '11' configurations { compileOnly { @@ -21,9 +21,16 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + testImplementation 'junit:junit:4.13.1' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/settings.gradle b/settings.gradle index 73e37fd..9d00a1e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'springstudy' +rootProject.name = 'carrotApi' diff --git a/src/main/java/com/cha/carrotApi/CarrotApiApplication.java b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java new file mode 100644 index 0000000..40f4c17 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class CarrotApiApplication { + + public static void main(String[] args) { + SpringApplication.run(CarrotApiApplication.class, args); + } + +} diff --git a/src/main/java/com/cha/carrotApi/controller/MemberController.java b/src/main/java/com/cha/carrotApi/controller/MemberController.java new file mode 100644 index 0000000..7fae985 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/MemberController.java @@ -0,0 +1,31 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.repository.MemberRepository; +import com.cha.carrotApi.repository.MemberSignUpRequestDto; +import com.cha.carrotApi.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Map; + +@RestController +@RequestMapping("/member") +@RequiredArgsConstructor +public class MemberController { + + private MemberService memberService; + private MemberRepository memberRepository; + + @PostMapping("/join") + @ResponseStatus(HttpStatus.OK) + public Long join(@Valid @RequestBody MemberSignUpRequestDto request) throws Exception { + return memberService.signUp(request); + } + + @PostMapping("/login") + public String login(@RequestBody Map member) { + return memberService.login(member); + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java b/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java new file mode 100644 index 0000000..86fbda5 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java @@ -0,0 +1,22 @@ +package com.cha.carrotApi.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; +} diff --git a/src/main/java/com/cha/carrotApi/domain/Member.java b/src/main/java/com/cha/carrotApi/domain/Member.java new file mode 100644 index 0000000..0d469f8 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Member.java @@ -0,0 +1,85 @@ +package com.cha.carrotApi.domain; + +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.persistence.*; +import java.util.Collection; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Entity +public class Member extends BaseTimeEntity implements UserDetails { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_id") + private Long id; + + @Column(length = 45, unique = true) + private String email; + + @Column(length = 45) + private String name; + + @Column(length = 45) + private String nickname; + + @Column(length = 45) + private String phone_number; + + private int age; + + @Column(length = 100) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + + public void encodePassword(PasswordEncoder passwordEncoder){ + this.password = passwordEncoder.encode(password); + } + + public void addUserAuthority() { + this.role = Role.USER; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getUsername() { + return null; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } + + public boolean checkPassword(PasswordEncoder passwordEncoder, String password) { + if (passwordEncoder.equals(password)) { + return true; + } + return false; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Role.java b/src/main/java/com/cha/carrotApi/domain/Role.java new file mode 100644 index 0000000..d09ca3b --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Role.java @@ -0,0 +1,5 @@ +package com.cha.carrotApi.domain; + +public enum Role { + USER, MANAGER, ADMIN; +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..56dd5cc --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java @@ -0,0 +1,33 @@ +package com.cha.carrotApi.jwt_security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); + + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java new file mode 100644 index 0000000..41bd725 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java @@ -0,0 +1,75 @@ +package com.cha.carrotApi.jwt_security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class JwtTokenProvider { + private String secretKey = + "c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK"; + + // 토큰 유효시간 168 시간(7일) + private long tokenValidTime = 1440 * 60 * 7 * 1000L; + private final UserDetailsService userDetailsService; + + // 객체 초기화, secretKey 를 Base64로 인코딩합니다. + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + // JWT 토큰 생성 + public String createToken(String userPk, List roles) { + Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위 + claims.put("roles", roles); // 정보는 key/value 쌍으로 저장됩니다. + Date now = new Date(); + return Jwts.builder() + .setClaims(claims) // 정보 저장 + .setIssuedAt(now) // 토큰 발행 시간 정보 + .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time + .signWith(SignatureAlgorithm.HS256, secretKey) // 사용할 암호화 알고리즘 + // signature 에 들어갈 secret 값 세팅 + .compact(); + } + + // JWT 토큰에서 인증 정보 조회 + public Authentication getAuthentication(String token) { + UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + // 토큰에서 회원 정보 추출 + public String getUserPk(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + // Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값' + public String resolveToken(HttpServletRequest request) { + return request.getHeader("X-AUTH-TOKEN"); + } + + // 토큰의 유효성 + 만료일자 확인 + public boolean validateToken(String jwtToken) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken); + return !claims.getBody().getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java new file mode 100644 index 0000000..e433d33 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.cha.carrotApi.jwt_security; + +import lombok.RequiredArgsConstructor; +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.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .formLogin().disable() + .httpBasic().disable() + .cors().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/member/login").permitAll() + .antMatchers("/member/join").permitAll() + .antMatchers("/member").hasRole("USER") + .anyRequest().authenticated(); + } + + @Bean + public PasswordEncoder passwordEncoder(){ + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + private static final String[] AUTH_WHITELIST = { + "/v2/api-docs", + "/v3/api-docs/**", + "/configuration/ui", + "/swagger-resources/**", + "/configuration/security", + "/swagger-ui.html", + "/webjars/**", + "/file/**", + "/image/**", + "/swagger/**", + "/swagger-ui/**", + "/h2/**" + }; + + // 정적인 파일 요청에 대해 무시 + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers(AUTH_WHITELIST); + } +} \ No newline at end of file diff --git a/src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java b/src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java new file mode 100644 index 0000000..064fe16 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java @@ -0,0 +1,11 @@ +package com.cha.carrotApi.jwt_security; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public class SecurityUtil { + public static String getLoginUsername() { + UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + return user.getUsername(); + } +} diff --git a/src/main/java/com/cha/carrotApi/repository/MemberRepository.java b/src/main/java/com/cha/carrotApi/repository/MemberRepository.java new file mode 100644 index 0000000..c3674e6 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.repository; + +import com.cha.carrotApi.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java b/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java new file mode 100644 index 0000000..da1d208 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java @@ -0,0 +1,50 @@ +package com.cha.carrotApi.repository; + +import com.cha.carrotApi.domain.Member; +import com.cha.carrotApi.domain.Role; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +@Data +@Builder +@AllArgsConstructor +public class MemberSignUpRequestDto { + + @NotBlank(message = "아이디를 입력해주세요") + private String email; + + @NotBlank(message = "닉네임을 입력해주세요") + @Size(min=2, message = "닉네임이 너무 짧습니다.") + private String nickname; + + @NotNull(message = "나이를 입력해주세요") + @Range(min=0, max = 150) + private int age; + + @NotBlank(message = "비밀번호를 입력해주세요") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,30}$", + message = "비밀번호는 8~30 자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.") + private String password; + + private String checkPassword; + + private Role role; + + @Builder + public Member toEntity() { + return Member.builder() + .email(email) + .nickname(nickname) + .age(age) + .password(password) + .role(Role.USER) + .build(); + } +} diff --git a/src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java b/src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java new file mode 100644 index 0000000..0c27033 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java @@ -0,0 +1,19 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return memberRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/com/cha/carrotApi/service/MemberService.java b/src/main/java/com/cha/carrotApi/service/MemberService.java new file mode 100644 index 0000000..55455a4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/MemberService.java @@ -0,0 +1,11 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.repository.MemberSignUpRequestDto; + +import java.util.Map; + +public interface MemberService { + public Long signUp(MemberSignUpRequestDto memberSignUpRequestDto) throws Exception; + + public String login(Map members); +} diff --git a/src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java b/src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java new file mode 100644 index 0000000..4adbab5 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java @@ -0,0 +1,57 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.domain.Member; +import com.cha.carrotApi.jwt_security.JwtTokenProvider; +import com.cha.carrotApi.repository.MemberRepository; +import com.cha.carrotApi.repository.MemberSignUpRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberServiceImpl implements MemberService{ + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + @Override + public Long signUp(MemberSignUpRequestDto requestDto) throws Exception { + + if(memberRepository.findByEmail(requestDto.getEmail()).isPresent()){ + throw new Exception("이미 존재하는 이메일입니다."); + } + + if(!requestDto.getPassword().equals(requestDto.getCheckPassword())){ + throw new Exception("비밀번호가 일치하지 않습니다."); + } + + Member member = memberRepository.save(requestDto.toEntity()); + member.encodePassword(passwordEncoder); + + member.addUserAuthority(); + return member.getId(); + } + + @Override + public String login(Map members) { + Member member = memberRepository.findByEmail(members.get("email")) + .orElseThrow(() -> new IllegalStateException("가입되지 않은 Email 입니다.")); + String password = members.get("password"); + if (!member.checkPassword(passwordEncoder, password)) { + throw new IllegalStateException("비밀번호가 일치하지 않습니다."); + } + List roles = new ArrayList<>(); + roles.add(member.getRole().name()); + + return jwtTokenProvider.createToken(member.getUsername(), roles); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d54a9ba --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + datasource: + url: jdbc:h2:tcp://localhost/~/carrotapi + username: sa + password: + driver-class-name: org.h2.Driver + security: + jwt: + header: Authorization + secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK + token-validity-in-seconds: 86400 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + # show_sql: true + format_sql: true +logging.level: + org.hibernate.SQL: debug + org.hibernate.type: trace \ No newline at end of file diff --git a/src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java b/src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java new file mode 100644 index 0000000..c6e2a31 --- /dev/null +++ b/src/test/java/com/cha/carrotApi/CarrotApiApplicationTests.java @@ -0,0 +1,13 @@ +package com.cha.carrotApi; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CarrotApiApplicationTests { + + @Test + void contextLoads() { + } + +} From 4090790e82dc47796c7c8171034bc113f217d947 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Wed, 1 Feb 2023 06:04:28 +0900 Subject: [PATCH 02/12] =?UTF-8?q?2023-02-01=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +- .../controller/MemberController.java | 4 +- .../cha/carrotApi/domain/BaseTimeEntity.java | 3 + .../java/com/cha/carrotApi/domain/Member.java | 72 +++++-------------- .../CustomUserDetailsService.java | 8 ++- .../jwt_security/JwtAuthenticationFilter.java | 1 + .../jwt_security/SecurityConfig.java | 66 +++++++---------- .../carrotApi/jwt_security/SecurityUtil.java | 11 --- .../repository/MemberRepository.java | 2 + .../repository/MemberSignUpRequestDto.java | 4 +- .../cha/carrotApi/service/MemberService.java | 48 ++++++++++++- .../carrotApi/service/MemberServiceImpl.java | 57 --------------- .../springstudy/SpringStudyApplication.java | 13 ---- src/main/resources/application.properties | 1 - src/main/resources/application.yml | 2 +- .../SpringStudyApplicationTests.java | 13 ---- 16 files changed, 107 insertions(+), 204 deletions(-) rename src/main/java/com/cha/carrotApi/{service => jwt_security}/CustomUserDetailsService.java (73%) delete mode 100644 src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java delete mode 100644 src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java delete mode 100644 src/main/java/com/dku/springstudy/SpringStudyApplication.java delete mode 100644 src/main/resources/application.properties delete mode 100644 src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java diff --git a/build.gradle b/build.gradle index 132cb17..ac72340 100644 --- a/build.gradle +++ b/build.gradle @@ -15,17 +15,17 @@ configurations { } repositories { - mavenCentral() + mavenCentral(); } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-websocket' - implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' implementation 'io.jsonwebtoken:jjwt:0.9.1' testImplementation 'junit:junit:4.13.1' diff --git a/src/main/java/com/cha/carrotApi/controller/MemberController.java b/src/main/java/com/cha/carrotApi/controller/MemberController.java index 7fae985..fe2c9df 100644 --- a/src/main/java/com/cha/carrotApi/controller/MemberController.java +++ b/src/main/java/com/cha/carrotApi/controller/MemberController.java @@ -15,8 +15,8 @@ @RequiredArgsConstructor public class MemberController { - private MemberService memberService; - private MemberRepository memberRepository; + private final MemberService memberService; + private final MemberRepository memberRepository; @PostMapping("/join") @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java b/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java index 86fbda5..190e868 100644 --- a/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java +++ b/src/main/java/com/cha/carrotApi/domain/BaseTimeEntity.java @@ -5,6 +5,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import javax.persistence.Column; import javax.persistence.EntityListeners; import javax.persistence.MappedSuperclass; import java.time.LocalDateTime; @@ -14,9 +15,11 @@ @EntityListeners(AuditingEntityListener.class) public class BaseTimeEntity { + @Column(name = "CREATEDATE") @CreatedDate private LocalDateTime createDate; + @Column(name = "MODIFIEDDATE") @LastModifiedDate private LocalDateTime modifiedDate; } diff --git a/src/main/java/com/cha/carrotApi/domain/Member.java b/src/main/java/com/cha/carrotApi/domain/Member.java index 0d469f8..5b4df4e 100644 --- a/src/main/java/com/cha/carrotApi/domain/Member.java +++ b/src/main/java/com/cha/carrotApi/domain/Member.java @@ -1,85 +1,47 @@ package com.cha.carrotApi.domain; import lombok.*; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import javax.persistence.*; -import java.util.Collection; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @Entity -public class Member extends BaseTimeEntity implements UserDetails { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "member_id") +@Table(name = "MEMBERS") +public class Member extends BaseTimeEntity { + @Id + @Column(name = "MEMBER_ID") + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(length = 45, unique = true) + @Column(name = "EMAIL", length = 45, unique = true) private String email; - - @Column(length = 45) - private String name; - @Column(length = 45) + @Column(name = "NICKNAME", length = 45) private String nickname; - @Column(length = 45) - private String phone_number; - - private int age; - - @Column(length = 100) + @Column(name = "PASSWORD", length = 100) private String password; + @Column(name = "ROLE") @Enumerated(EnumType.STRING) private Role role; - public void encodePassword(PasswordEncoder passwordEncoder){ + @Builder + private Member(String email, String nickname, String password) { + this.email = email; + this.nickname = nickname; + this.password = password; + this.role = Role.USER; + } + public void encodePassword(PasswordEncoder passwordEncoder) { this.password = passwordEncoder.encode(password); } public void addUserAuthority() { this.role = Role.USER; } - - @Override - public Collection getAuthorities() { - return null; - } - - @Override - public String getUsername() { - return null; - } - - @Override - public boolean isAccountNonExpired() { - return false; - } - - @Override - public boolean isAccountNonLocked() { - return false; - } - - @Override - public boolean isCredentialsNonExpired() { - return false; - } - - @Override - public boolean isEnabled() { - return false; - } - - public boolean checkPassword(PasswordEncoder passwordEncoder, String password) { - if (passwordEncoder.equals(password)) { - return true; - } - return false; - } } diff --git a/src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java similarity index 73% rename from src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java rename to src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java index 0c27033..8b269cc 100644 --- a/src/main/java/com/cha/carrotApi/service/CustomUserDetailsService.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java @@ -1,19 +1,23 @@ -package com.cha.carrotApi.service; +package com.cha.carrotApi.jwt_security; import com.cha.carrotApi.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor +@Service +@Transactional public class CustomUserDetailsService implements UserDetailsService { private final MemberRepository memberRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return memberRepository.findByEmail(username) + return (UserDetails) memberRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); } } diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java index 56dd5cc..aa68b4c 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.OncePerRequestFilter; diff --git a/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java index e433d33..83bc6d4 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java @@ -3,56 +3,42 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -@Configuration +@EnableWebSecurity @RequiredArgsConstructor -public class SecurityConfig extends WebSecurityConfigurerAdapter { +@Configuration +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .formLogin().disable() - .httpBasic().disable() - .cors().disable() - .csrf().disable() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() - .authorizeRequests() - .antMatchers("/member/login").permitAll() - .antMatchers("/member/join").permitAll() - .antMatchers("/member").hasRole("USER") - .anyRequest().authenticated(); + // 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다. + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); } @Bean - public PasswordEncoder passwordEncoder(){ - return PasswordEncoderFactories.createDelegatingPasswordEncoder(); - } + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - private static final String[] AUTH_WHITELIST = { - "/v2/api-docs", - "/v3/api-docs/**", - "/configuration/ui", - "/swagger-resources/**", - "/configuration/security", - "/swagger-ui.html", - "/webjars/**", - "/file/**", - "/image/**", - "/swagger/**", - "/swagger-ui/**", - "/h2/**" - }; + http.httpBasic().disable() // rest api 만을 고려하여 기본 설정은 해제하겠습니다. + .csrf().disable() // csrf 보안 토큰 disable처리. + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 기반 인증이므로 세션 역시 사용하지 않습니다. + .and() + .authorizeHttpRequests() // 요청에 대한 사용권한 체크 + .anyRequest().permitAll() // 그외 나머지 요청은 누구나 접근 가능 + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class); - // 정적인 파일 요청에 대해 무시 - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers(AUTH_WHITELIST); + return http.build(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java b/src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java deleted file mode 100644 index 064fe16..0000000 --- a/src/main/java/com/cha/carrotApi/jwt_security/SecurityUtil.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.cha.carrotApi.jwt_security; - -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; - -public class SecurityUtil { - public static String getLoginUsername() { - UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - return user.getUsername(); - } -} diff --git a/src/main/java/com/cha/carrotApi/repository/MemberRepository.java b/src/main/java/com/cha/carrotApi/repository/MemberRepository.java index c3674e6..92db7f7 100644 --- a/src/main/java/com/cha/carrotApi/repository/MemberRepository.java +++ b/src/main/java/com/cha/carrotApi/repository/MemberRepository.java @@ -2,9 +2,11 @@ import com.cha.carrotApi.domain.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; +@Repository public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); } diff --git a/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java b/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java index da1d208..ac52886 100644 --- a/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java +++ b/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java @@ -17,6 +17,7 @@ @AllArgsConstructor public class MemberSignUpRequestDto { + protected MemberSignUpRequestDto(){} @NotBlank(message = "아이디를 입력해주세요") private String email; @@ -33,8 +34,6 @@ public class MemberSignUpRequestDto { message = "비밀번호는 8~30 자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.") private String password; - private String checkPassword; - private Role role; @Builder @@ -42,7 +41,6 @@ public Member toEntity() { return Member.builder() .email(email) .nickname(nickname) - .age(age) .password(password) .role(Role.USER) .build(); diff --git a/src/main/java/com/cha/carrotApi/service/MemberService.java b/src/main/java/com/cha/carrotApi/service/MemberService.java index 55455a4..1a28787 100644 --- a/src/main/java/com/cha/carrotApi/service/MemberService.java +++ b/src/main/java/com/cha/carrotApi/service/MemberService.java @@ -1,11 +1,53 @@ package com.cha.carrotApi.service; +import com.cha.carrotApi.domain.Member; +import com.cha.carrotApi.jwt_security.JwtTokenProvider; +import com.cha.carrotApi.repository.MemberRepository; import com.cha.carrotApi.repository.MemberSignUpRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -public interface MemberService { - public Long signUp(MemberSignUpRequestDto memberSignUpRequestDto) throws Exception; +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberService{ - public String login(Map members); + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public Long signUp(MemberSignUpRequestDto requestDto) throws Exception { + + if(memberRepository.findByEmail(requestDto.getEmail()).isPresent()){ + throw new Exception("이미 존재하는 이메일입니다."); + } + + Member member = memberRepository.save(requestDto.toEntity()); + member.encodePassword(passwordEncoder); + + member.addUserAuthority(); + return member.getId(); + } + + + public String login(Map members) { + Member member = memberRepository.findByEmail(members.get("email")) + .orElseThrow(() -> new IllegalStateException("가입되지 않은 Email 입니다.")); + String password = members.get("password"); + if (!passwordEncoder.matches(password,member.getPassword())) { + throw new IllegalStateException("비밀번호가 일치하지 않습니다."); + } + + List roles = new ArrayList<>(); + roles.add(member.getRole().name()); + + return jwtTokenProvider.createToken(member.getEmail(), roles); + } } diff --git a/src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java b/src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java deleted file mode 100644 index 4adbab5..0000000 --- a/src/main/java/com/cha/carrotApi/service/MemberServiceImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.cha.carrotApi.service; - -import com.cha.carrotApi.domain.Member; -import com.cha.carrotApi.jwt_security.JwtTokenProvider; -import com.cha.carrotApi.repository.MemberRepository; -import com.cha.carrotApi.repository.MemberSignUpRequestDto; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class MemberServiceImpl implements MemberService{ - - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - private final JwtTokenProvider jwtTokenProvider; - - @Transactional - @Override - public Long signUp(MemberSignUpRequestDto requestDto) throws Exception { - - if(memberRepository.findByEmail(requestDto.getEmail()).isPresent()){ - throw new Exception("이미 존재하는 이메일입니다."); - } - - if(!requestDto.getPassword().equals(requestDto.getCheckPassword())){ - throw new Exception("비밀번호가 일치하지 않습니다."); - } - - Member member = memberRepository.save(requestDto.toEntity()); - member.encodePassword(passwordEncoder); - - member.addUserAuthority(); - return member.getId(); - } - - @Override - public String login(Map members) { - Member member = memberRepository.findByEmail(members.get("email")) - .orElseThrow(() -> new IllegalStateException("가입되지 않은 Email 입니다.")); - String password = members.get("password"); - if (!member.checkPassword(passwordEncoder, password)) { - throw new IllegalStateException("비밀번호가 일치하지 않습니다."); - } - List roles = new ArrayList<>(); - roles.add(member.getRole().name()); - - return jwtTokenProvider.createToken(member.getUsername(), roles); - } -} diff --git a/src/main/java/com/dku/springstudy/SpringStudyApplication.java b/src/main/java/com/dku/springstudy/SpringStudyApplication.java deleted file mode 100644 index ef164c9..0000000 --- a/src/main/java/com/dku/springstudy/SpringStudyApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dku.springstudy; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SpringStudyApplication { - - public static void main(String[] args) { - SpringApplication.run(SpringStudyApplication.class, args); - } - -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d54a9ba..23eb0d0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,7 +11,7 @@ spring: token-validity-in-seconds: 86400 jpa: hibernate: - ddl-auto: update + ddl-auto: create properties: hibernate: # show_sql: true diff --git a/src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java b/src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java deleted file mode 100644 index 79d9975..0000000 --- a/src/test/java/com/dku/springstudy/SpringStudyApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dku.springstudy; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringStudyApplicationTests { - - @Test - void contextLoads() { - } - -} From 2ee687706f3f1dbf2b297d5b90d18d7194414224 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Thu, 2 Feb 2023 05:06:34 +0900 Subject: [PATCH 03/12] =?UTF-8?q?2=EC=A3=BC=EC=B0=A8=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=ED=9B=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../cha/carrotApi/CarrotApiApplication.java | 2 - .../controller/MemberController.java | 31 ----------- .../carrotApi/controller/UserController.java | 31 +++++++++++ .../domain/{Member.java => User.java} | 9 ++-- .../CustomUserDetailsService.java | 6 +-- .../jwt_security/JwtAuthenticationFilter.java | 2 + .../jwt_security/JwtTokenProvider.java | 18 ++++--- ...berRepository.java => UserRepository.java} | 6 +-- ...uestDto.java => UserSignUpRequestDto.java} | 10 ++-- .../cha/carrotApi/service/MemberService.java | 53 ------------------ .../cha/carrotApi/service/UserService.java | 54 +++++++++++++++++++ .../resources/application-API-KEY.properties | 1 + src/main/resources/application.yml | 2 + 14 files changed, 118 insertions(+), 109 deletions(-) delete mode 100644 src/main/java/com/cha/carrotApi/controller/MemberController.java create mode 100644 src/main/java/com/cha/carrotApi/controller/UserController.java rename src/main/java/com/cha/carrotApi/domain/{Member.java => User.java} (85%) rename src/main/java/com/cha/carrotApi/repository/{MemberRepository.java => UserRepository.java} (54%) rename src/main/java/com/cha/carrotApi/repository/{MemberSignUpRequestDto.java => UserSignUpRequestDto.java} (87%) delete mode 100644 src/main/java/com/cha/carrotApi/service/MemberService.java create mode 100644 src/main/java/com/cha/carrotApi/service/UserService.java create mode 100644 src/main/resources/application-API-KEY.properties diff --git a/.gitignore b/.gitignore index c2065bc..1e97e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +**/src/main/resources/*.properties +**/src/main/resources/*.yml ### STS ### .apt_generated diff --git a/src/main/java/com/cha/carrotApi/CarrotApiApplication.java b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java index 40f4c17..dc1dd44 100644 --- a/src/main/java/com/cha/carrotApi/CarrotApiApplication.java +++ b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java @@ -7,9 +7,7 @@ @EnableJpaAuditing @SpringBootApplication public class CarrotApiApplication { - public static void main(String[] args) { SpringApplication.run(CarrotApiApplication.class, args); } - } diff --git a/src/main/java/com/cha/carrotApi/controller/MemberController.java b/src/main/java/com/cha/carrotApi/controller/MemberController.java deleted file mode 100644 index fe2c9df..0000000 --- a/src/main/java/com/cha/carrotApi/controller/MemberController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.cha.carrotApi.controller; - -import com.cha.carrotApi.repository.MemberRepository; -import com.cha.carrotApi.repository.MemberSignUpRequestDto; -import com.cha.carrotApi.service.MemberService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import javax.validation.Valid; -import java.util.Map; - -@RestController -@RequestMapping("/member") -@RequiredArgsConstructor -public class MemberController { - - private final MemberService memberService; - private final MemberRepository memberRepository; - - @PostMapping("/join") - @ResponseStatus(HttpStatus.OK) - public Long join(@Valid @RequestBody MemberSignUpRequestDto request) throws Exception { - return memberService.signUp(request); - } - - @PostMapping("/login") - public String login(@RequestBody Map member) { - return memberService.login(member); - } -} diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java new file mode 100644 index 0000000..f16494b --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -0,0 +1,31 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.repository.UserSignUpRequestDto; +import com.cha.carrotApi.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.Map; + +@RestController +@RequestMapping("/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + //회원가입 + @PostMapping("/join") + @ResponseStatus(HttpStatus.OK) + public Long join(@Valid @RequestBody UserSignUpRequestDto request) throws Exception { + return userService.signUp(request); + } + + //로그인 + @PostMapping("/login") + public String login(@RequestBody Map user) { + return userService.login(user); + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Member.java b/src/main/java/com/cha/carrotApi/domain/User.java similarity index 85% rename from src/main/java/com/cha/carrotApi/domain/Member.java rename to src/main/java/com/cha/carrotApi/domain/User.java index 5b4df4e..b31108c 100644 --- a/src/main/java/com/cha/carrotApi/domain/Member.java +++ b/src/main/java/com/cha/carrotApi/domain/User.java @@ -6,14 +6,15 @@ import javax.persistence.*; @Getter +@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder @Entity -@Table(name = "MEMBERS") -public class Member extends BaseTimeEntity { +@Table(name = "USERS") +public class User extends BaseTimeEntity { @Id - @Column(name = "MEMBER_ID") + @Column(name = "USER_ID") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -31,7 +32,7 @@ public class Member extends BaseTimeEntity { private Role role; @Builder - private Member(String email, String nickname, String password) { + private User(String email, String nickname, String password) { this.email = email; this.nickname = nickname; this.password = password; diff --git a/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java index 8b269cc..84981fc 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.jwt_security; -import com.cha.carrotApi.repository.MemberRepository; +import com.cha.carrotApi.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -13,11 +13,11 @@ @Transactional public class CustomUserDetailsService implements UserDetailsService { - private final MemberRepository memberRepository; + private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return (UserDetails) memberRepository.findByEmail(username) + return (UserDetails) userRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); } } diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java index aa68b4c..f4c63b8 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.cha.carrotApi.jwt_security; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -15,6 +16,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; +@Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends GenericFilterBean { diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java index 41bd725..28a3cbb 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtTokenProvider.java @@ -5,10 +5,11 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; @@ -19,13 +20,14 @@ @RequiredArgsConstructor @Component +@Slf4j public class JwtTokenProvider { - private String secretKey = - "c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK"; + @Value("${secret-key}") + private String secretKey; // 토큰 유효시간 168 시간(7일) private long tokenValidTime = 1440 * 60 * 7 * 1000L; - private final UserDetailsService userDetailsService; + private final CustomUserDetailsService customUserDetailsService; // 객체 초기화, secretKey 를 Base64로 인코딩합니다. @PostConstruct @@ -34,8 +36,8 @@ protected void init() { } // JWT 토큰 생성 - public String createToken(String userPk, List roles) { - Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위 + public String createToken(String userEmail, List roles) { + Claims claims = Jwts.claims().setSubject(userEmail); // JWT payload 에 저장되는 정보단위 claims.put("roles", roles); // 정보는 key/value 쌍으로 저장됩니다. Date now = new Date(); return Jwts.builder() @@ -49,12 +51,12 @@ public String createToken(String userPk, List roles) { // JWT 토큰에서 인증 정보 조회 public Authentication getAuthentication(String token) { - UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token)); + UserDetails userDetails = customUserDetailsService.loadUserByUsername(this.getUserEmail(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } // 토큰에서 회원 정보 추출 - public String getUserPk(String token) { + public String getUserEmail(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); } diff --git a/src/main/java/com/cha/carrotApi/repository/MemberRepository.java b/src/main/java/com/cha/carrotApi/repository/UserRepository.java similarity index 54% rename from src/main/java/com/cha/carrotApi/repository/MemberRepository.java rename to src/main/java/com/cha/carrotApi/repository/UserRepository.java index 92db7f7..3a12bde 100644 --- a/src/main/java/com/cha/carrotApi/repository/MemberRepository.java +++ b/src/main/java/com/cha/carrotApi/repository/UserRepository.java @@ -1,12 +1,12 @@ package com.cha.carrotApi.repository; -import com.cha.carrotApi.domain.Member; +import com.cha.carrotApi.domain.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository -public interface MemberRepository extends JpaRepository { - Optional findByEmail(String email); +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); } diff --git a/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java b/src/main/java/com/cha/carrotApi/repository/UserSignUpRequestDto.java similarity index 87% rename from src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java rename to src/main/java/com/cha/carrotApi/repository/UserSignUpRequestDto.java index ac52886..7ce2f48 100644 --- a/src/main/java/com/cha/carrotApi/repository/MemberSignUpRequestDto.java +++ b/src/main/java/com/cha/carrotApi/repository/UserSignUpRequestDto.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.repository; -import com.cha.carrotApi.domain.Member; +import com.cha.carrotApi.domain.User; import com.cha.carrotApi.domain.Role; import lombok.AllArgsConstructor; import lombok.Builder; @@ -15,9 +15,9 @@ @Data @Builder @AllArgsConstructor -public class MemberSignUpRequestDto { +public class UserSignUpRequestDto { - protected MemberSignUpRequestDto(){} + protected UserSignUpRequestDto(){} @NotBlank(message = "아이디를 입력해주세요") private String email; @@ -37,8 +37,8 @@ protected MemberSignUpRequestDto(){} private Role role; @Builder - public Member toEntity() { - return Member.builder() + public User toEntity() { + return User.builder() .email(email) .nickname(nickname) .password(password) diff --git a/src/main/java/com/cha/carrotApi/service/MemberService.java b/src/main/java/com/cha/carrotApi/service/MemberService.java deleted file mode 100644 index 1a28787..0000000 --- a/src/main/java/com/cha/carrotApi/service/MemberService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.cha.carrotApi.service; - -import com.cha.carrotApi.domain.Member; -import com.cha.carrotApi.jwt_security.JwtTokenProvider; -import com.cha.carrotApi.repository.MemberRepository; -import com.cha.carrotApi.repository.MemberSignUpRequestDto; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class MemberService{ - - private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; - private final JwtTokenProvider jwtTokenProvider; - - @Transactional - public Long signUp(MemberSignUpRequestDto requestDto) throws Exception { - - if(memberRepository.findByEmail(requestDto.getEmail()).isPresent()){ - throw new Exception("이미 존재하는 이메일입니다."); - } - - Member member = memberRepository.save(requestDto.toEntity()); - member.encodePassword(passwordEncoder); - - member.addUserAuthority(); - return member.getId(); - } - - - public String login(Map members) { - Member member = memberRepository.findByEmail(members.get("email")) - .orElseThrow(() -> new IllegalStateException("가입되지 않은 Email 입니다.")); - String password = members.get("password"); - if (!passwordEncoder.matches(password,member.getPassword())) { - throw new IllegalStateException("비밀번호가 일치하지 않습니다."); - } - - List roles = new ArrayList<>(); - roles.add(member.getRole().name()); - - return jwtTokenProvider.createToken(member.getEmail(), roles); - } -} diff --git a/src/main/java/com/cha/carrotApi/service/UserService.java b/src/main/java/com/cha/carrotApi/service/UserService.java new file mode 100644 index 0000000..44c76f9 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/UserService.java @@ -0,0 +1,54 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.domain.User; +import com.cha.carrotApi.jwt_security.JwtTokenProvider; +import com.cha.carrotApi.repository.UserRepository; +import com.cha.carrotApi.repository.UserSignUpRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtTokenProvider jwtTokenProvider; + + @Transactional + public Long signUp(UserSignUpRequestDto requestDto) throws Exception { + + if(userRepository.findByEmail(requestDto.getEmail()).isPresent()){ + throw new Exception("이미 존재하는 이메일입니다."); + } + + User user = userRepository.save(requestDto.toEntity()); + user.encodePassword(passwordEncoder); + + user.addUserAuthority(); + return user.getId(); + } + + + // 위에랑 마찬가지로 변경 + public String login(Map users) { + User user = userRepository.findByEmail(users.get("email")) + .orElseThrow(() -> new IllegalStateException("가입되지 않은 Email 입니다.")); + String password = users.get("password"); + if (!passwordEncoder.matches(password,user.getPassword())) { + throw new IllegalStateException("비밀번호가 일치하지 않습니다."); + } + + List roles = new ArrayList<>(); + roles.add(user.getRole().name()); + + return jwtTokenProvider.createToken(user.getEmail(), roles); + } +} diff --git a/src/main/resources/application-API-KEY.properties b/src/main/resources/application-API-KEY.properties new file mode 100644 index 0000000..e57b090 --- /dev/null +++ b/src/main/resources/application-API-KEY.properties @@ -0,0 +1 @@ +secret-key = c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 23eb0d0..5897fbf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,8 @@ spring: username: sa password: driver-class-name: org.h2.Driver + profiles: + include: API-KEY security: jwt: header: Authorization From 45236848bd9708c0f68878b7f4e121dafc04ab43 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Fri, 3 Feb 2023 12:36:41 +0900 Subject: [PATCH 04/12] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20User=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cha/carrotApi/ERD/carrotERD.png | Bin 0 -> 55887 bytes .../com/cha/carrotApi/domain/Category.java | 25 +++++++++ .../java/com/cha/carrotApi/domain/Post.java | 49 ++++++++++++++++++ .../com/cha/carrotApi/domain/PostStatus.java | 5 ++ .../java/com/cha/carrotApi/domain/User.java | 21 ++++++-- 5 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/cha/carrotApi/ERD/carrotERD.png create mode 100644 src/main/java/com/cha/carrotApi/domain/Category.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Post.java create mode 100644 src/main/java/com/cha/carrotApi/domain/PostStatus.java diff --git a/src/main/java/com/cha/carrotApi/ERD/carrotERD.png b/src/main/java/com/cha/carrotApi/ERD/carrotERD.png new file mode 100644 index 0000000000000000000000000000000000000000..167b646ba61472994d27db87dc0d7a03cc6fc837 GIT binary patch literal 55887 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 s0@FZkaj>8_XxtnusH#K_ category_posts = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post.java b/src/main/java/com/cha/carrotApi/domain/Post.java new file mode 100644 index 0000000..d425932 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post.java @@ -0,0 +1,49 @@ +package com.cha.carrotApi.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Getter @Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "POST") +public class Post extends BaseTimeEntity{ + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "POST_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID") + private User user; + + @Column(name = "TITLE") + private String title; + + @Column(name = "LIKE_COUNT") + private int likeCount; + + @Column(name = "POST_STATUS") + @Enumerated(EnumType.STRING) + private PostStatus postStatus; + + @Column(name = "CATEGORY") + @ManyToMany(mappedBy = "category_posts") + private List categories = new ArrayList<>(); + + @ManyToMany + @JoinTable(name = "INTEREST_POST", + joinColumns = @JoinColumn(name = "POST_ID"), + inverseJoinColumns = @JoinColumn(name = "USER_ID")) + private List interest_posts = new ArrayList<>(); + + @Column(name = "IMAGE") + private String image; +} diff --git a/src/main/java/com/cha/carrotApi/domain/PostStatus.java b/src/main/java/com/cha/carrotApi/domain/PostStatus.java new file mode 100644 index 0000000..83c9d88 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/PostStatus.java @@ -0,0 +1,5 @@ +package com.cha.carrotApi.domain; + +public enum PostStatus { + ITEM_RESERVED, ITEM_SOLD +} diff --git a/src/main/java/com/cha/carrotApi/domain/User.java b/src/main/java/com/cha/carrotApi/domain/User.java index b31108c..83abb9b 100644 --- a/src/main/java/com/cha/carrotApi/domain/User.java +++ b/src/main/java/com/cha/carrotApi/domain/User.java @@ -4,6 +4,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; @Getter @Setter @@ -21,21 +23,32 @@ public class User extends BaseTimeEntity { @Column(name = "EMAIL", length = 45, unique = true) private String email; + @Column(name = "PASSWORD", length = 100) + private String password; + @Column(name = "NICKNAME", length = 45) private String nickname; - @Column(name = "PASSWORD", length = 100) - private String password; + @Column(name = "PHONE_NUMBER", length = 20) + private String phone_number; @Column(name = "ROLE") @Enumerated(EnumType.STRING) private Role role; + @OneToMany(mappedBy = "user") + private List posts = new ArrayList<>(); + + @ManyToMany(mappedBy = "interest_posts") + @Column(name = "INTEREST_POST") + private List interests = new ArrayList<>(); + @Builder - private User(String email, String nickname, String password) { + private User(String email, String nickname, String password, String phone_number) { this.email = email; - this.nickname = nickname; this.password = password; + this.nickname = nickname; + this.phone_number = phone_number; this.role = Role.USER; } public void encodePassword(PasswordEncoder passwordEncoder) { From 8977fc0d4ad823b5f50d5d72e7b69d28882e4f71 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Fri, 3 Feb 2023 13:13:38 +0900 Subject: [PATCH 05/12] =?UTF-8?q?Interceptor=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cha/carrotApi/DTO/SuccessResponseDto.java | 17 +++++++ .../UserSignUpRequestDto.java | 2 +- .../carrotApi/controller/UserController.java | 2 +- .../carrotApi/jwt_security/Interceptor.java | 48 +++++++++++++++++++ .../jwt_security/JwtAuthenticationFilter.java | 9 +++- .../jwt_security/SecurityConfig.java | 11 ++++- .../cha/carrotApi/service/UserService.java | 2 +- 7 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java rename src/main/java/com/cha/carrotApi/{repository => DTO}/UserSignUpRequestDto.java (97%) create mode 100644 src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java diff --git a/src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java b/src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java new file mode 100644 index 0000000..387f961 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java @@ -0,0 +1,17 @@ +package com.cha.carrotApi.DTO; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.lang.Nullable; + +@AllArgsConstructor +@Getter +public class SuccessResponseDto { + private final Boolean success; + private final T data; + + public SuccessResponseDto(@Nullable T data) { + this.success = true; + this.data = data; + } +} diff --git a/src/main/java/com/cha/carrotApi/repository/UserSignUpRequestDto.java b/src/main/java/com/cha/carrotApi/DTO/UserSignUpRequestDto.java similarity index 97% rename from src/main/java/com/cha/carrotApi/repository/UserSignUpRequestDto.java rename to src/main/java/com/cha/carrotApi/DTO/UserSignUpRequestDto.java index 7ce2f48..569db82 100644 --- a/src/main/java/com/cha/carrotApi/repository/UserSignUpRequestDto.java +++ b/src/main/java/com/cha/carrotApi/DTO/UserSignUpRequestDto.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.repository; +package com.cha.carrotApi.DTO; import com.cha.carrotApi.domain.User; import com.cha.carrotApi.domain.Role; diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java index f16494b..a295b53 100644 --- a/src/main/java/com/cha/carrotApi/controller/UserController.java +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.controller; -import com.cha.carrotApi.repository.UserSignUpRequestDto; +import com.cha.carrotApi.DTO.UserSignUpRequestDto; import com.cha.carrotApi.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java new file mode 100644 index 0000000..9f44578 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java @@ -0,0 +1,48 @@ +package com.cha.carrotApi.jwt_security; + +import com.cha.carrotApi.DTO.SuccessResponseDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RequiredArgsConstructor +@Slf4j +@Component +public class Interceptor implements HandlerInterceptor { + private final ObjectMapper objectMapper; + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object object, + Exception ex + ) throws Exception { + final ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response; + + if (!String.valueOf(response.getStatus()).startsWith("2")) { + return; + } + if (cachingResponse.getContentType() != null && (cachingResponse.getContentType().contains("application/json"))) { + String body = new String(cachingResponse.getContentAsByteArray()); + + Object data = objectMapper.readValue(body, Object.class); + + SuccessResponseDto objectResponseDto = new SuccessResponseDto<>(data); + + String wrappedBody = objectMapper.writeValueAsString(objectResponseDto); + + cachingResponse.resetBuffer(); + + cachingResponse.getOutputStream().write(wrappedBody.getBytes(), 0, wrappedBody.getBytes().length); + log.info("Response Body : {}", wrappedBody); + } + } + +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java index f4c63b8..b16794a 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/JwtAuthenticationFilter.java @@ -7,6 +7,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -24,6 +26,10 @@ public class JwtAuthenticationFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + + ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper((HttpServletRequest) request); + ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper((HttpServletResponse) response); + String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); if (token != null && jwtTokenProvider.validateToken(token)) { @@ -31,6 +37,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha SecurityContextHolder.getContext().setAuthentication(authentication); } - chain.doFilter(request, response); + chain.doFilter(wrappingRequest, wrappingResponse); + wrappingResponse.copyBodyToResponse(); } } diff --git a/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java index 83bc6d4..bc3cf9f 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/SecurityConfig.java @@ -13,13 +13,17 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @EnableWebSecurity @RequiredArgsConstructor @Configuration -public class SecurityConfig { +public class SecurityConfig implements WebMvcConfigurer { private final JwtTokenProvider jwtTokenProvider; + private final Interceptor interceptor; + // 암호화에 필요한 PasswordEncoder 를 Bean 등록합니다. @Bean public PasswordEncoder passwordEncoder() { @@ -41,4 +45,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(interceptor) + .addPathPatterns("/**"); + } } diff --git a/src/main/java/com/cha/carrotApi/service/UserService.java b/src/main/java/com/cha/carrotApi/service/UserService.java index 44c76f9..e2fd1b1 100644 --- a/src/main/java/com/cha/carrotApi/service/UserService.java +++ b/src/main/java/com/cha/carrotApi/service/UserService.java @@ -3,7 +3,7 @@ import com.cha.carrotApi.domain.User; import com.cha.carrotApi.jwt_security.JwtTokenProvider; import com.cha.carrotApi.repository.UserRepository; -import com.cha.carrotApi.repository.UserSignUpRequestDto; +import com.cha.carrotApi.DTO.UserSignUpRequestDto; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; From a74f6e7d5d71cc21747f38b86a561e3fc7931cc1 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Fri, 3 Feb 2023 15:09:38 +0900 Subject: [PATCH 06/12] =?UTF-8?q?http=20=EC=9D=91=EB=8B=B5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/cha/carrotApi/DTO/BaseResponse.java | 2 ++ .../DTO/{UserSignUpRequestDto.java => SignUpRequestDto.java} | 4 ++-- src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java | 2 ++ .../java/com/cha/carrotApi/controller/UserController.java | 4 ++-- src/main/java/com/cha/carrotApi/service/UserService.java | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/cha/carrotApi/DTO/BaseResponse.java rename src/main/java/com/cha/carrotApi/DTO/{UserSignUpRequestDto.java => SignUpRequestDto.java} (94%) create mode 100644 src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java diff --git a/src/main/java/com/cha/carrotApi/DTO/BaseResponse.java b/src/main/java/com/cha/carrotApi/DTO/BaseResponse.java new file mode 100644 index 0000000..4be6249 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/BaseResponse.java @@ -0,0 +1,2 @@ +package com.cha.carrotApi.DTO;public class BaseResponse { +} diff --git a/src/main/java/com/cha/carrotApi/DTO/UserSignUpRequestDto.java b/src/main/java/com/cha/carrotApi/DTO/SignUpRequestDto.java similarity index 94% rename from src/main/java/com/cha/carrotApi/DTO/UserSignUpRequestDto.java rename to src/main/java/com/cha/carrotApi/DTO/SignUpRequestDto.java index 569db82..903364f 100644 --- a/src/main/java/com/cha/carrotApi/DTO/UserSignUpRequestDto.java +++ b/src/main/java/com/cha/carrotApi/DTO/SignUpRequestDto.java @@ -15,9 +15,9 @@ @Data @Builder @AllArgsConstructor -public class UserSignUpRequestDto { +public class SignUpRequestDto { - protected UserSignUpRequestDto(){} + protected SignUpRequestDto(){} @NotBlank(message = "아이디를 입력해주세요") private String email; diff --git a/src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java b/src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java new file mode 100644 index 0000000..b252ef0 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java @@ -0,0 +1,2 @@ +package com.cha.carrotApi.DTO;public class SuccessResponse { +} diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java index a295b53..802df3f 100644 --- a/src/main/java/com/cha/carrotApi/controller/UserController.java +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.controller; -import com.cha.carrotApi.DTO.UserSignUpRequestDto; +import com.cha.carrotApi.DTO.SignUpRequestDto; import com.cha.carrotApi.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -19,7 +19,7 @@ public class UserController { //회원가입 @PostMapping("/join") @ResponseStatus(HttpStatus.OK) - public Long join(@Valid @RequestBody UserSignUpRequestDto request) throws Exception { + public Long join(@Valid @RequestBody SignUpRequestDto request) throws Exception { return userService.signUp(request); } diff --git a/src/main/java/com/cha/carrotApi/service/UserService.java b/src/main/java/com/cha/carrotApi/service/UserService.java index e2fd1b1..2dbf99b 100644 --- a/src/main/java/com/cha/carrotApi/service/UserService.java +++ b/src/main/java/com/cha/carrotApi/service/UserService.java @@ -3,7 +3,7 @@ import com.cha.carrotApi.domain.User; import com.cha.carrotApi.jwt_security.JwtTokenProvider; import com.cha.carrotApi.repository.UserRepository; -import com.cha.carrotApi.DTO.UserSignUpRequestDto; +import com.cha.carrotApi.DTO.SignUpRequestDto; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -23,7 +23,7 @@ public class UserService { private final JwtTokenProvider jwtTokenProvider; @Transactional - public Long signUp(UserSignUpRequestDto requestDto) throws Exception { + public Long signUp(SignUpRequestDto requestDto) throws Exception { if(userRepository.findByEmail(requestDto.getEmail()).isPresent()){ throw new Exception("이미 존재하는 이메일입니다."); From 7bbf75a9540e2ab21a706e8be821ad77f00cd38c Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Sun, 5 Feb 2023 04:51:39 +0900 Subject: [PATCH 07/12] =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cha/carrotApi/DTO/BaseResponse.java | 2 -- .../cha/carrotApi/DTO/SuccessResponse.java | 2 -- .../cha/carrotApi/DTO/SuccessResponseDto.java | 17 ---------- .../carrotApi/DTO/request/LoginRequest.java | 20 ++++++++++++ .../SignUpRequest.java} | 27 +++++++++------- .../carrotApi/DTO/response/BaseResponse.java | 10 ++++++ .../carrotApi/DTO/response/ErrorResponse.java | 29 +++++++++++++++++ .../DTO/response/SuccessResponse.java | 15 +++++++++ .../carrotApi/controller/UserController.java | 4 +-- .../java/com/cha/carrotApi/domain/User.java | 4 +-- .../carrotApi/exception/CustomException.java | 10 ++++++ .../cha/carrotApi/exception/ErrorCode.java | 23 ++++++++++++++ .../exception/GlobalExceptionHandler.java | 30 ++++++++++++++++++ .../carrotApi/jwt_security/Interceptor.java | 4 +-- .../carrotApi/repository/UserRepository.java | 2 ++ .../cha/carrotApi/service/UserService.java | 31 +++++++++++++------ 16 files changed, 182 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/com/cha/carrotApi/DTO/BaseResponse.java delete mode 100644 src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java delete mode 100644 src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java rename src/main/java/com/cha/carrotApi/DTO/{SignUpRequestDto.java => request/SignUpRequest.java} (62%) create mode 100644 src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java create mode 100644 src/main/java/com/cha/carrotApi/exception/CustomException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/ErrorCode.java create mode 100644 src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java diff --git a/src/main/java/com/cha/carrotApi/DTO/BaseResponse.java b/src/main/java/com/cha/carrotApi/DTO/BaseResponse.java deleted file mode 100644 index 4be6249..0000000 --- a/src/main/java/com/cha/carrotApi/DTO/BaseResponse.java +++ /dev/null @@ -1,2 +0,0 @@ -package com.cha.carrotApi.DTO;public class BaseResponse { -} diff --git a/src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java b/src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java deleted file mode 100644 index b252ef0..0000000 --- a/src/main/java/com/cha/carrotApi/DTO/SuccessResponse.java +++ /dev/null @@ -1,2 +0,0 @@ -package com.cha.carrotApi.DTO;public class SuccessResponse { -} diff --git a/src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java b/src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java deleted file mode 100644 index 387f961..0000000 --- a/src/main/java/com/cha/carrotApi/DTO/SuccessResponseDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.cha.carrotApi.DTO; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.lang.Nullable; - -@AllArgsConstructor -@Getter -public class SuccessResponseDto { - private final Boolean success; - private final T data; - - public SuccessResponseDto(@Nullable T data) { - this.success = true; - this.data = data; - } -} diff --git a/src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java b/src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java new file mode 100644 index 0000000..3ad59b4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java @@ -0,0 +1,20 @@ +package com.cha.carrotApi.DTO.request; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Data +@AllArgsConstructor +public class LoginRequest { + @Email(message = "이메일 형식에 맞지 않습니다.") + @NotBlank(message = "이메일을 입력해주세요.") + private String email; + + @NotBlank(message = "비밀번호를 입력해주세요") + @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\\\d)(?=.*[@$!%*#?&])[A-Za-z\\\\d@$!%*#?&]{8,30}$") + private String password; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/SignUpRequestDto.java b/src/main/java/com/cha/carrotApi/DTO/request/SignUpRequest.java similarity index 62% rename from src/main/java/com/cha/carrotApi/DTO/SignUpRequestDto.java rename to src/main/java/com/cha/carrotApi/DTO/request/SignUpRequest.java index 903364f..59353c9 100644 --- a/src/main/java/com/cha/carrotApi/DTO/SignUpRequestDto.java +++ b/src/main/java/com/cha/carrotApi/DTO/request/SignUpRequest.java @@ -1,39 +1,41 @@ -package com.cha.carrotApi.DTO; +package com.cha.carrotApi.DTO.request; import com.cha.carrotApi.domain.User; import com.cha.carrotApi.domain.Role; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.Getter; import org.hibernate.validator.constraints.Range; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; +import javax.validation.constraints.*; @Data +@Getter @Builder @AllArgsConstructor -public class SignUpRequestDto { +public class SignUpRequest { - protected SignUpRequestDto(){} - @NotBlank(message = "아이디를 입력해주세요") + protected SignUpRequest(){} + @Email(message = "이메일 형식에 맞지 않습니다.") + @NotBlank(message = "아이디를 입력해주세요.") private String email; @NotBlank(message = "닉네임을 입력해주세요") @Size(min=2, message = "닉네임이 너무 짧습니다.") private String nickname; - @NotNull(message = "나이를 입력해주세요") - @Range(min=0, max = 150) - private int age; - @NotBlank(message = "비밀번호를 입력해주세요") @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,30}$", message = "비밀번호는 8~30 자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.") private String password; + @NotBlank(message = "핸드폰 번호를 입력해주세요.") + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", + message = "핸드폰 번호는 010-xxxx-xxxx 형식으로 입력해주세요.") + private String phonenumber; + + private Role role; @Builder @@ -42,6 +44,7 @@ public User toEntity() { .email(email) .nickname(nickname) .password(password) + .phonenumber(phonenumber) .role(Role.USER) .build(); } diff --git a/src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java b/src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java new file mode 100644 index 0000000..97b4765 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.DTO.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class BaseResponse { + private boolean isSuccess; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java b/src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java new file mode 100644 index 0000000..e02e267 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java @@ -0,0 +1,29 @@ +package com.cha.carrotApi.DTO.response; + +import com.cha.carrotApi.exception.ErrorCode; +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class ErrorResponse { + private final LocalDateTime timestamp = LocalDateTime.now(); + private final int status; + private final String error; + private final String code; + private final String message; + + public static ResponseEntity toResponseEntity(ErrorCode errorCode) { + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(ErrorResponse.builder() + .status(errorCode.getHttpStatus().value()) + .error(errorCode.getHttpStatus().name()) + .message(errorCode.getMessage()) + .build() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java b/src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java new file mode 100644 index 0000000..3cdd6f8 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi.DTO.response; + +import lombok.Getter; +import org.springframework.lang.Nullable; + +@Getter +public class SuccessResponse extends BaseResponse{ + + private T data; + + public SuccessResponse(@Nullable T data) { + super(true); + this.data = data; + } +} diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java index 802df3f..bb98886 100644 --- a/src/main/java/com/cha/carrotApi/controller/UserController.java +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.controller; -import com.cha.carrotApi.DTO.SignUpRequestDto; +import com.cha.carrotApi.DTO.request.SignUpRequest; import com.cha.carrotApi.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -19,7 +19,7 @@ public class UserController { //회원가입 @PostMapping("/join") @ResponseStatus(HttpStatus.OK) - public Long join(@Valid @RequestBody SignUpRequestDto request) throws Exception { + public Long join(@Valid @RequestBody SignUpRequest request) throws Exception { return userService.signUp(request); } diff --git a/src/main/java/com/cha/carrotApi/domain/User.java b/src/main/java/com/cha/carrotApi/domain/User.java index 83abb9b..0c655db 100644 --- a/src/main/java/com/cha/carrotApi/domain/User.java +++ b/src/main/java/com/cha/carrotApi/domain/User.java @@ -30,7 +30,7 @@ public class User extends BaseTimeEntity { private String nickname; @Column(name = "PHONE_NUMBER", length = 20) - private String phone_number; + private String phonenumber; @Column(name = "ROLE") @Enumerated(EnumType.STRING) @@ -48,7 +48,7 @@ private User(String email, String nickname, String password, String phone_number this.email = email; this.password = password; this.nickname = nickname; - this.phone_number = phone_number; + this.phonenumber = phonenumber; this.role = Role.USER; } public void encodePassword(PasswordEncoder passwordEncoder) { diff --git a/src/main/java/com/cha/carrotApi/exception/CustomException.java b/src/main/java/com/cha/carrotApi/exception/CustomException.java new file mode 100644 index 0000000..e37a820 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/CustomException.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class CustomException extends RuntimeException{ + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/cha/carrotApi/exception/ErrorCode.java b/src/main/java/com/cha/carrotApi/exception/ErrorCode.java new file mode 100644 index 0000000..7b93f5a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/ErrorCode.java @@ -0,0 +1,23 @@ +package com.cha.carrotApi.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + USER_EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 이메일을 가진 회원을 찾을 수 없습니다."), + USER_EMAIL_DUPLICATED(HttpStatus.CONFLICT, "이미 가입된 이메일입니다."), + USER_PASSWORD_INVALID(HttpStatus.UNAUTHORIZED, "비밀번호가 틀렸습니다."), + USER_NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "닉네임이 이미 존재합니다."), + USER_PASSWORD_INSERT_ERROR(HttpStatus.UNAUTHORIZED, "비밀번호를 정확하게 입력해주세요"), + USER_PHONE_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "동일한 전화번호가 존재합니다."), + DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "데이터가 이미 존재합니다.") + + ; + + private final HttpStatus httpStatus; + private final String message; + +} diff --git a/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java b/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..82ab088 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java @@ -0,0 +1,30 @@ +package com.cha.carrotApi.exception; + +import com.cha.carrotApi.DTO.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import javax.validation.ConstraintViolationException; + +import static com.cha.carrotApi.exception.ErrorCode.DUPLICATE_RESOURCE; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(value = { ConstraintViolationException.class, DataIntegrityViolationException.class}) + protected ResponseEntity handleDataException() { + log.error("handleDataException throw Exception : {}", DUPLICATE_RESOURCE); + return ErrorResponse.toResponseEntity(DUPLICATE_RESOURCE); + } + + @ExceptionHandler(value = { CustomException.class}) + protected ResponseEntity handleCustomException(CustomException e) { + log.error("handleCustomException throw CustomException : {}", e.getErrorCode()); + return ErrorResponse.toResponseEntity(e.getErrorCode()); + } +} \ No newline at end of file diff --git a/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java index 9f44578..e2e5edc 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.jwt_security; -import com.cha.carrotApi.DTO.SuccessResponseDto; +import com.cha.carrotApi.DTO.response.SuccessResponse; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,7 +34,7 @@ public void afterCompletion( Object data = objectMapper.readValue(body, Object.class); - SuccessResponseDto objectResponseDto = new SuccessResponseDto<>(data); + SuccessResponse objectResponseDto = new SuccessResponse<>(data); String wrappedBody = objectMapper.writeValueAsString(objectResponseDto); diff --git a/src/main/java/com/cha/carrotApi/repository/UserRepository.java b/src/main/java/com/cha/carrotApi/repository/UserRepository.java index 3a12bde..ac661ab 100644 --- a/src/main/java/com/cha/carrotApi/repository/UserRepository.java +++ b/src/main/java/com/cha/carrotApi/repository/UserRepository.java @@ -9,4 +9,6 @@ @Repository public interface UserRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByNickname(String nickname); + Optional findByPhonenumber(String phonenumber); } diff --git a/src/main/java/com/cha/carrotApi/service/UserService.java b/src/main/java/com/cha/carrotApi/service/UserService.java index 2dbf99b..d3cdc38 100644 --- a/src/main/java/com/cha/carrotApi/service/UserService.java +++ b/src/main/java/com/cha/carrotApi/service/UserService.java @@ -1,9 +1,11 @@ package com.cha.carrotApi.service; import com.cha.carrotApi.domain.User; +import com.cha.carrotApi.exception.CustomException; +import com.cha.carrotApi.exception.ErrorCode; import com.cha.carrotApi.jwt_security.JwtTokenProvider; import com.cha.carrotApi.repository.UserRepository; -import com.cha.carrotApi.DTO.SignUpRequestDto; +import com.cha.carrotApi.DTO.request.SignUpRequest; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -12,6 +14,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; @Service @RequiredArgsConstructor @@ -23,11 +26,9 @@ public class UserService { private final JwtTokenProvider jwtTokenProvider; @Transactional - public Long signUp(SignUpRequestDto requestDto) throws Exception { + public Long signUp(SignUpRequest requestDto) throws Exception { - if(userRepository.findByEmail(requestDto.getEmail()).isPresent()){ - throw new Exception("이미 존재하는 이메일입니다."); - } + checkUser(requestDto); User user = userRepository.save(requestDto.toEntity()); user.encodePassword(passwordEncoder); @@ -36,14 +37,13 @@ public Long signUp(SignUpRequestDto requestDto) throws Exception { return user.getId(); } - - // 위에랑 마찬가지로 변경 + @Transactional public String login(Map users) { User user = userRepository.findByEmail(users.get("email")) - .orElseThrow(() -> new IllegalStateException("가입되지 않은 Email 입니다.")); + .orElseThrow(() -> new CustomException(ErrorCode.USER_EMAIL_NOT_FOUND)); String password = users.get("password"); if (!passwordEncoder.matches(password,user.getPassword())) { - throw new IllegalStateException("비밀번호가 일치하지 않습니다."); + throw new CustomException(ErrorCode.USER_PASSWORD_INVALID); } List roles = new ArrayList<>(); @@ -51,4 +51,17 @@ public String login(Map users) { return jwtTokenProvider.createToken(user.getEmail(), roles); } + + public boolean checkUser (SignUpRequest requestDto) { + if (userRepository.findByEmail(requestDto.getEmail()).isPresent()) { + throw new CustomException(ErrorCode.USER_EMAIL_DUPLICATED); + } + if (userRepository.findByNickname(requestDto.getNickname()).isPresent()){ + throw new CustomException(ErrorCode.USER_NICKNAME_DUPLICATED); + } + if (userRepository.findByPhonenumber(requestDto.getPhonenumber()).isPresent()) { + throw new CustomException(ErrorCode.USER_PHONE_NUMBER_DUPLICATED); + } + return true; + } } From f73275b5543c8890a017c210c83731891fd60ab7 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Sun, 5 Feb 2023 05:19:50 +0900 Subject: [PATCH 08/12] =?UTF-8?q?ERD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cha/carrotApi/ERD/carrotERD.png | Bin 55887 -> 55695 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/java/com/cha/carrotApi/ERD/carrotERD.png b/src/main/java/com/cha/carrotApi/ERD/carrotERD.png index 167b646ba61472994d27db87dc0d7a03cc6fc837..6abf398f203056d8af7173a1cea172d315bd935d 100644 GIT binary patch literal 55695 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

MvX9q8tmEYe;8IyIJ5%8*%LBtrJ??PuTU? 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 Z|IBN)|J>G*#%;|21fH&bF6*2UngCrIf|CFM literal 55887 zcmcG#by!qi-#)6Mgfs>*bV>+(hV}eAgxkEmr_Fr4Bg#u zHtO?ypZ9%#=bY>M{m%J=>tbg1UVE+2UZ1+}dxfbg%i!K8yMOD}EnGQSDfL^o?jV6b zCQNkT8#(Iu?pwF~Z^=nXXudVxXu@)NIN?#deR9&aE9X7WX6ENO&##=e%M~G#nO^xo z2)^Ifrrfwl#T3!Gphxoh)#KM@x*5+{=K0=jrXIsy#M`6-&Z7<@H*%#U&rt3TAw_o2cp>7lH|Z8-oa5fHNcc*{Okl zfKTCgadv;cHN0z)`sV^-i3!HvzqU!M!m)1dz;q=1zr9TOVV#dc?=>bVkNtGL07`J* ze_N_30VFL%0+cK%EpKMg_ZlmM1+4YrmYpYM@M{u z@?Y~%g@>_zg5cx-w!`kTC&;!(s_>8ym=a*8w6Mqk&2(L^DVuZPCJG}TTVhNHeoZO zCuY03LF3BR(_fx9f3)Aei@7B6eSRhATdP#$0=={?$8st*T_|5WD$X6hY9i_ zbAdB!bu%`DYmBxo9cg>wA|DlHltc3z-R({eDhd)12Gsy1F9wHT@G?A9?kBHQ!r3jD9rY%vXbXbQ!Op7+bF zGN2n#>Bk(@gl?)wE0N{lDI-Qt+tTjS)nCCan4DbnRu^A; z*e^<83B;G|zp`YvIUpaArkl(3dOiKs*YcDykqH_)%X=-6W}@e(o56$RtuY$etA&$m zJ(Fnj$sX?Iq#U$k*D@h3T-YpwyRv``7Bgpe<2y}2Xa9AU{H#bs^i^jOlIIcH2*%a! zUb|eXCM^2h?q;nUBB1S~)2U5J!fdmT!S&-Y!*~vrqnPxeRY5=D9H|j0N*~7&fAQIR zTvy68>Xqvnt4x=}1G)1<$_e|Ry2_u7BNfRDK`+hT<(gfaq%`UgvB#~#>Td6H)tJD0 z*7YH$$R!`2)%L!Wt#{0#RhrrT#A~)KNjRr#A~Sp*fHSp9{*Efb;n4_Q&xdl%h1zmP z4?MN6w%$Wi^zT&aQ>GR6&~y^amqix|nwXn|7Oy(FTt)js*-;=QYKNFUrE<~Y+`I5_;8%z};WTXO!f#93K4tObW-+0_1&~*9WT*J4B0K`blGHq4EO0!Ji^Z zz*F8dOjBVO{sSGFM2!q3r;|Zy&#-@V%Zy4KlSaUrcqL?Jc*%5{zbr+abmW4PBcirj zmJ5n#(!teerW^>TDt6k*Sugj3i#PQxhSn|R$l%PrL~)IXDSK4RwT)b8W7Sh|&wVHa zNhOv$X;T=6)rqjjG(!6)XmJuvfK}+Ne(36&_4vT^Te}ib%lm;CcS#-$%XyyJ%X|2g z*dJqS%uEgOs79o1RkYM=IoCylO~v}A91X8lH!0NBy$&N5llqA@ZGCxY$DgLd(Ib)w z%4SG>m75Rd8IxaiKcgYD_q&L)<8H1#s%7I>Yd#|af$RSxj+)WKaHAr2TaPa-_UbW{ ziKD1Bnj&llXjVGOXd}3zjvFJb1Ep|G13O~ImEU_weD2--_&l4ORV*Ep3#F&>^GW4> zDP;~i9gQ6g>UrQ`KZYro%fF%6+_>$mI2yuWdmocbBzs*9Wqs{!{JGJxJL=t@n8ML| zC5;S71lsuJC~DM}f{%6_@j#~5zmV97;Hnb?ZuZ7I5(PxSf5&Su3-VUr*Ogn(Ignq> z{K*e(M|QatpN=1p2m1I# zT?Q$TR<5Ti^%iJrXJ$S=iDE=eb1|r}k0Z+`{q@G=geksGkrJS81 zdwTtn?|$xc@ixB(F36{h5rO!ow7C83%Y zZHIX@q|vok=HMrz4cQ^1ojOeCv-q~BwKqOSMO=~IkXxSPAWMOASJ2vbl^bApIXoDgQh3X7SS@6OmpPDkp@eBVy;y$!Aw=bV!l<#`-*D=n<|p4?E9k z4c{Ui)oG}_`(;h443gQU|Ol>r;oURoHAIc+ih|>YFY|=Mg$cK;%B|l+` z)ySLlUh0>veGW6yw~4i@^KnKT@~i!|^oQp#T;`r9rBS#uzC>QUTFbUbPn5TP^xX~o z#2{y4)X$>rLI{U`T>?x5FFP!z`fZp|#j0FqUXO4f*PkF4 z2&BqY=nD|=GK0mH-Mjmrf3Swo=9qWlnM{C)XpwNcpD=%a@j6vqMvukB_5m?*y1ADn ziHU7(nT-x-UDM{E!10F2 zoH?xYyCVJ0oYSw5GcR8xlubX0^geseJ7Fw6K^a*HVVW=(V&OHm5So{*?#I}=Kdea@ zZ~vT%)W=YMO`o2NQQNz*$-+doklq3%>*koJ5xsd-ZcJF0XHDxqD8>VK?Xe9YHdofo za5*+LiZk>1nEF8ZEAyYaeZCapu#3#&y6nY{WoUs(TrD|VD~s-ismNZ`;ncl$v0kfA zR-_~CoKk|<+V93Z09UUII&>#jvq9fbVMY$_<@S=NM}~e#SmRxGg0oh*$-kDaA|EJA z{L+}!W&(+lT&>|^c&3rR*%Q4my4Q#<$KFgk5&u-(rOv2aVjTJwh!&iC^?&EWr9N@B zncEekq0Q>IJJAv+b`>iY{o_L2n%B76s-3L+h=0i**3^!0lUEMtFIX}r+O|V#!iO-A zAoWVk_{oPXeMgUu!)yXm5^Rayx=Th;OLIVPR#pem>Fg79_ga-yZynp$b|vM;EB^^ zy`T8o8d~tCMokze{wh3i6Lp2)UFu1h%&Ts8m2Ha9B&AV#+Q1@H#jc=uq#CjKLr&MG zCO^8dflp>k=yifjYKvEFiMfJz5u1P^HOIMSv3Cn7txRourWs`abm~vDBI&3N=iA)T zNxs@H$c)b4GT6IHI~Baf@Y})I8}r-0cX}D=XSlpvwcg^0WiB+gA+S5ys7GrSQDPWE z6%nHZ;=_@G4_Gdjc!vbNS8%nhU9!sIBjS#lqNtWJB}%iUo*1%Qgy08Gjat<#L8v}E z!OOTnO-(-PhO_Jq1KJ3MD^fD7kkAHad91$?7zjn7;{iF2dp}yPr4mZ@3#P9gjT*2hCyI5s+{?!^V)}tNzCwHDB*-vQgdWp+2sXX_s+`lcaXypz`3t9zP3YZy zIC^|73ZfPZt~1$DN{owPt)Hq?cK83&tUc*R@P74Y!X0zK4dph+L7SvD^{mTLk*Hf( zi7~(MAHX5BWfg?pwtMD)#eHQXuvc|nTeooNlRf#%7Y1FV$QQ0TC^nDd>#5nAX(-30 zo?_YuEqc%)tME~BwwAS1^{c+$mHrh(EY9txIELkfKc08-e(}f=bJ~=OBRixnGuJ~g zcyur+4TtB_Ew(u}?c)749HllKtFn`Hf|5DZp2-k0n4tjAWOJ4u?gagP(^-(%p*9AX z&w9Qlx2u&-05<+2KC?T<-yRl-UO8c`<{&5wNWTG9_K9utTW;0*jK|CZ;ot133h%O- zzdr#El;tiCvTCX2lPQNcn9#}nx1WusMSN0Tyj$TtTXm}yt^Mn9H^XE7Q&jw)d>o$f zP|}|i!X%}b6hwKtA4jcky!z9ox7pRXZKk-mxyWMBXsR?_nf+>kyH7d#Q3t zaCx-x(sQ&?>vc=n{rjYUMy_Pu`===iW{?yF#4ULFf%xj+2@ z6&2M)vrj{sn4hospep6#$N3glS64BqsU_LtQ&Xi#pgfU%`SMdjPDDxy85S;X{8IzC z)sIK(S?rjvBqRb(PEWfcD1_)aIn^eS$Pn@osawlWSy^p=3G4T8adE{ZBs>;z2vwAk zl3Ibm;k+Uh#O#JqABj0^1p2)k9V;`0F4|A=@bSl-Lj)r!gcxNa$fHpx^D-t;QDb(K zX5pQQ1#h!d$q+ob7cbs%b91kV+SuBNO}>t(g(muH7g zYd@GLTKrlRGewQ0%W7+N@*Ix0rk##9^d2pq?k_9&_%s>TIY}q58?iVq^+qEQvI1{+ zKJ}&vXe7=Z?k;p%bcB<#_!nXfH^2OLN8rsbfA&-%_ww$1pIq;3|Kp$#qO^+odcpM0 z)9;dUuV3eL^YY5OxK!sB6!auNw`?05)42y)#<~Obkabm8kIArIr@6QM@J^&1^=?w9 zBwjz=TDwQcrv)1HMO}Pa=m0-IzwTJ8rLnPbIlh)|@BsO_8)LoFKf4Z0i;S9&i6L5D zUmsay)h->BeS*3#erLB922*Dc;NVcnsrvZ%srpc}$A12n!{%fS%X<+Kk$T8jxe*zz zOw;+%W^9>3Z87ExQJQ##1h%2j@&(5_Ap#w#gTQz14ld@sf2B#l%T~Dh3l2ZMl1{9o zq#YW^Lz+><8Q?!TDiVDQbq?MC;QINgXr{matxMP2nHZy$>YmgKy2PE@YHC?AcwHTC z{f4Xn^z|>d;ipq2;FQuKCbbS0mTdc)uarv9&aTV=S|x}wmulv-pDG-(m9nc8H78xg zOyH+=y*s4QD>+ZVAQSiaeD~;R^a+2EcV7w**=>v{70c}6E%ce^*&U(03sb)u#Um=o zw@KBkWx3Ubcf?J*ctlA&O?mA3rpwF(Phf zLkG~1v@ockjOP|J5^(e1cwV=xx!3M|JJ~C^IY=Ff;8mx7-ItO#v|e1y(zlz==P=_B z;V^(Jj*ziU9PRC?pE}3cHG(JaWS19WO#hTPp$>V#3B!GJMQ9y-v9!EgGBtlOcR?xY ztt;W7p%Dv)t7HfZP$Q$PAd#*H1_p=1^H~^~#A_W+3h+wN(VE1~wKbc*%Z^j<{rkCO znN^SOb)L!`ke}y6#K`|fBM;pPQOEfp-fqb&UKEZi?b+E@!*+x&oRCI_o>%}$AQ4BC zeiwR59%-HZQjU#wEXt6ZtlbOjiRtufWO(?XQTH8$Y64gr(qNUAhK45P*iO1Z&6jVY z!$g>E?!sW`4R>xXBvSP6(@4XB%D8$3Z(`>2tHAtdalBnJC~fprfF@C{ueqgV31p$B z3F0u*cpAY?grG17S%ePK)??3s$*|N_RkwN_>sjBkasd4fb6jZ4Dk+qt2ij&W=$*I!(e`n&=i z$lv)U6DVP8)VYu%`-=u`xxG8AlPe7arA60{Mho2!v*PJ?RQC6wIAJ3s>^$w@jj~~L zHzYoPL3*Vh6LDC{u|c)3i(j!j5njD6{ZEpZ4+lEr44+mZf4irg)!k+2UPl(AusL|u z(q?39anG8TS@uWx`vs561ij_2cX~EWkY?##tb)F^uA2k3muI6#t!2w>|73+l3G)w8 zUK0O!`2R~@;U%?u^6x!+(@XOHGuBPJ2>NHHHyQPRH=PmC;e1p^SPdbOLMTBmlHUmu z$a7yXe7NyN?Fn-*6DgN#bzZaLDxtu=w3Ec-OVhbSiKuNCV!5KYN@= z)Xn>+2A~Jt6&sMm|0aFFHl)~3kb!^mJz$^?0NA4b<57X1AhDG|Sj7BuEA{{5t)`8L zqcLc>&!GiJA_dx-^ktN2C)d2^KVzE;BY34YW|ub-+~LwaEse`evv2nJDXCshyy@9( zzFM_2@fZ*S-(bxjnihDEzgLP{_8_R^HYJ`Q?c2&y`3~PPv}y4hI|oIDbq01HjTuj5 z`*H4}#2V{eUmApREw!8mN8YKtJc}#Sf87M{ru6HBZ_cVr(qkwuW97@)e#cm&#QOoT z1?x(g0T$!eW5zX`%T%+~KO;R5I{?H#I=8~vhZ~w$(TK`h&+663*lrbcd%9k+ojN`M zf?tjY9)1?>CLaj62d6UZgl8O52OQ&l#yNje>X!|8LJ(||af*vr`y^TT^claJ_NJH; zVgGls9-D|cp{Z2=VA0wlLy`nM`%8h$6Rh{ ztQ{%Up#G_}sqUO-1YpQre%=JRlHabaj_kvfnPc>;8k_SpoO;O2*AkxSeL2rHT;I|n zwdd~Pi2KnM`}|IDuD7_RW7@boKxq6h-h8&_rj>t69_yeoP4f+FLVaS81vfw9!`b6q z!L;mm*K5JDb&%0^mzvG`gr-K(7tN3e>*Q0)z7ngrvn6G@{)z=A^ewqor9EH&rR!UD;h)A)IDD*bakNStYfbOhFuLkPuXW$H95MeX_H{a0 z*|ni>JS8YW;^;cl!*BI{P5nlmD`CA#r?Y7E?@j3T;0;iUDMq#UyvkYnM97u$>*FmG z8?7a5ER(i~F)AAK`Cp9?hmlZDsqqxHfOc*G;r5HaF`|Jel>d z4cQQiF4>O<4-TNTyItv&qNZ1V(AN2!!xOalYUH}}$UK~uR40O?wjz3lfmb=Yx!J&6 zbve)Nlx8wPp9cu`#F~6M$tkomQiJpXl4W1jH7hMNjjoOA!d`e7Yp~L z(bmmac)t7YC^k0-zg-YLKN_`4Z<(B7Q0bfGK{Y9J|MrsUyM~1dy(7CiCK=-@-%;*p zw7RprCgDGE0dHg!q>JCI{;nS{B^6EB7a{B}w`?||fGPm1(|jY?yrEabGFv)-Oy@5m zIz``{Wj8l#`bg)Wwjnr{Fl?K~%lj}n%#VLULwMe8eeshj%uFLKboywmg&^%~?SS*U zVY2Nt&mMe;;R}CBcNT%A_WCXXku!^n@7&gdl?(RhljbNGWR>D@yeux9kxD0#zFV;^nUs0cey`of%O+%?k?37wIk4#{d;9PLu-yd<3%M*ff z$eiVm>UQF-P+z}V%=Q^}U&wbko*Be=_jv?f=;}XF`!i{ubKmE(+&!GVR$c#oNyUo5 zVGyO#-<{me>&O6XTj3ClcsrCC21pIu&&inU>g~1WR%Bm|#kQA^pZ;`kENbwE6!wnD zweH_HFo>`*=FPt>aN@~q$j;JQCX}9WSxH-xMlN%%UnyN`_rL90d_D0vIlD9PtkmVP z>~8m^Wp0kxFwVpka+yz7(JzQ1`c!Fd8KtA}Li2!gQHRI6|MZ;^lS`9Hl9`X2MN&e1 z%H>JBIpTv)0nY&0@~^xaLNWmv6lj%EyIQQER3hgGp&Z-{E4=9$HPQ-Wf7;!rM$~o^ zGPC?f?IH}*!i%6Ao(kaCNLrKhRRyShuzJ-wy zf;kwD9_-cmHP9+HEJ@n)(2gb%FkEFaoNF0Ynqc?R)z%lAtdv<7kEuP{ieOKDY4+Wb zRz!*p8Oz%hPSKfH)ib()l}{>UTGL6GTQ_bQU2U{Jgn`zGFHFy`{PwFqHC-aISycAG znHAp#O%B0(WJfiDZj!N)CsO-zZDT9mgndSA@aHc8u3y|Lju$2xTu9;9b1r_<=&KJ^IK-&0ZI&?1_i-Ig5BF3(yw=rD9q6)T zB_-&`kAR&z(AeRFRW~5^?9j|BH%45SP)$bViC8LeX-^agFZ$~G@zziFqc)$Y zIvmvD`?~Hw-W#x48XOJ(W)5)`|ALV?F^AMxuQ^{k)rzm)7Qn~@&mnLD7U zbYg3gnmw*On7=JI8XS3fII-O+C~8aE-@cLFxd<_i&(EOdyF65q5nl63{_`5%Xu}=R z+S*0Lo=F%R`;D*C*x2zkdW~($4@TYPW9nQqd%x9k{dz6z_X>cvnWU1iZd5i}TvF1O z9;~wyTPHy_2geWz4VN~1(KvSaste7>+U!L)WbGt&I}0Txh2X9QNk>;i&6%0_=Z4tu^X_9RN}uJ4B9#g)^e z6Jzloq^tCAD`cNb9Q$H^AD3B6m)5Wfx5{SEEz*^26DsEe*d(CrbbA2QK!#J-4&YCB z8O+yJUX4Nt>Fo5P4Ww8~Gq+XozO`AIygj?BO%6cgee zRc6Lh(X#s4Pm1cz^Nl7)4O8`x+YQ%YH?Y{it6Z3!+~?=(^+HGAuc>3~@S8d84izu< zdCC8jp6{w)P(v>6jjw~%YRlMA!!tyTNI`CPvlg#RTH@3mI`nM;@z3t(tlu?t1Lv!F zba2bQ58<<%vA6f#RykaW!D`*l&J(CtgpnmlfjIlvn%NYTrW1?Ht!8sVKP*^^9oXs5 z4i{aCB)PwI z6pj^wJ*bk(U5}GFKDlka>vp_;KymY4TV*X$qc8&gy-TCPd;PoyOlKuudzGP=bnF3=lv70A$^Gvqh$l?cWEFdv*vEKr>73^rWwxWo zJblaKUqektug2ap;7(lMP@BJrMzepy1KbHt*!~{kAY{6}P*u28yp+?2Ej#Fu3;mRu zJ;p1&=56F$aN%MfwN$2;V@xyI0FbqMmDkk=q@*%&pzF7=9>^XPQ!sc?+FdD>7P+>n z8aCt0ZL~}?gXr99aKyf1mDttWD^2dSGP=>`V1T~{J)TX04`;@h;+unf@=e!P*5a>? z$g&CTYzYEQdhtg??kcW~Jz}Oez%N)4&ORmB1ubEt`;?ga-y_H0!>{y1@Z=Z9eQ((E z%LbGoZO;Gh#E?oPxD#OIy;D3yLdCMo# zSS)Ubkiv+^d)!|2J-UqfDk&k|^wcwAsEL5NGpL#rr|+YE!n3)oJ5ML0(F0}q7#rQI z@viq2QV%8WjLe}F+^7E+)+dAECb!G#43r5Ls=Zc#o9IzxEh5G`#KJj*Yb9Q9R);-x zs^OF$`)G^TRRY2T(Q0)&^?+);jl~-&9~^x=z*h{Vm-J;ywaZUBtb9twKwChDhTUe7 zu;#hq{xFb=jdvUSfm3_s_8eK>cn`1ot7p&dL>$VeKD~FZZocbO>x>Ky^Wx`o+nQ7s zMFfS+L(u8QKO)*LKV?t}GOu{m-4QB9E`@^)Tr`zY`8e!5lh(tz*gpZdnC^&?lF?1vM=iFoo z##hk-he#ge5@Ic?YH!^lE2X4KTtmHIR>$v(m+Y0k>FnTe5tL4B=VOc67lW4^$RuS8 z9V?$vx2RS8;|bayeH2Ep^S24S`EAvm#&YbPzBRh{_VF+~r=)tg6 zBdxmmQU4d3^URgsDdXP_y4wRFdpM9z1oIPX@)kiH!W4&2>6WoE%o_@}^Simx3B~D; z-v&RO3@>VImTop{Td#pePuhlFK6E?F_FipQiqRPTTwNBcjd7TWe=0*$KPu zk#W}Mqdscw1zKP2HF^+;uZ8ox@{;ARfBj<2PrFrFan2I^AmID%l=6*^@h#p7pYim# z&DEyEh^C;o0|D3MFRz2lnWOmm6vl4&!W}MAfa;^kU&+`e#(uXbCW<+1A@)oqAxWki z6GcP|ZL~d*+KbevT_0hR!X}Ccuq)jpn3k?b&z{n#C670GiW}|cF~D2+tz}@M%ykl7 zU}g4rop-LoH^acXWWD9_=Ew?GJ;}xCg~(pMPMjhq(e?8xKZQN&^YH;z5MB=o=v^<} z`@F=_j!HBDndj{s{+h~Wcp54KT33MH#pozfp+s&x-%l-bV zX}0eAmXWB3T~PB;wbhM=a$s(>FV!ipM&tE)NB8;6P%z00aJn@z+akj+?FVm-$dBV2 zpp3M?8?KaA8gXD(*QwokHu{o>fc2@L>oY!5|o#Cot!+h`ZOON#*0@)>{Fe zLH-gwo2`mH#~Ah41Mne0>VbJTlE7Vl;;3NMkf?SzbdkHj4?wAWlH)wAn8n(Q>|} zdgoons5UN zOTz;mCA+uDN1g&IAHj|K1-HK!1GJp3_yk-6s1JnY;M*N9J#pwJ0Voi#)E`g^j{6_g zwW|&SA!x){AwsXgOU`Jn>EU|$%EN4*(E7_H zdo8e1#kT;NeQWnaWMtwdvR1*vqwc=0t!;@E7`z61z@%)Yc^;d2k&9YdQio4tUJykm zB;;F*pD1z|H$I7vk7pUt)6+ZK2|A!QM3dzUNb~aW=vo~qU=R~Csco3$k^240=>#i` z5|8jux`bTa2PQgAP5AB_^g&-R0HV5^aOh%H#gB#R6d^-TOZ%eMXF1&+b?m!+>@q=h zIYiY(bu~xToq5rfsS2OI>s^lcCI&EJkO`Qyyu3U&g;i$+1rZ6!6Mz(C`T0bWz9BD9 zn&`oUb|lhZ?3;Dt6kXqFEYAE%P>3|&% z-eCkA_O;GD#G={y7Km_DR1DvkZ80$s+wXm@kKyFevY)me0jHgg+U)IB1jJ>dCOqWpZb7gcC;Tr{k~KdsD#%d(7M%9#^tkz0Bpr~2 zI@Ls1UG7Hur5Rn(?r>!Y_|bV0%(^p5HFwskck4n1IGXg%or(O zKDZg0+%n*agpFZ@I{Tmbq+f)3{VIjzAZuOMN4s+6V(EE!v}z@D_n~|t&|E2GVIGWe_=Q>U%ItIZ zv$e$M(??OeXue0zhs%e576o**j3|k$-(iOkaD|K)Lt>2J1JBjGMDF32ev<9dEndg= zr#&qf3Le*&$2G>Cf`}6|v5Bm?`@a3ojGhOdOtSI4eml~ig2oPT5ll;q^Y8Dm_i8G9 zlrsFTP6Mo-z?%T#_2mmXOm2IR-F9NvWHSo$i-1N!Vft5J%rCq#sHeZ4tqz>cx4{6y z&#qTfTYJgHukhC&8vvRTfHAlUB%rx)h-9^mjZL+)r;E5l7_a_=oO_&iamqf@AVa-1 z;M0K$iR{)3^xUf7&>n&vCC%t%c__7+u574b7>KR?C%j|3pO_xPfFY)>xhq(xrcf$=-?PKCDoz;Uk)Mw z62c8Z^Lx$sv`F0}sDroUa%@bDCL9ReH(bcHIdXQ^#Dq?So{^Xx|f>L&Cz`yNeLG9&G8nR{JdT}~1ZosMav z$#MHvn}b9KAd}T{UToiZZgl|+^sp=S;!59LqAlH8y8rPKFfGkN>(s6<_fTCh?^7ou z^sf8&@5h*2pN;KxvsPGb9f2Ggv%aHqI+q59b5J9f2QtS(3MVJ^p_(~!bf3vYv(H-q zttnv5{SFS20+mc{EE%fl@&06v(U2x)q8C$W7Y5raE8NG#Q)ZZsxL-Q{6@!7tio*dJ zZW(f1Ajg^Cxj9e`K$KPOMTNc8s8o1c>Jb0B6oQGndT zW5172>oYNjF&nE+d1C>D&LX#bvA(f+{T;a0OKr9kHWp5E4*gfz;aL zP0_4zk!!2G8zvOXCG|zkE$j1(1fMVJFooifIF*Mx&HcH?Q6ojk?5Dld$Y~#Ux62T$ z>1(|yg=*JJWG#K!L9y^cEDOl=qx2Np`@1($7r?`LZt(EH=x$!A(n(A!CEG~C>T-!> zE|~VndvZdKn@Sa*KBSFo)>b`EFFg6pq zsEPRID|Eg9H{t!PQYC!yGDxZ;gG9HpxMY;v9V}7To4g-j_@p@PD{Pu!*Ro%3YBe_i zldr$lb>>Y3OZj-ez|ugjg`CY6Ly`45=^iPbjMhGnb@M1)EzQqb2RzwYo+Rk=z)5OY z)6*=?j3edvk$X`6$OXX#3LwVcoDuuI4}jzpY?bh%R$l#eMjIoef~Rj7yNqZ1`|SHr zoJ`?Nn)fw2nus0uzB4GSZ_K1WcUji>)FK`Z>)@4B?28G?a%vxywqn_F`v3DK&{i3?dX>mfja3=GzGd9Mss7rbZ zqd=n3%cE&gluwrMzIrk$;U`Y0&{|vIorrz!euiWrhpV6jyT)zd{j2?MTP@f=>w5eh zN!TgQ@ZN{q(kvyGzH<)lWvA^cRRJPUK@X>&*jza;tckf?>OwhQ!B;8uh=tavXEn{X zG*xGsrQ*c0`^P7?A_vEo4B1;i+wpbe*H66tIFCof95gv9(5!+)z&)6NLPRpuS;5x4 zeXRy&)upai`MM0UGg)j=Fj8ePG@K{c@^Y8xDQl5Kbm55xm)R1Vk>4dFhn!;1a3MT# z08bAFsrzv9PWmH*zJ&Ac;mNKto8B536dFb87kP$!a^^W1~tS^=b3Hwz*0FfiT ze%@Kz#QuDPbHUwv+wlY%33|G21N-XMdifTwar%28hhqRf1pDXho??9^x~SW+*3kWsd`wvoq#*7ZSnrP zMbmZ(;MFvGsc_F88@lSm;?&&gEG4?JCxN3`#0+VqUHkO6S}Ze5^&5=W17tC_aEIbz_dVWo`E)2gTIyc1g$;)~`Rmor%ZGsL zYweHYYrsJ?!BLw<8nbaxp>*n#_HS?9CeZ$8S zqIad><0fix>hrXF7rmL6#-jlHP_o4wzlCEiM9TuE%`rRlCKq!Bd%K^=gC&Z< zRnPCsn->snuUbh~?oDb18yw>t=Pqcuq!1mcWY_rqNzzhq8lG#TY`{EF|BaJY0~`3m=Tw5OAhnLg^K) zHx>~!hzYCPK)*`N18I=CN4u2UlswOAV{w7iSu}|?G5kw~ey#Bx8U#3Wz+$nd+Irje zerNlAx*g|bIeGi61c}Lk*QlYZdvq`7WsdjTQbj(XBazcBAi&oE0HiQG2hg$}Fvuo;8=wEH-mz>z` zYLdZ-lqC$XGVs_9XmFclMlzFL;xdJdTH-fr>>LMv{z#S%M|sG<%$GkhZ^23L+?#z{ zU{j1a+PcmRoMkXlj5aK^8kMcB5|~+d^cP6!Fos8Ssq5+%5^e%z^XbL17zfbh#$;x4 z6crUIGSjs60o$l%*FYnr)UZnXog{wuVn2xNP&$b8j^O5{HJOM#@4!;jlwQ>`qB2%a z-ET7iGCBXW0jfG?(qN4`5B{)y)@N|wYL+P5dWb}Gl^9F}4S`!_U8*d5YPW_81*ODT zw&KbtC6fpxQCM$7ZGR&u|vaClw>Z)!M;Ig)? zZ~T>NFC}DT{M@vOv0nDhfTor4QL=*4WCa{=*VoI`d+Y{0xXIB8kagUCML^2BhoFl@ zJWvs)$-F=@9ddxrQ@AWX;33acfQ@s-sq1aBIo(==v5?OZHI14AO&y*bMtl?ZbNPf=bjn=h;ctSTd zD83Jl^AkKzl5uzb;Q=ZKS0={ zi$hZS9Bp5atp>~j9h(xLITG(bdP)&7-RxiwFpR&!wh~80Itd+rS6A1)VFiw^nxI}#dNVO(nrKRGFG!w3oPf#K^|<;XR0km`Np zz0=a}6;keeaUft6Y$o5s5q*h4w{2Az!j+rNvbmB2I>uWpXgA+ReP7DAt%`l#-36fYOz(e zhU&I<3OaGmRpV6(alSuKacgxcs+TzDr_lFh;^lO|5*CMhQPf|x-mQbb)J%;E`}Q`< z&fm*bTP@EGV+BM)@AaaU;q-euA;u$B{Q8r9T+Eozq%Q%5{M|dmgm0vZFjmt)%|2Mb zMnR5K);Hl8(*K^z=v3_jlj}nc)ByAVyoysF`6V`Gwg8yS2HJUg%gbn~_DnaApiB4L zGuuO7?uLbjHEk!}9ep3Stn&j`K*b#m=FUu2&%&qlpwB1VujMbrz zn5bXTQq`MkMPPg;%BZtBc5!&a8E8hxI56WbP?7@Z^#V9hyF_L zxn%qO9R1S1d#Q_DCiPJ4YNFRRM;22dfAJ@?QQp-v zAc}AX#lDd@RmpHXvs6co-pgR8vZAneZ9vI+xCXtwzaR(iYJ2}18e{}c;zvD2kf7u7 zKYUQ&1I@P0336U_5)?7ub=V)8bKp$6m21E-DXYISmjiR!6tvTd&|XeB$tILDpJLuD z$D(bz9we+&)y$IIEH|uD28vy76+F6+(hO+{zySlBndwPrYR%8H)oO^rjN!HK;0Fq% z-O)^W=Uu`!CznH)J?~)1D%A(RmTX^HR?E=C)oQj0ubs~?SBxR4Nhx`)cg)63$k*Ss zD^zP;1jX4oiz*iwM@c!)*i3pd@Dzy8D>E^B9f#Ol7g)(f9QLOScot-4Zs6Td_#7%L z*TYKgjaiSK2c-~%ja}O-L^Iz`hMm3*%S|+jk61_4RMn8UH?jjLEg*FJ7fSR^ZvqY; z|Nf@7N2fgQq>34AF4=p*t#>weUmzhV*C#F;e|Th7kEKgHkSLrW_%_;L*!4uOupI>R zTZX7lFu5>M&NRoX75#7``0`hp_=R?bgsupGBf-$ydo{D(`?wSHRgxh??K=%5s9Vz_ za|IGO%O7uJ4)zfRky~`>ow9^rKV~1UAF{ePEs@ng)0ugjP(;j|ZKko}C(F+2DFc_; zD4U&XtQ9@Ekz~{1k-?G(qY9wwCjv1=4|P_zUs^j~)!I$7M00jB@EpgGi)4Jd%*r(FN@2ctplfsZKXVxN+(#{ zbcvlXzs)+BQ!+E`LsccmE2$33>P`Cu=}!!MSA?G3O9@JP- z^Q!|Ah1@-32ny%;2Mxrci`_JZikf`U%-U~veJU99IJ*V0_`bSq;AU*3 zt6X|;)4cqEmv0SQroT-Nn#U6wgil!=)kvKEzKl%VIlgdjS<>`kp$eWC>pcl0K~-LTKb-LL>{-=5!Y8ZkMSs~Od+ zHlGMmrmIY}%Z=UOdjX9jV;%ExAEZzMZ{zL!gjdE$Kz+sdu8K}) zIUt#3oFx3#|3%rGfJ6Pg|HG9^lBE*Cb|m@ zM-2Y%e|ay3EB^8-vw8gQ$!oYmc!Goc{lf-&Cpf$RBWnkeFj@kyu7fmr8cZg?n>KTV-_e3s)IZP9(yrf7qis6S2mEmv#jXbjTw@yEN6nc=U@sZ*+|Z zw@mv8U2t~muib_kZ`W&=4T8IQhV=cy;%}9=H(u5eRKDt=^-&uMhDmqvXtEW)re8Bk z9oqLR;7V<7tF*?}Nu~2}5pF$<8nc}>_e^JrxaLIyRF`Mmv)UB%Jt?F+^kFAH=#E;M z@Oqf^QDmLbG<0D3tw>CN&l~i0MT-&1#@%rJFFBc_*H^bRs#7L(TQ?8#KYs(~Y1bd( zgN8=<31RFCt<>WnwEVo(EH)ya4F?xBC34#kukZaLD=zgKfhJ0&og6K*~}M#G8cC5D~zc!>I>#x82UPy;JA&$mBw9^*M( zVC1Syi(O=CS)nR8&AWJ2!zOkD)h{H5e6~{>q&9JmGM$hqDxHpm%f*ad9|(TX!3emt>8lIjRc!Dq1U`R(J~S4v0IEBZMapi zPX2HTDqcgVU_8l8=dv>P0_+7fV1|Vz#n2W#A^W!{Ft?T5B9=ZHo;A6P^Wc&6Bsc3; zXQ_j)YvUN=B1`#L_GQ9Q^XZ%D@V5{s?^)?t`DmT&&mDW;5|0O%u3m0W)G3H@wtXl} zWJ=Hdvl-`lQ+gy(BQ}=bGWe%d`#TtgVFZ-Fx7O_QuCm5Tp!N>*|W04tfxfuR6KNLyj-!f@(pwB zX>k{Z_sW;rFSC3DCxW!-8sxXDi_`~?%^v%7Z8`DEXi3&{0*X~hb0dJR#c)*qE8m^^ z2gOQSkOpPvg{PYHx85nA!iEvu%~~$RJ?_3kN2&jwZc*cTGX&6P2J32C(`DvMo8)DS zjnt<+eS?}TKgHD4Lo{=aL&NrGU2*v@5*9P-PJ7Vkm!-MwtS_8i{uIoZogXY?tlG>S zO`c11kWZK97ytMM%>+QyMlEZCkg+HAfg?llZq2Tw$acZ5gA5B?f#~CzR$4-bN{*PcZ+3`Wuo9k7_H0Sp#qeoGZZg6l|WnD!>MEHGwg07_5~#;CdbB z!WY#_z17j+5V55t#ywa-TEfiqres?C2oIryDJQEsiH@qx=l;RmSr@2Og!bLLvD4Ew zs~J+u0XIOxuc6PHpnR9iaC(0EkTv%%RKXm{a->)9`lP}NhC}nLoqSt>29AOMBcOAH zAB#jg>b`mNW@W3}-@Gde0s)8uSBBO>Sg9Z9C`k>^fvFV`iZ|@|h^4Bl*`@gaEo_zj zi`jt0gRCQ{g6!?>%?4r`nO@@zxQ>o^BGJ@LIz|8#xc|kA@BG|`IqcDS68CrvUOQ0e zO^lpBt%{O4u#^Y(d`Xb~Rmk992>w)I#YpPU9>;$oew#8VwgTi7 z1ibMOgCd0c0}KPF=0Ov`lQnvKdiWzEB32ekyW3insW-gm@^Z7QtBs9KOoB~60{Z9Q z+~))E_dvwM3i$h0Z^Ogm4-&!S6iV@fMMnZg_s?hPe?y*woCfpaCBnvB^;zh7=S~+n zPR^o*mmq!NG+HNy^`sZ@hL!Xs`1Gq>)PMj8J(w+Uyc0;x4uivvWGdUpr!oZ>?&M z>)~F%2@8Vhq~}(DG69I*)KZU?=dP&eQq`2j;{)I92^7Nxv(HoMBl{_xI>4QWx*N-#6KyMasJ7(ps z8%?D1Nk2!em=62>ARbY}2%Eo%{R=b1z87Faj(Y;nN%yO@iIZz!<;xdz2NarbM);s- zJOKJT-7*fHS$t+x;uGOT+t#+Rth#$%CEHE&S+sBEi}C`}*9T`}`#M&{#S}6UFLPJ; zq@i!2;e9r82|f++xorh_0Aj~}Lff}Xm+E02rR@$;IDiU^M<*IUq1lt{b1o{?be)Qk z2M)zJN10(Fs`7d34Yrwv8TVda-rTNF^S06PF=8&E+GmiL9#9uqVxqqW#HJd&gW7i3 zR=MS79bn5pmrhju^kj6E;RRf)&|y>L0Tl z2AyJW9gim~c|BF%zULEpcECJ_Wx6wtL}O{)S~w!-zE+v8*DW3Ugdbp`XDJznZPFNH zO`a$SKKxSk{g$qz|JbEag|rVn&IK&DA(r2lw20a%O16=rW_x4NRzh6?H)GXeG^d;& zh3CwD&y*J!Az-wvJ6yEupNc*xSsv$Oea7p~bs1Z!stzTeW$H9{AX|#et2aK1R&By>Mn6xg-uPpyyuNTrq(>K z<9?!H7HhB-;o83$vA*kO@^M7^uxAaN^e+%;Ba7GYyq6?lk*FOf1c+3T7%oqZ8c-}5 z?gZRF6VHoKfu~UJ*N-0|AG3>_Uux-2PnyW>o^^2WJHg$nmnd%W{_1gCN!OdT)?LIZ zt}*d~st=8|4G#GW5cfM43X295O$6F+#kBh5UIE@sa5YxU<1j(}M^^9g_=8b$UL)k* z^2kQxsYYrTI!|)qT9|sOL=@U!rf=gN53 zDQ}c!wIU@=-02t;*SO@1_bPab5Ptk6c>+l{u5`Li;a_W}iC-j>A6LO09O5*2z3;57 zthm?Z=GwNdA7p@|mpfan3HmiPo}YiKR>pkXZ6JEPN=xaxeT}&Wl3sHv6D{weuNzg_ zWVCT%Bn?^ji;-x$^0 zHvXyoz>e91fbD#dy^?1f`A#dapxi2D{KOE%1onAv zm-|+se(DM}6ULxuz%SOiTt^-m<@;yxMbyHqPM$gyZ?QAZf%aat*~+iq7UA(aq_)tN zA~jRD`{5xtRz`txq2JCC=x~{RYmE3x620MLxSZzwZq?BCfJ<}2EKU6FAf3{ZYNvp!*VQSGL&*+sBnEc zZCBi)>aD#XOQ;*VR;PFM?M;tKl}*b~SuUxc2BB7dkryMfcYUI6rcqvYtk~dD=zHc& zWuIbHFdTt1I@Z(BJn6x4XlZw2J301=&tpt;(u-7_Q;h-CMyu%TFHBXrGw>$o`+j^sVEU+so2w4?NkL zV~m&gU~e_54s(f$OFpc5HugNhnQvd^>MK^mfq7}x=Ql%4EA2`IbIgCr>t}XH%kg#G z@(_!*%8kv&KlcQ3fX*BxNe^V}N~GTCqUY@1x2;Pl z=dC=skJ(JW5?9d1UNu6>-@>PgzczKm`jTU}5k{PkuhdiZVp3>spFVvgY+{h8sDIDY zNPEHLYAkjNC!7_}x3?6$0*GqbQ0SE3$KiqXyLch%9Z8_dcUIocKY98^?o5*1NV#8< z6DvGBCYvMvF0&8&EQd|}du-j!(hJZhUla#iI@3KZHb?~TD~Lqf@akF zM=Na(j_cfF3y&!7dpx9o4s7;CR9&m{DuMW-Jp$2{?RX< zL2e3|{=nsn1WZ=gBFd@>qOt=^HjtM+lc3aEs&FCAdC=7r0^4#s_PC2F!2$b9-c#+@ zdNJ`TL5nS~xHt)rmd;Ut2**`1v0B^hklnwZ(|8$-n{#BbUgtH-h2D6&mX)4EqfA=$Vlhom=s#t)YyoqJxwJsIV;Jguyj!k3TD2mRZll z7QK4=Jrvewly_k66^_mVBisnBnwkn0+=Qy&5eWY!O_l&IqC<$eq@pZE6QJmlLI|z3 z*3(+j**nj-rm!gUNEM7rA-`?cb_#P9;mPm`A=?lCt|y?nHKA}ckDHoztmxZzS4oQt z&p7JoE(~&Sj($?UxcafdgG6=q$|=u-#|bEjK|6^u7zg)nb9^3O3v`kMJ{-D%0njb< zDf5!S={a06>}S?GAagAGuzg=4_HqF`#PgKigMTK!hJp5?zvo+*_2feg-=TB^cmG?P z?Jo;2%PI@39eKH`7|mke<>bDj?P#>P`P(-0B5?PNz*omv%U{Nyvw$boTAoHfUpFRoc@Fa8t`(hivVeF~YB#Jw z|{BZ4Z|{VPWL6z`W-GyXY#oAoHlD|2JE$o)%?N0;Zw_S~cO0(Bjd*+MKi zr@>_+2`f$MKqMhHCV9~1X%+ZkF@j@r0bUw8;!E&&_p`9Vk#d+Df~wI?ktR)x89&F< zM;-V0I7X@Yv-23~p;3qIn*E*7lI}&DNo%&n8r(*U-I*8S!LxtJyBQ3VYEILnN2DiE z;Fy|+1{`a`q4VW7f&~2&d2P~X(#)KR=9HnH-PeQFj#x+givv6g9_UXFGSyklX4N(y zJt#d*PhdmisKenFzn|aZeO+@gdL^^ghcK;FwWxVwTEYbSCbw(&LxKaT*t3vWd5c=m z|MnVV)TlBK4C2w{%bl76oq4fXEh&GDmQm_e52@Hjs%6PujXS$O%le;LUIGy>1TUqX8)AiU- zwC<---p3YQNl{1SJC*I+sOgxvbB1JuA4cH`ANM%?TcIpo=>AG;&XNx~!I{SX;ZqDO zEb*P3b(*Mj)QN@KuRnKxc5ZzcE`y_COj?*M*3X~w+NdLqYy)|2xhl@9>84q=KJ7VO zsScDH$S*NI**`o$ANUOtesy8U;u-14mbj=g_lYf`jnZl`6w|~VV!6U1e96o^xWI7q zl)~8%neOpZltHpWGcxVC2Xxfl^ZiLq@F~Y z);zZPrr1Jn*ojgoKaEnY_>$8FKi&|RcJ<@4t#xjh!u|1N7~2KT)HCMAN{t)8d67_0 zzPTKJAasqx*fFH)V7z0sN7aLg-PMLffA?ozwFBYSGzNYpyfRSZ_N>^|e4F9U-y$O8 zSRr$q*}Yqw92YZ&?h5~*@%yBH0+#R6P=edul}=XHW@}AS zI)?i{~4#l)t(`NYUuH^A77xsIZKjG} z=t7dS5$?*#NF~*b(?`o*=|oi;qO^W*U9F;aY+l%itA;Rr$M!Aw3ipUceHh&xoDrI- z=9BzW`|c~s92rdU>K!n^$j*dMC7EJC5L0a_uWy{Ap@`SQsOwzBNAlxQs#^LO{iieH zX2kAE$(w;Q*qv9qN_)g95(JSI+SYyQ44H*`>>C-4hqinaCu0*%& z)=*ir*?o7Rhw&2t(AfT3@IV^gc2hX$ggg|%B8b9TW3>aJNVkgwHxAKoF&!a-+jLb2 z;#WcleOujm+VZO|H|Ajpp()u0)YTI&tUsN04BP@+ckJ@L{TIEcjW{gRnS#~T&=N-j z)gev$gs>;+@HI}8o)1~#xpKT3=8Wilme1+IqfD-)6hB}2!Qa#7ZJjlbH&~lce zQ1`{&evA#@*AI;S;&ho`DKvD1wUiBVtnJp~1OHQeWA=h>Nd?Z3jlvrQU4bfC96mCM zY_Er@`f$U4`JqV}40^r(+NYWROGHBlr%H9$|9pEMMN?8-NvU?%V}Zc zXS3(9u7yRnzZ^NAt1HA|DZ$iM^rb51t2r`z=V1hoSTUNcDKV5Bp-#QKREu)5#$dOC z`A}YGviLqLY z^ZnRjY)ov&Eh{9)#a1QcaGDd7Yt$_FEz}1Z=ivCeg_sE;*)5dQ<_S+WUg(yl>S4}b zMvO^AttAtr>u2sL1A!xfD^alR=nm2LKl?%0hKJ}30Zv2+WAjz5DI$AjF=2027kvbmFfXQ2!2&KDUNll)A>RN`99qR^N(%zDV8?-y! z3|z_goJF(BBJU`AUnKlU*tt-_8pXUVgu__VLixq;;<9ihy?A%GH2E~9CN7u68vlA+>IMT=qA&jTblN>Rt9nMAE~RfhQVDt;TZfO< zo9&=lzh2s@34&B;F-4w}xw8^*N0B~*qJWYUmQ@ZTH{+7bPSowk2Cg?=99G;eZVFeV z?R|zJ{cP%VM-XkyNsp`2wkuOu9-JC%zw)`*XU*cpPy27ZyHgDedKe}%H$_Vx{#5Sa z^{Ow%{JdAsJsu>5W>_oW?)7YI8DcoOYxnk%pBiUcNWNis&r*WYE-a+7vXnnwZmVh1@h z_oGHB*HP{E8?!G(`Q6WDrtCRm$!otA>RjeAwYB4)m6YIF=Mv3IkX(9~0$F1ndt9@x z5wu*p-p38>cfLaW_OhRhQ{!cq{H17?orw;Mu+dkaZ-#7BEKzcjb#-?!eF6FOgecRm zeBo`F6zttw2$K{1a2<>~W9%0gos7VxdK~;YOnkiMMGKp`{((f8sWJ0lZO%o!9$&Mo z6e>-)Va>>^)kB=cZT9gA{^28_2hXzw2zu#)b(eGn2~iw%Qg!<>?0guGoayS^ikr|A zQLL(Em%&B=WulljS{yj)#1CkR6A3fspkJB$EuA@1b0JSWbp9m&9VGAoj8EY(Uca<3 zd=OvFI1D>>CUqvse4x^i{|;93KY#0f(EyX>+D+^d8(<*qImYGGvK2&(WGkUGcKAZVor`x`%Ld+!23@8$NT89p52KH zim>7+V|!9_;c3;IDlP9a&YsO;Ozo@<#H@V<_Bd<|iv6RykhB+Q2WN$YM%B?IpWl>1 zBNK8|7T1fcZS@@14~Lc6%iN3Ps zP|c3C##(}(AvR!2{_mQ!6GQLjS%iOu(4aPrzWl~)|J{6V0=ZjRrgv|=>OtfXErwzK zdKC2MPf0_)>y%>i^NwcCg(WjVVx`^6+-ZCxdLaWZ|Env(?$$IniAYRwopggTN9*#8 z#RV%1q~B}5ye3)Nss*&axQSL=BiXel{gm22*i5iZS8T~9h#@^{OsM7W(!1%W_$Bsx zkwK(dWH*BEYH2+~qW{=b2?Ow@Ze9GZN#lQv-*dm%)_uy{+OVh}U!5M8=vG5~L3iVd zG7=BXK0T|yFWI+!5C06xkx$<4Bp~GMSSFrKe@EkAxwE9!c6L26XB_U}!SceevnnM@ zx9na`aTtD|oa!=@@&AQ}89v#RI?e~#(GyhAT z+iIq_XG|wCnKIHn6itD9`MNc;%a|eF4;T8cQh8)*VS^87cw`vKIYwVmszXDJFhA{t zgFyXpPZtGg9RGVNU)8%qu{A4S{HRqA;5AfSX%Z1sc@Gk{iOGR$Dd-+$>aY7Wn=DQQ zWkd=4)Xd6)d4LD~EY~a_%gL6Y;F%Hczhc|uIEjYIIJu2nr7e^iRo{MCW-Lfx62P;o z-c$fV2R|OJw3Ji?v&hRU-S79f`^`H4OVwCV#E$jPBPL}NUrm%P*HN@h{b1ukmwDnE zUT1J2jOV$G(w;%Ly|hE$ZQ(K428FW}_I+FlUd!|5)8eu}F7x@NI}R#n()CziWJ(tvV)nlqZDcMJPBF|%D+>+Ez)2-j*eK&4Qx0&hs=Z*i0r@`e z7O$&e*ebq6+l0 z|3UsjH|jLfr}fVilEkL7nUD1*3{0mxN1fR5Ny0thgiY)}we3wc^%R&KMWow#tIy1} zxOa~}`NPY3e(#j3BK&_(R3vQGbK5roS>3uZl0JWDIP8X1q4%BNqjkzXb&zL8+%}C? z`^u}ZMA{hFw7<-d5J~|aBp9|kC=pRYSzz<+NL^f~zF-PlT4-Dr8jz9(_;L;QxDjb5JBO`GZcw zW4&SGY=yZ&Aylv}{)asZegx&-3`GRJUXofEgQ}L%|M&2~sq<}#g=v+QeC~gBuMNXW z=KSwh!Sj6xjNSiu&H>gYA;126cV5J^U-J0xj|1b^0`~KN&cnY=IzGWrj`AR#Zw9jJ zzWmJ&H<$&x@b7ix)H(dZe+R7mQVZ*o{9M1S^qES)xp0|qx=b6dQI=oSZ#)v}+77uI zsFgJ5FleT5vZCWN7O!+ans)6#bGttaX;=CEc~x+mvZwMLd?^zcb-W#m7*$I5c%Ie89*j&WzL| zBbR*THyI7*3pN{XDR?|M(ZgC6=P(%8P6bk$d@sC0t+5H?P;Psbi;Z+!Z&-0#VKW+4 z3&7)B1`{Gj^iyiE0ClrG3J6ilq4W;$4EdE1v)be7yu<|kk z4q>7O8V9EmE18^|RoB}M-Bif$m!K$r%Kq26>U zxO`2dopP!HMn|IGSR1O_==*GMwtbK!f3Q)GKJX~50zIsY5Ce*-?*VLp_%UUSv>!e^ zuU!1?MeIUuxd0v&eo@+D?t0f=)u21gCrSIDMOLv;@SI+oI>|q>9OX`N1nw8)*A3Q) zK2uQWILVPwVl|b(@UUqu2F-}cT?*t871tkn5~8WQy!AqX_gCjQqaj6P6{e1Lf&eM5Ih}c_goLW z|FjzNFE($9Fl*XY@>*lAe~P{TO6eH$h*Zr)+(s^BJ*iMRny2V)9 zCY7L4_rM@dr`agT@O1e5t-Ubhl$kk4XvySySda2eq)BxLwfRV6B$TuE3;YrLrKO`w z`@(m={Iw>WJQ{E^__VBWk}XZ|Nor8+O^9#q4PJAM4qXaPT+z~>w!RN7c3(7?7>2%j zlvR)F{_>i1iuXpF^3p4oA$|X?bl|F9XHW$Tv% zbASDMNqKegQd*7lAWG4$j>)2oAjWsyEzEDde3?2O-YG41MZ^uqOY@n*QA2Hm`rE10 z*JmnN2K<4CVaS`s6SFrn(&xB2PLfu7N*&qZp+Xe&(fY@FcRQ7(99L3JY0#xA3$Btz zM_TE#{WC!G4s1ofx{U<-=SJxOSQ+Xc=GG@>Y!sfm^LslY4FmJ95_7d0@61Fl8qu0% z>U!Mq%#+spC1>XQ_r3E6z*;vB9zTBZgkY zCr(2!?-sqPxCSz91!h7$+zxs0!a>HV;#L~Qxa&Qe-WSdnXcR%5J0$^HI@4LPZi1*@ zzangkP-MbUYrIz%1o-yoMC9E@Y!;Or9au7T@*wX-zx$9HPA2*Okt_c8CBVyTgP~@4 za1qLVL+Jqu_c(;$`8-7~j63@8SwwAGd3mDJB)Y88#~8uXU4i$$wsk&}CYHSOtRpGv45qFD7z=x48u&@Qn-R>VqXYm@DN${a*>um|h#-O-Pr8S9NrXbLJDXo4hZE>9w zz;Tv(7Dc5~4g>fQ%O==Fq!L_|T2QqL1Ns@#mg9olw_LNKQ0Q=_%Y^8LRt#Cr>$@1Y z0-^xP!zntNT03giz>vhF9}jaogIpMTkS2>tKbQDq*x%a5gmUc7tDk~Y`ZaEt{wcu` zQnd%+$z&oyip_N)WmyhW{8@4qp?@Tntb$=Y;yp$AhI6AnoIWXnWsM^koHHy?8#pOk zu~RsLBsv? zCsy_sZsN?VBsvD2h-g%wG=3<7Ms$Cyo3TqNGJrXDY0KzN(=2EmUrE9G*x90q!QoQ1 zr0R_wvYhF3+rGv|-a4lje=i8u>5ol^f3TEro42Cvc#bEMWB)2keW5nrft6b+*vJJ8{} z(4X=Lh8p6a+D>)jOx&jZVHx#ky1h9e z)a>7$JO#I#q&$CIm|$q`!QwPSnNs;RnpWReI~wcX=13b4nSyiO-p}XIAb5@>TUEeE zJzOKsdsa#iXpxyE`g^{WezmLF?}%J76s%K6d1LI5?L>G<4Z0dNx{5hl{a$?M@sq(} z2sPPee>b;DaCrRvK~&rN+w`P-#shA>Vf_}eoBowC?1Ccg-F%?rVAj_jZvEjaF_OL( zM%;Qsj0SFG!?+DXD^rR0*!oL~XL}DLV*6oJ>}d&t|L&*aq2-GM{tS^NJn=l)BdtPVYr6R^{1E(J842|o zfqrI@0xi^s>0Gw~U>bHVdDneMdH%Xmw+4ORGC3BrNqzfKyy2U`9_pq|@$NHs%)0qg zyWqAW#hwpLv1D2N-mF5{f_^3^oZBsobaSE{ve);XN?mY-`*hZ^Q7OAN_z05Q{UkbM^AC(M1_T&`38-VA_8tj(Df7v~~_KhjJI*^2mDNL+8$gTkLSV^x4ePtZ=-o(hNuC<~exUDJeOQbmEFb%y;GOcf!jyt=3wY33#-xkF#iL z?ZL8sA3($Ld$k0SSb>|>!~_=z0v_))%H zuaH{hYt*uskGT@n-Hqp+_FH~N4K7M-s6E)>`_L_LY`=CCCL+fdM+2-rxlOA*osHnn z@yFXQ6F$g+O$XoURD<7J-gWtLi7C7#gjA+i$~@dIwX!fk9y;HbeRTebIX0#@nS6P^ zLOx}0&33O5MnuYW;oVC-!tRhzVk2h zL5WIDr=Wpls|u3tZ|(8XxO>&TN%6_e zxcKVV)_w!3X;bi*7wWII$VV8F+i1)0j=@QW8b$8toz4*{;?0SM)11777OUf<3@Xw4 zQ+uvKfu~Yy`)wTCD(VcRtHZr_KkobJdg9mdX)FT}uISvD9fR&!IAJ7C<~#4X!jg>z^Ql+}(kcP)xo^nK><~~{BAxw$JQr58?M^X$-;}~|7N~R2|_wLWVvDjYQ`@+{$ZTr+z*Q}y_qG?nDA06R1>*3_;9a<&(V97SmvSVS($+$sdDz_K&Od*2 z2cXKVd2w%H6}AVMrw{n{{Xdb>IS?|QJ_!8&#ZM1f{`*b;qO822g&Y3~oc>=nk}NGo z0EQ{vkBS!+CaQ)sj~PCa(@ubeQ6vAsAnv6lk|jLO&!gQfyu1roFCE+4{!~H*fxI?G z9ZPRtdA=2)eeukv1l5qBcNtokkM?nB-CR`3m*xQ;S?K&~AnWW`{@*M8!|!-5V-6sM zFpLU$eg28x`WMg^oLCw2Gw4pZ^i_iU+Mv@22Hjq+8gf&P&3VG6fah{qp-M8pA_Rbx zidQ%5mwYRETdDmjAj1#=PWznBHUv4sgux03BSkb#eXD-A8wjNVFhK7ve55Aj=X9eM z=11Hi;YgDTczG>yW0-^uZOZw3t`xYyyp11@#Yy{vf>G*PpFSbKxb8*8kr?=uCNVsF zA?IAt{y|_3<&k&)oE2FN_+SSx4NnBXOP3E>;x&|k0GG`{r7oF{Z}X+!dcK9n?Z;@b z%sZbelElr>fL3UvtsT0%6bvZ?!dJuKh8$AO+}zxDHE8_+#_2ggu2@(l2wL=q*BC7} zU;6=R6HIQVPg~teLNVw$8>7FlUi#MqvNKiKe?=ICn;S@KOqB0;<-O%7!i@5UoQQ~s zn_A9O!@pO^#{)2(_o9&v?N^=wynrfFT=U#wyElnQdt{BJG^$bawW|=bFEYS3;5vU? zohK9GU$;wj+Q;m6QFbC7hrTx8j ztrcHVxmnh)+~!YZn_I<~+LBrJmskotDI-+C=&rse)1))*6d(UL(r+h7IBZ)yf?h4( zD9A=jTN-IYBQo-AX2i6bY7lf)=@QWDI!%bi#2zD}msZu;0F_xd9d zU;lK_wZE~mM@&urLn&?XfoUFk7JB+!X=<3QbzHzF!-d=lD&C|$>gaq6nl!c^ z=w-T@f9sdk(X@K6o}9XY^5=MsRJ}|^dl0OkLD_%cROeyWW`@@HYQe;ak7HB5K|ndo z2=Gn=u?Z7V&<775G@`9u%s$Q2 zzh$p@e<#avqrpl%|GcZ)L$RemsZu#c4?B_BzNHp=*2@a{VtOn=A}Hp-Xa$heWH^h>{F|2_;s!I z%*^uwTvmmeAkv_r{C&7B54I#E2JVM2-cXS0>bmA z7Cp4ur*nE*AOZQc(O^b&<7p^a!PI)6{L$ZzZ|5i9Q0D6ikPI7A$#i4RDigzQ;6qE3 zNHr5v2~XVX+Cn!=&=+MhtIoG>+!Fo`HG~fhEbJyGq}UVCZshM(BJf)0DoX%*&Sd$I z)NTHd7%m@{2ZY0*epCIOSKL_7mNtq0A)UUJ1zR@P*Sk*$wa6YLXMSFpU+dq{W~ADu zexw18bT|E8yjwxz+zaj`CUo72Q>p>k*Qse6&RZ)`nJ7`q8shiju(EAvd{gy_n<-lL z;_b;c7NWKzd%Ltki+U8?2n3qDsNI?8CIe7~wlF^Si48SxASb1t%43UcyBx4M^FVq2 zq0m&s1-aftF%uy?e9G&U&CuDtx~_-xvdzEeKSOZif^U7|3gCijXdYc`5*e+MS-(XY zEs65m*)Q!k@g!``Z@`9!Gzyd~E8v$g5BQDgl*4p+ zlu(%AoW(08gEo^@)wHG_q@&p$C)TBQOlS7QU7+h2!J{yM!X`GT5(*rf^ExRFX%K6eU8Xi`DMWLz3wQX$c8a zm z3@J3P9j=A=ZI=}SoOCy+vB0Pl1`t~e;3a#eNAW7n&GGmhU$+Lgr(I>b8UxepTZE>G zQgb1+B#;L-zA`f6ORTM*Ju1{T`vkctud>aep3sgzb;9A<7nB z>zQV~0-F&th2;-8Vbng&aPibfz(#Al z+ZS&n-ufO*T$Jb(0HhWVjpSf^WY^lGv#v z0j^+*uHv=D>cKCm@&upFn%dIw&q&^#sFuZY=2tLqyHp#_Q}^UVbZzC5NPJ343Sx7~ zQn*B-eh1A)44`K*Mj$X)PPgt%rJ^WRFIIcOR2`b)>0@%jA49B57rQbP#2}DSaJL>PuHd2b#FV{D(A_Ft+QAb1A;FFmp_345&OO1?}7yZ|82$5j6%X(d`4kN3whI(bP zdp~~sD9YUS`L4}xC^FrdHj}tKnd$6}i-u_m;F+yyrT*vG=K4)GhBi6fNVMV|^60U4 z_;AvfP`1CTP%3EorG+8BId)n4FKW;M)VwHpuE zh0hx$ZHhfo+&YhJ zK}}FWk6L(i96(KVAMOIsz4vanp^>hD)sq-U@y^woY=Li-Pq>hKJ_fg+m5d;s;*|WZ zEqf&Fx3lUN`3~obHXE1n5^nIYJN_<>#o2s5Ni(TOfi(8lBa^9y&2Gg{ zOH|RMwz2bW&t+`8E^P3IPG?6{HCS!Mex4J>ytKddgNz~ZVhuUmfLF78~4zAW+t-x_0t@z9UN zZVk3mi-@W+JmONE!rd0-i>oK;T)IjCV=wDl@sa2k%~kfEdy6N1{N$ym{_#M)Ne!T;OELcNgz7i`5EjU zknOc_2V9=1z38V6uA_60IwhKaSFF_RY%BHKC(VM14&V8k^UscC*z%Pb=FH%wCfpW& zsnKG)!MR z)wrv=Bo8CI5ZpdU{!xPucXCHCW`jy}8FLJ6gFDR5Ttt;)DQE_6Lo46cM!AMeKesr*A%rU#yFJ%GZpyOn!)!mGD z%={DKgVP#>|6|_*Cn@`5`$PV_zk^|0IXvt70$cujf1(y-{O2ncM(b#Pv~9@F$KBc7 zeIm4K>s7QfC&_hFs3vW`6{jR`1PS^$$;=y}PwJJla`q zf6#M|ZDF`X>CRA?NtCJy^=T|&N#fM_%gu6>eS*S8#+!Zpu-d=K`NR4sqs}$anLk%O z`;1(!@7v*j#AiMb{r)k~_C;1fh>{^em!2{<*q+b#RBso$HP-8fQ2abIwi0`K{SSoY zHG0GPlSh`xGZB{bLjScaR{d)`C)hZhivBD>JaABK6~t1lffL&k0;6kEIUmNeFA_F3 zyE$`CK&F-PL%_RLF2E^ti@-DQ+-!f^LK$Sa%}=MD80)$V^*28}YXc!xrobTa4GeVn zF0bU7eVO_UNXqY-#a-6qM_Emxyb>2gERx1Yk+PA?9`PG(Fd3Fu$H_};?MV2@C*!2U zYweBgZ@gT!ikW4E2;Z#s&M&^RwF|@C5heHuCz6?mJAa70DiDf!YaQ599+Vq5_DKOn zdQ_n!ErIu~7kvS#xn!mom3B(H>FLW2MQ%m>p0D@VZU{zGZT(uf^*_P(!x+cWuEy0s zJCSH|Q9f;5LLB4K`g~ioT9eZuhWW3PK{bS4>oMky-TZaLEzz@%oLN8a38!5R{6BFw zh)tiD$$(t9SqE8-^j_jfw*pAe7)|9^w-10qGx-{=PL z2L68=aWuw+(b==AX45x<*j*I_+wy))$C4#AN+3bhJ6W$I)X#oj3u)-(myr_utFgy-T?&D z53-(%>1K7la+Z4^4?f8U7}%DUEwIOifucGI`K-!91DOf#`9&r;hiM*;t@WgsEBJ0Y zhcQ$f3<<%g5=}xw!#aUu=T`)R^Bw8=Aj*j(E5H@E#Uf)wz2&-I{F`1RhpaEs-9ExAODzNoez0 zpDM#qgpNKYiFEQ0mr=)%21lN|Nl;hb6cl!?cgFW9LCopRZI8;B)0 zmVDy<&HdeK{MK$WptqR%t3qB`bfn&D2X`e;53C;PL_Sw)Kk#hF6;@|0KD!k}`QIN| znQA8?C5dbNjO?P$;^VYKMd|Eci(D@ktYghMpp7)Jn*j@!cX0L z*onBucJI|i9qOUOxqG6fl|PRe9Fe>o_e|^AWjnOE$GP{y*U>y#ueOdF&hfU}(S_p$ zp002XC01wN6&C+%a*zK%#Jzbul<)gKTv8}PiiGS`L@2wlX318OC0m$5nUN(+)aE(+@k`o_y3tAn1%;zZGy7)5*5^6lY3)rv)JbOBag9t04j`I4w zIVJXVfKM*qs4*>n>c{hoEG!KW8&FHT6mpT7xqkl-e>==ZUhqZOtz2D;Hi=(Sg`w1y zoCSE|DJy1o$En7b1e&jbffPxvxLe!m|L_jIgq+K4yAAv34RG1t(2k)dlCJX>?!j^j zIm}or4ZQ;TgBt|wg!{%faVa6wPlN{(nn0E68Mv+>v#%}9>_fZ~s& z7;Jv?eNMl*P(tVZ%Bz?dH}UDUG{UQ0=m0j;bxe^E!=yH{3kx|81sd3gr=v))S%Juz z#S^ti896>`o@)uOFP2%gG+ROoUr|fGfBV$w6}N5c?XPXD0`FO6R;F88^i*O*ZXrc5itkSI9L3<3igb_3ISZ+*_$6DA)<2{+eg+>lYe8gmjx!bJ4Q* zmrO!wPh;J8RO?>+9?}vAM)^PU2Vb)k&yq(3BZ*+fE~aVcX@Y$X8TxGrWsvJr?ozkz@TGQ_QvW#Wyy=+>0uQL zVj11iBzoh!^JR~{v36tJ8%3UBa(*-^ZgFydwDxGx+!qx8T!L#`sVS;sm;bPV6Mno2 z&a`&6+UqCg8+>a zuhOLSqUhWU6a>?=1(T+ZY2v!(6pThIME;Qv7{Yr2x1=R_bEkf>*rT7 z0XLnse9leR2dyYnzJ>cc;;o6b<_JBFK&^6nF}8<#;E1iUSVN#xX4hieG900$X5CvuwUw#-qO zR7cjUyHuEub3nqZKc7N-TF})Ro6nA9qcjhYi8)vZja5TSvO0Fn=Ckf7n|AL;v&RDm z;R^txY@{Em41_RQ{Cp!R_u|+6(eqD}x;F+3q9bU+&LiJFaSPOKC{UdF#9TqaAfyn~ zfLApafJhj=Cwdn?pCI0~P(rbKYjU>Uf0Tt47{p895?UoE4to`#v@B4cbv7H;)es5X zAF&~2lyadAdOk1OpU)Cys*jxX1>y7j3lGE5;xXa*NbYZjFK&pbDtCJLVC}qU=H_8x zTLzkUQV=3-w??AwVZ6+d4sR* z(&IVvxOO311=PxOLm_u)6JA~iTf{ST@MeoFxRCl zyZlTW?ft#jT7-}M0Bn8(vISeU^}U*{7C*{13cttSCFWjt&-sW7SNS*Q4f_K#3)}e@ zZ}^`D#cvpBz(bHV*8fE~D8VEmKNl4lPywqwNFDlr!sKDeaAH;^@c#ejBX<3NyXSuw z=l37L((KtrhlcpNSw1rz%dc!6q5}V7Znog>YXOnmjXLrvLn9-oUt2(d+uDOQ`$M%K(`z?K$tYjga?3W%xz(I7BES?-@^M)pf8Ni{5vW0NNQ1#gf;Ln_+{?G zj2B|O3Y{oyI^e**l%L= zIvdblqyhk|+sU&|UH-Y`H(U=NEHz2GY;J74L>&=j6%rGS`Dj7JA3oBedjwE6ox~8N+OH2@9LWZSCtXoRtCv;JL!+iyZ0Ll{ZS+lYwCt(m>63f@DAjtY%Tnf_e8 ziVQ17pe0x=_MFvLUmm_*&kIZ4>LTcimcT6VXXF1 zZ6S1(Cr_P`Cew>6YEDk2O~myEN#_|QsjMejLD{~+p*j@r!hRoOuhG3-pk{r?Cm}I- z`T>`-0d~LS_IQef{W##PpTR6IRW66GPt=t*)=c~3?lb}_sD&~+u|m}vuM^mJ-evBt z-`v;*on`YPRrX5I!D}Q!hJ!PMef6Q|Sd^0Xk3SvBy%(+RQlnDmBfUf|>W4Dm{rgBd z%w<_MnHN>*g%A2bZ6EWQ{BGblzyhbi!%s|L2YVQg80S)Lj4`!=Y)J_TVd_0M*5_pL zF$?WWK!TPHw_$b37}HN%17d+=gM?kTGf4>~Ks2Pjfp4#zez({6NW1H@lv6rk4Le9+ zY`-}7r8Aay@(3H!7i7*T_)9-I-`D1JR*HX-eilSTvS0r}CQI$+@wI^tPSH7$xE`7d z(_$Tyf}6=2ish~g_dFL)atnA23hv>hxqrhRXh_1xW-P;(!^E~htF+Yv$|<+G4kBpu z)^PA`D^uDfcQtg3Wnn<0oL-cWp1t4BAQC?XD2bx(~%qtH)W%p&~3@h3zsR8YPYB)~D^|jmQMS{$InOR#l z$TYdmf}nn;j#i1(Fqf$QfCY5!0Zr4KRrrHBUxw^G?2D=)#+9I!feS~9z_ap3 z(*gy?6;alF@X67>iepY>P7dg?Qn~?3)5?)tM@+@Bc5|oZg2I?Y#{}{5qyfu1m1U;e zP+=lrbxEy@{%%f|QnH^No5L>P*X}MSmhCNqZvy)I9>0)v6c)_5Dz2-jT5@k8fSFD; zV`hwJ^e8w&S{KPDZ-=A5sS?a+F*O+44M_4ZFIOoq+Ppd+!nnGuovcq%N?sPd7C|WD z^#ZW^fPRG@v&=S@2nk>Kqs`}Bs0wY;`aN@LjjAIXe(=sP7<8|C2bC*Krc}PcsrGL~3r1>xP05n5C`aebFA0xp*oK_Z?R0JH@LmQ$4I>u6i8ChJsM z3f41V1`Zg(L%7p*66)+9;?sVBEoTnauYlLc%C`IlJY7zUCNW2ny1zHLe zBOjbD2rqZ~Eh-!;0)ZVEu8{bJ;STfhtB5pdeIma)?_75Qowv(k7c)NdD6nF*-SdEI zToJ~N<3n7FhrG@^yOq)RP1YQn$DS1{pzfTjpJqRu?ocUW{7@pW`7I$?z(3~69Q4&s zS~RB~vnKl{YuoV$P5LMnKN%MsJ?BraH=(i4-_lBuEH-gB**WJV{bW}kFdKvMHHlK? zT1)SKz|sgO*0Urc_@?X!U&{vmAs)Rs!eFG0EwS{(6A&SH-r;tT{tZHI2;oXyySVR4 z3;Dmr)!OX&XiUuQJ`9kfF5#lM2Qs9O(uspg$s1W`0&mk_l^=su?#mmRU!k_GVt5YC zJQ3krp?KvS-Rs@k_peZ7J;k9ArM_1L--Y$H`9c)4l3)-FT2eDBB(_SJTCyYjAkd5H z%icQre$1V0f5uH}C{$Kj51Lu)I{PvG)pykFS|w+t?h}~YNP+qH$H=Q3O`M-pNKY&R zYm!_?A~+2)&ys_k@d&lC>4bHOw|`jD;k<**y587c>Ypri){2%dro2~gQU&VfU%=)o zg18W0)Kdss(q1eAa%v$yWAS}+go(=ZcW+Gv;6hUIWDm{dt25CYg6i!KD|8iU5FSg^^^RTMl8AIS_{ry+(HH)t;ICnKR72-rX?+(( zaePo|?A#4nSS9^VXv<#l;q0-*U!b?+hh21~$K5xreWA~#;?KYmobpPmF5NWl%??ku zyWrqC#On*Y5QR48$b#)fGUvg0$>{pZ@jwMslYPbK?zB$Xf6|FmkOApv_#QN z0gh<-K#{uis4_-jD+NIzW32v{2Yo`~Lt|q;Ho7h8`L%rg7%+YTpXnb&xSnKf6`3Po z6pyeY`RuOOsnKs`d90qk%4)+F9I8h9yB!j?do;x{M`f&)vKS3Bx8@i7x$y2N91;Rc z=ovOUU^}{eFSj{&KU;6lnvZsPR?Z%hsl?gq8gP1Olx0rFC4K%bNy(d3&U?Y(4}Y`B z{mdj7G|l_Ff1x9M`Xctbt>y_H!jS4t{3nf-H5VBkr&VDU2SAgXM=G)dKmEJzmd{3AZmNDuN7Lk#ov- zKYrz%id^_8EW*j?Ofz?Am41n-y$L>8UOO@05wyMkG(og^&!#KMzrAhtZc4q1^W9(> z2&GvR@4N(!CA9R{iTTjj;O8cJ5C&?RSN^{)aPYO>vP3P*(~MBsLR0Z$R40@AlwN&= z>=%GyHg7buUO>|*sphnas;wE%91=p~PBh+zUaF9>Xnz8X)ilM=?AH6tbngf>y|?*L8hiY>o2Qf`o@iz$dCQ0> zdAvTD%c4-4`Q4^0T?)N*DE50m&zUd2$HRPNek{Hz-*WGjmipNoJGRA2E0l60O)Xr5 zl)1oKZB&@(*gK!^GHi<>({%);s2G zE**mY-uPzpG$*JlCi`<#3ssucaWK@fxAeTDEDMhNJ3^UoqEK2Dhu(`emt;aR{no<4 ze)s(Fu7VQlEP>?hZxQgGX%1s=dg$@){rj%t8v{Wm-Mj0v$$Q@YKg7eV65*DIhkM9F zAFJrEt0BAdqxAZMIDOl{k_pwN@DfQ@<$&#bUK|b%{z%w^?LqEI@c|2~Dq39`5#31v z`Ld#aa0=Q)yfH|}tx@<~f9AiuHE9@v`II()`Pu%fSt1y@v8ZN zN;|;6hH&VDd2o-5|ASSP%30rX_cN1|qyLM9KTJY^`WX#mG9LE-f~5a<3H85)vwva) zVQ5K3L*8Hq_WyXKg#QxP{zu{n|NBRDqRiwl1^o_hDKA5OW?=i(h@H1dGr1&O(NlyJK&a1Abq+9bX*>?Z=J{d zX;0#q3P8gAWgELUSm=H_dG0)zhkK(ESB@ELjJ#Mc0D zvj^bz2eW+8cC;o^VRbtbDQaH3`KA2gltlE}-bd}Em85;d^>h7-K>9E{1KN&L5SNN)zcxrP1R-mQ>HU_h3DPg{EhpQLA;+2@6iR!Q@8dNPz7-nL?9g-AbPj5 z7D}`gXKkg*K(wL?hDod&>yvF9SS0z+&A*$DYW}jz`F0<{6kr}c%YMAqJk9B8BS)fk z|30d55a`^!>leS_7&gp<}ZWV}HP|f}-o9}l=%${SxJtyo%uU#t#y_s=8(eabw$W>Z?llm7$O{5)MVr=Xzq>@GG z%1kf~(}cG$h)N#cNad7mx*NeJy`q;{)*YQo)$Y@9aSEW2Ymn>#$9vRBWt6ux_oXc_bsHGby6E{{- ziYLct$Zk-Uf@RgP;r4f+6)ASLtYv+X4w&X(b&Su)C5R_%Fh6osEzFfaIczldc>`%A zW2k^vBbPA$G5K=u7NFWFH|oUxV}s{_&prWP#SOE}>&&>aUSZo|ZZ*4*iZD?W740L? zgEwC`?ExycmNq}w_%-+|=$AoZN72ajzf@L9K7^jr$)f&6#o8J?i936MyV+0=dOCIo z`!iC>HI@sBrJ}$3;-Ts{I}YUK+tgET=&ZLI3ju^-V+7eY>GLyn0rH`hU;T#E0@uh! z%dvkGUlTAz>XlqBj;Lzcn~Tu<5-XioWKf`|cZ8+SO5xJkNzV}_Yhr>FMK73hmz`;4 zP}C9}*QccfpRv=+szwQc<(~ERtBR+JnugXSPmEYbTH0!q*z5SHq$gUWT+0fP?II0o zsf_EZ$n&q`NEPYzoxV|ut(Uc0+*cEc!Y>Y2M?Sm3_Sd}kj;VpW0*Jo?M?ANo<;$lW zLfhRb!;Njk#9pDaaH^Kc`T^yBM32?+>SD)@_Op2@$X}t?(jc!d&%)C7X-)HJgE910 zPzXX{c;{v&yYt04nRO+O9QrTHzm}HWex3XEJh{(RjdhdyfISau#a|%b)#_eY(|Lz* zjw)w;pd*q~acZB65MNInhY!^tqaXAEx1b?&`$rIy5pjNyBLTc!3Z|W8Xwaqd>uCdOC`6^};n8-!1o3o%hN)IBu^}Y@ogN`;<7I_~^BFCL6GrM4wNV z)$GXX{lW8(s=3P5({3Mk^UtMr_&8*%8L<3yC@$$PP5`SSSKv3m8H2a;Lk^~^RVpB2 zP}=O#z$cc0VDySpOV8$2hsS@q7%q59Q9d}2?MRvP6t#p)X05*KY5Hu-lctV~53-yY z4^q6a`O}+zH146Y7!RHJgq&j=v#dXDjt}ute=+<9M$$fzQdKELoZ=-v*LRGhat!8m zy6!<&#}1UW^OiFW1JL5cr!kT|iwijW)|Yq3(S{O#g0y3x)#$>(SDhY6D-pKeDKUsZ z@vn=a*)gYcVk=repsyAa2d$~2)cUa)nLr9Tw(mb=I0OcbmEh3`sgCe$b)#`A4QQ%f zoL+3Vr17=J1OvgbL~j<(iNj=sUhY^Fffc2aQqs}fS;F)LZ5V@oa_#MZ5KRygqyMS^ z%-pP3Bl-w03^Vkz~~j6v^#LSp^1} z37GQt7d}`4v#W^+@tG}T&wa)bjo*y7vL0Bo!DJqv5pAKArpQiuBC8${9_1cm*N(GC z(PWzUZg<4XeuvrW6@Dd)o988kahkVN&QkEf$~iLbWcc}>F$Y1710Y6LS&!2qVv@Tb zX&s!OlXppz*7M;L8w5cVmg(@}s+Vdh-KZo)aHyZb)vZ9x0c2Z~R;l+T?CNUybCf;g zYZ;|BjvG>`+cHZ$E!v^^LKUeS7=kd3uJ692x028zNC%YhdnitYIDEDWcL!jnfq9(9 zy0CeL);R6gth*g>-X2;|q4C5$CH}Tf*~Wsvhg*lq4PIEgx|WG(KHk;aXG7iqnn|gj z_zi>VbMTT;9ik~cEQ3lgx?kad4JN;+)WOW$P4QOFe{0*ZC4kY@Kv!g2a?MTLT7ae5 zGB;btGTox!Cj{H}it*hSkK_Z7ngW$Rq)SZ~K5&`!J);;&nPO}Qs<_0yg%A42+Rsp? z?Y!IjeUCHH3y5Z}qPU5r)*s@?DByT;7CQwJrw@P+iluFt>av~sM1e&m!p<(gVK%N- zNq#0T&DE>V8+sjRn&(zVL6A6|&uyYe!#}dR&%u5;!YFN#`?cFYY(~< z`uuM`;cHpn8e5yG%ohg%W3D;Z&E-#^%KBpVgSjRf%g|Jfk+%q4tr11f;8*-KsC3;u(drM35sv^jSmG{<6S!)VqU9_Q6njhceKkFCAghv z>6O1W!kLmu2DhYbVem^7z5Mr(Q@88AVws}PN8J?f5zC%(_I+gyIa*ZDJUiDhTnS;N z)LZ<^&y21x7FZ5kH{hgx%kINTNmB}z2Ah*_WWYU|*D3hIvV^|m#{VTXq9s><>?|{d zHSp<3pUj^efGnTV9AnrGr|YB*zc|7x;=B-VqMJ7VexFr2>kG{tSWI~i+(vGdX?NK% zjKpW+8+YdV!2I#}Rd)Tn)Y{(R`v+%MP8!!~p%IhGU_6ws@e&}Y}wwiZ# zp{R)N=YS21Euj|9Js9TJ}F;4g7DhmsF4;x}^vc3I=ti{>z@I3z9;e?*dOG z@KOHnX$-T<|Gh==zx5m-w#mEMJv%-=9=wmpNKL(>;nnOV?<`yxF}oj4cDF2}J$w}z zr;Spk;}FJSAzlgYpu;by5^%CG0d!-p0yM_t(W6ISf1}PGdp>c0ILCQH%gXkn4@;rc>e z(6M##rf4uzYN&v1O*--ZBLJE3T_7XfyLZpr$D-2iU4wX7703%Lw?z5*Mm}Gi-8)wjbdf9} z=If-`0NY{(S%1vaicjw**2cljrbp%#ZGCa}7MwzdJ4f4_{=MlkiAhQJ!13nIwHP7D z7R66^0qj+Jd43$f3H;4bo0&of0D=~KzWK)w|JT!(kdc8`vjZSq(rr-(SMOeYF4<80 zukxD+Jd?mg{-WLfa#4c0fH@E;aiA1nQ|X1u^d}AWeOGIafc}aA0-gwN_SEG|Afql# zst;(|b6qsrhKEaeIrpbxc{L^^zxX6R5FmL+b!*mV4ocZ0)-ih0aJdVzMdd# zUrAUU9kuVU>rS2pc{vY9faP}ZKvS$d^rhNT)beHwZ#G5BBSx1ysyXzfAfpgksb>y6e1@4-i!OOBD>WA+X`#P zMZp;bcMo6#h|107sfqOW1@>K$S#_(68#n3xrPC@40YMe$mQJRorh2k8sX#f>^Y?E) z=S?jot^khJ8j;^BGr1RbR)#S>g2Ya7%6O6+O$=fhNCtxAzD~zwB zdeih`Wxx6cdmrv?l@8`vI*V6(ZP^%~nU*5vm;R{ZC9w-d$)t#p@qc_NeES{|G%%+^ zuwVtaTkG2$4p_&91BpAh=QtqKTC}M8E~=%H;u@{@w2*42#X|hQ?#i_@*ht~CJhZ!? z{QAIo;|p50BDL0Kcin=8-Xx23AznPt|CLEkvr(z}@*KqP91ZIQ^`iE3{RcTyGkodk z`+4W}R3=rc>f%&dzKYqi`kiri1YTW2c&&4a^*>Sld!hHw135{T_nIbc(gt-pWp$F3 zKNco>J^qUXz}%iGG43?!nhb#$*T5UE6HC4WlJptK^n^K882>Ns2mWR^AR9$_hA#pI zVQ&-#m9s)rUQ)9XbLA;yvfe!HQeA0+A0Oi2?l;{M_gj0LF~Szf{45n%?fED$!c7-6}Qhr5Hc!pQ1;pL za zcb;0!h48&`OlM#_7m{0C0?V_A%lpm|h&Z0Bj;!`RKsI^c#|3^v&EQs*cDiQk((DBY zjvCu|c}6MmchG5-mbvpk$5(sOq-rW!_bhs?GbrDo1ZNH$0=&_^VQ_sW?2#%8#_E2t z^F2duGQ+b8Ni>2*U8R;y_2>ugn$f4j3u()^1c>*e=iow`T`Jz9GmPe}XG$Jd ze}biRjNa;eWM?)>fc40Fu(bC=8^m$^7MBc%O*_`LrIA@HaTa$oG+vvX@Eo+B;acCJ zI@NhZ+?a+u$9KGGEQ^q=ig(k>Rfv-qV>N_TR|PezMZ)sX6>cl?GShcX7xcEE#5&Kn zM4DN}46$SkuAEbw^k%&t^ufDQ7=tw0H6<~!H0X{R7{8N45%>^b*2*Oyh?jU*LZ-yn3ZS?7X@_n) zb^pe;O|{)e5$@W)Z-2u7Ch~#PNF>KWL|%VM?z_&N+p&1X;9*;Dj*k>cGxJf=GbQ69 zt$wX|3@Y2m%72+k0e3+Jm#TM*AD7S2X>!gfSgV{*zis{nw}~i2y)-mtq z9xYH$k$jX*SF(yhr6pbYL+c@q^TU?9J$EkIIBRIdg&V;4bXc&-p(KFC+l-oUBRB!FA-{4m8?*7O{wwOD7-8EVaLSawVKr(dFV;kM0q^W_tYK>{=knH34Dz)HE>lIA1rz@zf_} z9isR1>|)W19hjNI`&NOz@pe}Xs3d*HjbJ!(Pk}PaSp%iI=hn@WUWx`7wIGRdj3644 zdS1Y;UR|r=vQX69*;02xxS;rKsn2NXYJbJhb3B_{JG0nVj}^Y|c62g~)|Gw7c09Ex zCXP=o9?^k(Moi{EgY`%@rS}ot?m`{$yfn7$FK-s(!40aRYCL(OwF`Efw59j}kdKArN-&^7y+==pgkTlDs6{WBAesU-I!3pykCyTTiDdw4* z@d$>CL}Zgc=|JEJQb}0UYhY!F+l=e>JGyfhLD^*(5On-uCKOx-`9e6f7aEtU%z5k+Pl~8*K*Lsi!@CMxih# ze;xk*eB{~Ybej_%ypLMsKQuk_4&%Dsws1Wd;wT3zKK343tL4FQclVhHUw!!X8SPfm z&nr|b=i=tdt*E%mD8G9?o^Xz!x2YCWioC<2Uvj@7S;_008&V|PrfGHJsNK{m@Z#|| zc9$u-n)yqier25=D}?deE%n^SNgEM{6D-B6a#D%moGTJ%E>GKC`n=(f}OEk zD)qnmh?Z$P#Y7T!ND3wk-d;n3D`gTk;UeqFQ-K_sS) zkNiFm61r|C>`06C@ca;%+bdsE76@-Dv#n^1;z*=Z?AY`hUbyT?lr5e8HZpDO*IPjn zW?4D&sipg6A)CPJYJO+^W64MQ0ZpSMx5tDL;*II)z9F$56|ntcp=@onqX7z${Un#% z?_qC0wmG^>Ln7aCv@O`KhE(KP`FpJq{Dl`oo6?SI#n@GUYzPwkf!6!4SAA?CN0~f( zpz!Y8m&rVxpvY@bEofB1qFZQrLpVJ2Rs%egfd?mNgT+YJ5gdALcUv$PjclN)@5MH` z9}J0cogUP&c!ou8q(Toe2|>%b{ib{Z{67Y;`+*!}uvS&6Bfv5H|JToJgGhWpFfDgC z`Je*Ny3K&iw987wTWe5rcdP&VcLGQb6%)1p13&MBE`N6Clkj*rV1`+T=9o8G{s&K^ zQC3i&)Xp7|?CrBEZ42CMLgXo|k7@x1$GeyvyXNB~;`%$Osb9@U2dfG<;MTj@J&>cl z`E^hSXv`MRg3S~_DhPrW%{5aiBK5y=*MTo?!Zt%XSV8OWj4<2TY@=6P12B3TG1XYt z#F>c2ZL)@q=Y(DzZ#q~h))K+Lai|1Gbdk?mMaPZ_@h*AYA5U+1zPfu{kVcl;o7 zhIqK$=NZc3VY0hikR?JnGK=EYc|dwLW*kB)ZD~GOGTG-q$oVP?feBz2j~E>>w(=XE z1ZiaFXfESe=|R|dd~@90p>CJtUF_SkHdKFvBDo)}uU#p#y*y&E(S)+}%L|8d`tQxg z;`wtYa$8EqHE~`Re=x%yiMzO_mJ-l0WihRxgEOG9+WzLoZv3ny$>qbey*=XiFCxHG zCnz56+3xm8O~2S^+O;H_We|htAr9EHtHtwV31;%sqhe;LnTBBpaieA0kBG04@a!%> z{9z^UboA>(^!Pm1pJ6zZQYM;IVq7}1YTRXPO6vb|1V*lEx4IXV<@;O9tdw&Fv$C?g zYzihZSPTtESG{Il?~GO-fB~+#ei>B9KzITvsHPyNvQ>Yh=nVff6{Mva&)d^=uT;= zZkpsB7ZFnl|2pMz`mI(mwpFNVS;Kn0-CYCm=uVlVlOfj+y=-0>umB!ZB8#bIU zgqpZ#coG;26e832HyYIofgB%hdkC+f0!Wpj%LZVJFx^4k45Kz0HFwkXp{;$^pwCo&>9dQCAVI?9?YcoE7SjAnX;gq zT3KgvU{s`G4Y~eSsMNvlo9j03^V-=ENe9c93-3ScU-6~$64wpPcv4i7ZSQOtO}J$E z*?8$2i`B%W_v+`*Sy!^@))V`-!{m@0U*Sl6Dvu%+r>`?n`;%^q^zTkyoVyeI)=@i? zK72r@kREUg0mygyQF6D!35x&{Ve&MK`47^P;_*`zSjOtep6aJaeK08kJb~@BhE$LG z%-gX7Aq*KDYvhG=)Pi)vbs)i-cV;%QI-7%%tgy_HD;tsSHIp7@L}4^sl&*J)H(Gw+ zAe61(!o+s3Tv^8R`Czs?Qc5-r3pP!VS-(kNMr%u+e$UxAEU2C8NO@eehy3n@8_w08 zl`WEKIJ2w%r0=hs(8tr2ddK&(@P8iJ(wK6t8ms;b-pDFH`}Xz{{`~z=l)1fSg9ifh zV$~}!CmY)bZ;+X)JbZ&8Q%XMT_TRxZd-`)>D)=)*n@-C50M1$#9F%e+hCFB38@ zg~#QdpAV$1SRJmWRS{wyx_u(>LrJ}6wM%etti^m>05Xm0s@IcvH&Y!~Q8B2Te&upHSfODrepoR{8(X*an-c&=`k5tZ+vJ~EY!iwv$W3J2)~J-KJzh?=sMOVLu+TkQWf6B}9b zX4F<$R9OGUEA;)L4=O4At*ER0tdXZ1udG3vDIegv0;!RHSmtCR@^j>q0(~LvQpcl@ z{+~)`U3AF+eiVjt%$+;n?Gf)ohJg<3iD}jYwoNXt7!4lQ>?BCgPczfY(jfyQgBqa~}4bhw5~ttfm4v;^<8A_K&}?Adtd$|gJrUBzBr z5TywMTAqS{hAP&>yi0B|4&Q8|fl9i%WXQ?5`|?fN4H{Ol$0d@G%k(AjE{S2+v57;) zCZaNhqK^U$Y8Lo$1V_I2o3VMZB}Ib8&5BB;J@vDFToFnth5C-I&ZV+oB!iL$MsIpgSksMJPT9{stAJsbUpx?00M zFi)qt0El3eSYJ8P17?*kcQ$E)+i|u_afn<{@%LPF;8QfpeTNN9XAisgh*9MJ5I>I3 zXt5S3)=Ov|G^!%F+vNGZ@MUgFj0w~X_*r!m!8d4;JSqW!2v}3Gp`sTi=`=AGm=K{##e7Yx1}W=Inp$xVu-?yAq_$jqcs;n#1~_ygX#QvJQ&kfLx)jQGJ#s8q*y=J{}BoYZ$tB;V|*`QEJ# z&X!Gssui&S<8-cBLVQ+r)?5BdEJH;lvgj&Vjxy11nZ+wLx6CCECuaNR8&?F!KQ>>c zDN|r|3Ca^yaxDwS_B?3O6;7(^&qVc$3U4HR_V(j8P~4jG`p|f^EdA`UP0Pe)vZopV zh+sGhm4>Vq{U%{&)h&%#0}zAL%~vO>@4vTTUl*h#sT}_(TVb0exwj$PAjYH$;KfH) zHcqqt1IUKF9*mkCyEkoEmANxbOrx4>sYkQc^57`xSrg&iKiVD*R{A}yJ4w!3hh8i0 ziz)V!P^3@;R@7?V==gG|m~E}_nt%Fqj>Hi1=$*l5&hJOLP9+C^jwYxT4qSeVlZlhj z-_y$Tt9+ceK9<3wx@>L50|0C=&#+UIFw7r?_a!$JD8FD1@!*;0^Gs3{<6Jra_I0pxRp8?k;=v5pMs0Pjmb33 zPVk>`Vo?y4xtaJU>g|iz1xSyW2aAsdJ zV*Ahl;GO3+pOrEK6#s_B@|UF_VYo$YV&_ge&OXZzD3ZneF~ z)ILZ1OP)&rt2)SB+XY2nJ^v#4iP|+JrXJkfe^xr;$fXZ?T+vr+e$8d7-rbRW1!^0s z$T=)}u(VC~i~%rIcWgRU=2%P87Ht~xJ6J9_hoAYr0#>TkH8a{>17fD^;!E7ke?Q+< ztbpuvz_H&hfWaPVQqP^rXnvEcQ?@UFr-WmQ*I2B4`F1^ zn~c@CiUr`?QMbe&e1=*l7gT}a3q(6;$M3G-;)V!{ya~+PMZP=P`3uCTI=D0nGQT|ztMu8fVsIdEmZc(7Nao| zee#1GZrx&&E*%`B907-0H$T9w)Rw!1%HS9m&;h-sh${FX!9Z`>sKlf{15lhvnWgl> zuDIQJJ=vo7kF0c=T;%m3`_LM1!pb_C2^y4vw6B|)hJO=Er$Ktu$s~Ojy#FdWGSC*6QtWC0^zm~hIJO%Rw%*Ym z@!v*7Mkbxx=sx$6+18|izQV9m2$|1~gzQ)mxfi=Wt>#9EJoBqjo0cWk^h!DSAaAzPry z!P9L7j0+?}%z+d(cZ4F}ukyC@W`!N7_N4T9msNV%U)ch6$kb4FS53LA*!y3PHUf?t zw1$$U8D9`zutyb(uYgtO5ihY6=(R7LCW&}P6g)iM>rhM`W7H5|cSGln7qFe?`9%WL zc7ntER}Oyh3H+FNrMJK>o3!3?eB|LTZ(TbRcrNvc993@wYG}Q8_s*#KV1CNjPDRI6 zM-{yMD%H^^jBYA^BK_sP*`gq~PnLM6EL6*KepYmb!h5Xp0DD4 zKf}YiOyXfMs1QsI#ui?+Rz+g)A_7Nbk&~;g2f9i}0LyPoP8-qaMkmadTVeZ(p7JF! zLIT&46R2_ou)U|36CafwKtG}D=H2D<`XJ~JY-c(aD?df=$H=mUM66=JDzh3(+&P6A$FY-E|UYn&y=v$Q(i z?-Nee-DfV>O6ClvDlM|cwxZY3s=SKx;d8~xDZalUl28T}YTj-h9A(f%^1Xg5Jw0j{ z#Sg`6h?>cf_U%ZCUK6GK*a=^-N+u5os}0qTqmmCK-1DGjb_wu!sR6aHp$|I4ZeV|s z3K#sU!QpP0_RPlY!t?r9vK@J7@eBGW$9tIjD4NH*K^2)wa;Q`}5eYpMhZUNTVxNhP zLRX}u&Ivq8bgQqK_ho<@$Z5jEKVRpD;v54JpS}ZgEzT(>;m%~6tkgAVLq26)toUcO z@8dfJn5$`M6Yw*j~!^b^$R<=_KorxvcP<*5FCC<){HSfl-hwyu|F z0@i4xxzmGV^gc>gV=M(ue3->XMND$9MsVQ9ns@Ksvk5O26{?pBlp*kqc(>eBL1t(# zg)qebj6*D$$>7jW($sBaLs2CM>7Jxk-Gpuw48_@nA_4V6*O+y^CK+~?=0CQySbTF~ zoKW;ZkE1|G@ZOhG^l+X}<~<7pezlZ)R<07?+RZHn?zK}F@7LrRXA~QKen#X*kV!69 z-{$TJJ~}<#(LIIJe7gx&M4I4yV9>HZ5{%IC;z&AxE;+JofWCc6ZnNr2`SoB{PgJ~z zr=7Q+s_%o#zN=d#$9Uv8A^M~)bjch_YF?u2V%mnVgpNaXY}zHP%~GGMDPpxd%fr?H z`FX>u?74#{zK8=pFYg#&W8T8RE3|%?>h^i^zzwCK#l6pu%k&+(eF5`_MPFeEb~a|z z>9?>aukm(!XqHRle=aUIzK|2K4_X?P^Ly$f>Bq&t8pah1xANfp`&D1E+N-E~be7`% zisIy%pcHqbd{YKQUni$3FKQ+W^J^JJ7hT~i%1?ohmFEoK{T-GeTtG5j^b4!|8il^i zthl6c6qeLo<(Eb@8&JQBhG0>Vz{WT)QHZe@a3Zk!SxWI^lu@(kF;;VCEh=+ozOj>5 zPW9T;)a?(9;PkAx`uN7veHRqOxO(!F#?(>ui$5E3xPKf3ZkJvq=_|&}mQGKEuyG5rIyN5+`GcLDOUxKY-Jr@A{+-x)|o!(tBkFGX5_IuANw>azXq5fY8>#ID{qMJ9NE9yO` z8LAlfCn^4pn+Gf#)?oc9jMlwJh|_7?K1FtFp*;$6Tw0RmxnEi?TH>w#_2mJKeT(R+|!1gL!ul2p_IHLF3A s0@FZkaj>8_XxtnusH#K_ Date: Sun, 5 Feb 2023 06:12:47 +0900 Subject: [PATCH 09/12] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20DB?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cha/carrotApi/domain/Category.java | 3 ++- src/main/resources/application.yml | 16 +++++++++++++++- src/main/resources/schema.sql | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/schema.sql diff --git a/src/main/java/com/cha/carrotApi/domain/Category.java b/src/main/java/com/cha/carrotApi/domain/Category.java index e90222e..eb73c95 100644 --- a/src/main/java/com/cha/carrotApi/domain/Category.java +++ b/src/main/java/com/cha/carrotApi/domain/Category.java @@ -4,6 +4,7 @@ import lombok.Setter; import javax.persistence.*; +import java.sql.Array; import java.util.ArrayList; import java.util.List; @@ -11,7 +12,7 @@ @Setter @Getter @Table(name = "CATEGORY") public class Category { - @Id @GeneratedValue + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "CATEGORY_ID") private Long id; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5897fbf..0c50977 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,6 +4,7 @@ spring: username: sa password: driver-class-name: org.h2.Driver + initialization-mode: always profiles: include: API-KEY security: @@ -18,6 +19,19 @@ spring: hibernate: # show_sql: true format_sql: true + defer-datasource-initialization: true + sql: + init: + mode: always + schema-locations: classpath:schema.sql + logging.level: org.hibernate.SQL: debug - org.hibernate.type: trace \ No newline at end of file + org.hibernate.type: trace + +server: + error: + include-exception: false + include-message: always + include-stacktrace: on_param + whitelabel.enabled: true \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..86d94e3 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,17 @@ +insert into category(category_id, name) values(0l, '디지털기기'); +insert into category(category_id, name) values(1l, '생활가전'); +insert into category(category_id, name) values(2l, '가구/인테리어'); +insert into category(category_id, name) values(3l, '유아동'); +insert into category(category_id, name) values(4l, '생활/가공식품'); +insert into category(category_id, name) values(5l, '유아도서'); +insert into category(category_id, name) values(6l, '스포츠/레저'); +insert into category(category_id, name) values(7l, '여성잡화'); +insert into category(category_id, name) values(8l, '여성의류'); +insert into category(category_id, name) values(9l, '남성패션/잡화'); +insert into category(category_id, name) values(10l, '게임/취미'); +insert into category(category_id, name) values(11l, '뷰티/미용'); +insert into category(category_id, name) values(12l, '반려동물용품'); +insert into category(category_id, name) values(13l, '도서/티켓/음반'); +insert into category(category_id, name) values(14l, '식물'); +insert into category(category_id, name) values(15l, '기타 중고물품'); +insert into category(category_id, name) values(16l, '중고차'); From 7187abd7b6a1f44129282b9b62aa1f99e9ebe1fc Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Wed, 8 Feb 2023 04:47:44 +0900 Subject: [PATCH 10/12] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../DTO/category/CategoryCreateRequest.java | 24 ++++++ .../carrotApi/DTO/category/CategoryDto.java | 29 +++++++ .../DTO/category/CategoryHelper.java | 78 +++++++++++++++++++ .../{response => exception}/BaseResponse.java | 2 +- .../ErrorResponse.java | 2 +- .../SuccessResponse.java | 2 +- .../DTO/{request => user}/LoginRequest.java | 2 +- .../DTO/{request => user}/SignUpRequest.java | 2 +- .../controller/CategoryController.java | 51 ++++++++++++ .../carrotApi/controller/UserController.java | 2 +- .../com/cha/carrotApi/domain/Category.java | 29 +++++-- .../java/com/cha/carrotApi/domain/Post.java | 6 ++ .../CannotConvertHelperException.java | 7 ++ .../exception/CategoryNotFoundException.java | 4 + .../exception/GlobalExceptionHandler.java | 2 +- .../carrotApi/jwt_security/Interceptor.java | 2 +- .../repository/CategoryRepository.java | 13 ++++ .../carrotApi/repository/PostRepository.java | 8 ++ .../com/cha/carrotApi/response/Failure.java | 10 +++ .../com/cha/carrotApi/response/Response.java | 27 +++++++ .../com/cha/carrotApi/response/Result.java | 3 + .../com/cha/carrotApi/response/Success.java | 12 +++ .../carrotApi/service/CategoryService.java | 47 +++++++++++ .../cha/carrotApi/service/UserService.java | 3 +- src/main/resources/schema.sql | 35 +++++---- 26 files changed, 369 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java rename src/main/java/com/cha/carrotApi/DTO/{response => exception}/BaseResponse.java (78%) rename src/main/java/com/cha/carrotApi/DTO/{response => exception}/ErrorResponse.java (95%) rename src/main/java/com/cha/carrotApi/DTO/{response => exception}/SuccessResponse.java (86%) rename src/main/java/com/cha/carrotApi/DTO/{request => user}/LoginRequest.java (93%) rename src/main/java/com/cha/carrotApi/DTO/{request => user}/SignUpRequest.java (97%) create mode 100644 src/main/java/com/cha/carrotApi/controller/CategoryController.java create mode 100644 src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java create mode 100644 src/main/java/com/cha/carrotApi/repository/CategoryRepository.java create mode 100644 src/main/java/com/cha/carrotApi/repository/PostRepository.java create mode 100644 src/main/java/com/cha/carrotApi/response/Failure.java create mode 100644 src/main/java/com/cha/carrotApi/response/Response.java create mode 100644 src/main/java/com/cha/carrotApi/response/Result.java create mode 100644 src/main/java/com/cha/carrotApi/response/Success.java create mode 100644 src/main/java/com/cha/carrotApi/service/CategoryService.java diff --git a/build.gradle b/build.gradle index ac72340..6000719 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'io.springfox:springfox-swagger2:2.9.2' + implementation 'io.springfox:springfox-swagger-ui:2.9.2' implementation 'io.jsonwebtoken:jjwt:0.9.1' testImplementation 'junit:junit:4.13.1' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java new file mode 100644 index 0000000..721e6c3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryCreateRequest.java @@ -0,0 +1,24 @@ +package com.cha.carrotApi.DTO.category; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +@ApiModel(value = "카테고리 생성 요청 처리") +@AllArgsConstructor +@NoArgsConstructor +public class CategoryCreateRequest { + @ApiModelProperty(value = "카테고리 명", notes = "카테고리 명을 입력하세요.", required = true, example = "category 1") + @NotBlank(message = "카테고리 명을 입력하세요.") + @Size(min = 2, max = 15, message = "길이 제한은 2~15자 이내입니다.") + private String name; + + @ApiModelProperty(value = "부모 카테고리 id", notes = "부모 카테고리의 id를 입력하세요.") + private int parentId; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java new file mode 100644 index 0000000..275b307 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java @@ -0,0 +1,29 @@ +package com.cha.carrotApi.DTO.category; + +import com.cha.carrotApi.domain.Category; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Array; +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class CategoryDto { + private int id; + private String name; + private List children; + + public static List toDtoList(List categories) { + CategoryHelper helper = CategoryHelper.newInstance( + categories, + c -> new CategoryDto(c.getId(), c.getName(), new ArrayList<>()), + c -> c.getParent(), + c -> c.getId(), + d -> d.getChildren()); + return helper.convert(); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java new file mode 100644 index 0000000..62f10f4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryHelper.java @@ -0,0 +1,78 @@ +package com.cha.carrotApi.DTO.category; + +import com.cha.carrotApi.exception.CannotConvertHelperException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class CategoryHelper { + private List entities; + private Function toDto; + private Function getParent; + private Function getKey; + private Function> getChildren; + + public static CategoryHelper newInstance(List entities, Function toDto, Function getParent, + Function getKey, Function> getChildren) { + return new CategoryHelper(entities, toDto, getParent, getKey, getChildren); + } + + private CategoryHelper(List entities, Function toDto, Function getParent, Function getKey, + Function> getChildren) { + this.entities = entities; + this.toDto = toDto; + this.getParent = getParent; + this.getKey = getKey; + this.getChildren = getChildren; + } + + public List convert() { + try { + return convertInternal(); + } catch (NullPointerException e) { + throw new CannotConvertHelperException(e.getMessage()); + } + } + + private List convertInternal() { + Map map = new HashMap<>(); + List roots = new ArrayList<>(); + + for (E e : entities) { + D dto = toDto(e); + map.put(getKey(e), dto); + if (hasParent(e)) { + E parent = getParent(e); + K parentKey = getKey(parent); + D parentDto = map.get(parentKey); + getChildren(parentDto).add(dto); + } else { + roots.add(dto); + } + } + return roots; + } + + private boolean hasParent(E e) { + return getParent(e) != null; + } + + private E getParent(E e) { + return getParent.apply(e); + } + + private D toDto(E e) { + return toDto.apply(e); + } + + private K getKey(E e) { + return getKey.apply(e); + } + + private List getChildren(D d) { + return getChildren.apply(d); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java b/src/main/java/com/cha/carrotApi/DTO/exception/BaseResponse.java similarity index 78% rename from src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java rename to src/main/java/com/cha/carrotApi/DTO/exception/BaseResponse.java index 97b4765..d224107 100644 --- a/src/main/java/com/cha/carrotApi/DTO/response/BaseResponse.java +++ b/src/main/java/com/cha/carrotApi/DTO/exception/BaseResponse.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.DTO.response; +package com.cha.carrotApi.DTO.exception; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java b/src/main/java/com/cha/carrotApi/DTO/exception/ErrorResponse.java similarity index 95% rename from src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java rename to src/main/java/com/cha/carrotApi/DTO/exception/ErrorResponse.java index e02e267..4ec2d24 100644 --- a/src/main/java/com/cha/carrotApi/DTO/response/ErrorResponse.java +++ b/src/main/java/com/cha/carrotApi/DTO/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.DTO.response; +package com.cha.carrotApi.DTO.exception; import com.cha.carrotApi.exception.ErrorCode; import lombok.Builder; diff --git a/src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java b/src/main/java/com/cha/carrotApi/DTO/exception/SuccessResponse.java similarity index 86% rename from src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java rename to src/main/java/com/cha/carrotApi/DTO/exception/SuccessResponse.java index 3cdd6f8..b2352b3 100644 --- a/src/main/java/com/cha/carrotApi/DTO/response/SuccessResponse.java +++ b/src/main/java/com/cha/carrotApi/DTO/exception/SuccessResponse.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.DTO.response; +package com.cha.carrotApi.DTO.exception; import lombok.Getter; import org.springframework.lang.Nullable; diff --git a/src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java b/src/main/java/com/cha/carrotApi/DTO/user/LoginRequest.java similarity index 93% rename from src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java rename to src/main/java/com/cha/carrotApi/DTO/user/LoginRequest.java index 3ad59b4..3055fd5 100644 --- a/src/main/java/com/cha/carrotApi/DTO/request/LoginRequest.java +++ b/src/main/java/com/cha/carrotApi/DTO/user/LoginRequest.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.DTO.request; +package com.cha.carrotApi.DTO.user; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/cha/carrotApi/DTO/request/SignUpRequest.java b/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java similarity index 97% rename from src/main/java/com/cha/carrotApi/DTO/request/SignUpRequest.java rename to src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java index 59353c9..2eceea8 100644 --- a/src/main/java/com/cha/carrotApi/DTO/request/SignUpRequest.java +++ b/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.DTO.request; +package com.cha.carrotApi.DTO.user; import com.cha.carrotApi.domain.User; import com.cha.carrotApi.domain.Role; diff --git a/src/main/java/com/cha/carrotApi/controller/CategoryController.java b/src/main/java/com/cha/carrotApi/controller/CategoryController.java new file mode 100644 index 0000000..b227174 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/CategoryController.java @@ -0,0 +1,51 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.DTO.category.CategoryCreateRequest; +import com.cha.carrotApi.response.Response; +import com.cha.carrotApi.service.CategoryService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@Api(value = "Category Controller", tags = "Category") +@RequiredArgsConstructor +@RestController +public class CategoryController { + private final CategoryService categoryService; + + @ApiOperation(value = "모든 카테고리 조회", notes = "모든 카테고리를 조회합니다.") + @GetMapping("/categories") + @ResponseStatus(HttpStatus.OK) + public Response findAllCategories() { + return Response.success(categoryService.findAllCategory()); + } + + @ApiOperation(value = "카테고리 생성", notes = "카테고리를 생성합니다.") + @PostMapping("/categories") + @ResponseStatus(HttpStatus.CREATED) + public Response createCategory(@Valid @RequestBody CategoryCreateRequest req) { + categoryService.createCategory(req); + return Response.success(); + } + +// @ApiOperation(value = "카테고리 첫 생성", notes = "카테고리를 처음 생성합니다.") +// @PostMapping("/categories/start") +// @ResponseStatus(HttpStatus.CREATED) +// public Response createCategoryAtFirst() { +// categoryService.createAtFirst(); +// return Response.success(); +// } + + @ApiOperation(value = "카테고리 삭제", notes = "카테고리를 삭제합니다.") + @DeleteMapping("/categories/{id}") + @ResponseStatus(HttpStatus.OK) + public Response deleteCategory(@ApiParam(value = "카테고리 id", required = true) @PathVariable int id) { + categoryService.deleteCategory(id); + return Response.success(); + } +} diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java index bb98886..d3d2908 100644 --- a/src/main/java/com/cha/carrotApi/controller/UserController.java +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.controller; -import com.cha.carrotApi.DTO.request.SignUpRequest; +import com.cha.carrotApi.DTO.user.SignUpRequest; import com.cha.carrotApi.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/cha/carrotApi/domain/Category.java b/src/main/java/com/cha/carrotApi/domain/Category.java index eb73c95..3787abe 100644 --- a/src/main/java/com/cha/carrotApi/domain/Category.java +++ b/src/main/java/com/cha/carrotApi/domain/Category.java @@ -1,26 +1,39 @@ package com.cha.carrotApi.domain; +import lombok.AccessLevel; import lombok.Getter; -import lombok.Setter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import javax.persistence.*; -import java.sql.Array; import java.util.ArrayList; import java.util.List; @Entity -@Setter @Getter -@Table(name = "CATEGORY") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "CATEGORY_ID") - private Long id; + @Column(name = "category_id") + private int id; + @Column(length = 30, nullable = false) private String name; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + @OnDelete(action = OnDeleteAction.CASCADE) + private Category parent; + + public Category(String name, Category parent) { + this.name = name; + this.parent = parent; + } + @ManyToMany @JoinTable(name = "category_post", - joinColumns = @JoinColumn(name = "CATEGORY_ID"), - inverseJoinColumns = @JoinColumn(name = "POST_ID")) + joinColumns = @JoinColumn(name = "CATEGORY_ID"), + inverseJoinColumns = @JoinColumn(name = "POST_ID")) private List category_posts = new ArrayList<>(); } diff --git a/src/main/java/com/cha/carrotApi/domain/Post.java b/src/main/java/com/cha/carrotApi/domain/Post.java index d425932..53f862e 100644 --- a/src/main/java/com/cha/carrotApi/domain/Post.java +++ b/src/main/java/com/cha/carrotApi/domain/Post.java @@ -20,10 +20,16 @@ public class Post extends BaseTimeEntity{ @Column(name = "POST_ID") private Long id; + @Column(name = "BOARD_NUMBER") + private int bno; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "USER_ID") private User user; + @Column(name = "content") + private String content; + @Column(name = "TITLE") private String title; diff --git a/src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java b/src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java new file mode 100644 index 0000000..e5fa216 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/CannotConvertHelperException.java @@ -0,0 +1,7 @@ +package com.cha.carrotApi.exception; + +public class CannotConvertHelperException extends RuntimeException{ + public CannotConvertHelperException(String message) { + super(message); + } +} diff --git a/src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java new file mode 100644 index 0000000..bb1c6bd --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/CategoryNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class CategoryNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java b/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java index 82ab088..19e2844 100644 --- a/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/cha/carrotApi/exception/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.exception; -import com.cha.carrotApi.DTO.response.ErrorResponse; +import com.cha.carrotApi.DTO.exception.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java index e2e5edc..f0ee565 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/Interceptor.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.jwt_security; -import com.cha.carrotApi.DTO.response.SuccessResponse; +import com.cha.carrotApi.DTO.exception.SuccessResponse; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/cha/carrotApi/repository/CategoryRepository.java b/src/main/java/com/cha/carrotApi/repository/CategoryRepository.java new file mode 100644 index 0000000..bc50a71 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/CategoryRepository.java @@ -0,0 +1,13 @@ +package com.cha.carrotApi.repository; + +import com.cha.carrotApi.domain.Category; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface CategoryRepository extends JpaRepository { + + @Query("SELECT c FROM Category c LEFT JOIN c.parent p ORDER BY p.id ASC NULLS FIRST, c.id ASC") + List findAllOrderByParentId(); +} diff --git a/src/main/java/com/cha/carrotApi/repository/PostRepository.java b/src/main/java/com/cha/carrotApi/repository/PostRepository.java new file mode 100644 index 0000000..68e92e5 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/PostRepository.java @@ -0,0 +1,8 @@ +package com.cha.carrotApi.repository; + +import org.springframework.stereotype.Repository; + +@Repository +public class PostRepository { + +} diff --git a/src/main/java/com/cha/carrotApi/response/Failure.java b/src/main/java/com/cha/carrotApi/response/Failure.java new file mode 100644 index 0000000..466af20 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Failure.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Failure implements Result{ + private String msg; +} diff --git a/src/main/java/com/cha/carrotApi/response/Response.java b/src/main/java/com/cha/carrotApi/response/Response.java new file mode 100644 index 0000000..2d77959 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Response.java @@ -0,0 +1,27 @@ +package com.cha.carrotApi.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Response { + private boolean success; + private int code; + private Result result; + + public static Response success() { + return new Response(true, 0, null); + } + + public static Response success(T data) { + return new Response(true, 0, new Success<>(data)); + } + + public static Response failure(int code, String msg) { + return new Response(false, code, new Failure(msg)); + } +} diff --git a/src/main/java/com/cha/carrotApi/response/Result.java b/src/main/java/com/cha/carrotApi/response/Result.java new file mode 100644 index 0000000..82a639f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Result.java @@ -0,0 +1,3 @@ +package com.cha.carrotApi.response; +interface Result { +} diff --git a/src/main/java/com/cha/carrotApi/response/Success.java b/src/main/java/com/cha/carrotApi/response/Success.java new file mode 100644 index 0000000..99c67ac --- /dev/null +++ b/src/main/java/com/cha/carrotApi/response/Success.java @@ -0,0 +1,12 @@ +package com.cha.carrotApi.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Success implements Result { + private T data; +} diff --git a/src/main/java/com/cha/carrotApi/service/CategoryService.java b/src/main/java/com/cha/carrotApi/service/CategoryService.java new file mode 100644 index 0000000..f6bedbd --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/CategoryService.java @@ -0,0 +1,47 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.DTO.category.CategoryCreateRequest; +import com.cha.carrotApi.DTO.category.CategoryDto; +import com.cha.carrotApi.domain.Category; +import com.cha.carrotApi.exception.CategoryNotFoundException; +import com.cha.carrotApi.repository.CategoryRepository; +import com.cha.carrotApi.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CategoryService { + private final static String DEFAULT_CATEGORY = "Default"; + private final CategoryRepository categoryRepository; + + @Transactional(readOnly = true) + public List findAllCategory() { + List categories = categoryRepository.findAllOrderByParentId(); + return CategoryDto.toDtoList(categories); + } + + @Transactional + public void createAtFirst() { + Category category = new Category(DEFAULT_CATEGORY, null); + categoryRepository.save(category); + } + + @Transactional + public void createCategory(CategoryCreateRequest req) { + Category parent = Optional.ofNullable(req.getParentId()) + .map(id -> categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new)) + .orElse(null); + categoryRepository.save(new Category(req.getName(), parent)); + } + + @Transactional + public void deleteCategory(int id) { + Category category = categoryRepository.findById(id).orElseThrow(CategoryNotFoundException::new); + categoryRepository.delete(category); + } +} diff --git a/src/main/java/com/cha/carrotApi/service/UserService.java b/src/main/java/com/cha/carrotApi/service/UserService.java index d3cdc38..a260140 100644 --- a/src/main/java/com/cha/carrotApi/service/UserService.java +++ b/src/main/java/com/cha/carrotApi/service/UserService.java @@ -5,7 +5,7 @@ import com.cha.carrotApi.exception.ErrorCode; import com.cha.carrotApi.jwt_security.JwtTokenProvider; import com.cha.carrotApi.repository.UserRepository; -import com.cha.carrotApi.DTO.request.SignUpRequest; +import com.cha.carrotApi.DTO.user.SignUpRequest; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; @Service @RequiredArgsConstructor diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 86d94e3..8efff75 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,17 +1,18 @@ -insert into category(category_id, name) values(0l, '디지털기기'); -insert into category(category_id, name) values(1l, '생활가전'); -insert into category(category_id, name) values(2l, '가구/인테리어'); -insert into category(category_id, name) values(3l, '유아동'); -insert into category(category_id, name) values(4l, '생활/가공식품'); -insert into category(category_id, name) values(5l, '유아도서'); -insert into category(category_id, name) values(6l, '스포츠/레저'); -insert into category(category_id, name) values(7l, '여성잡화'); -insert into category(category_id, name) values(8l, '여성의류'); -insert into category(category_id, name) values(9l, '남성패션/잡화'); -insert into category(category_id, name) values(10l, '게임/취미'); -insert into category(category_id, name) values(11l, '뷰티/미용'); -insert into category(category_id, name) values(12l, '반려동물용품'); -insert into category(category_id, name) values(13l, '도서/티켓/음반'); -insert into category(category_id, name) values(14l, '식물'); -insert into category(category_id, name) values(15l, '기타 중고물품'); -insert into category(category_id, name) values(16l, '중고차'); +insert into category(category_id, name, parent_id) values(1,'DEFAULT_CATEGORY', null); +insert into category(category_id, name, parent_id) values(2,'디지털기기',1); +insert into category(category_id, name, parent_id) values(3,'생활가전',1); +insert into category(category_id, name, parent_id) values(4,'가구/인테리어',1); +insert into category(category_id, name, parent_id) values(5,'유아동',1); +insert into category(category_id, name, parent_id) values(6,'생활/가공식품',1); +insert into category(category_id, name, parent_id) values(7,'유아도서',1); +insert into category(category_id, name, parent_id) values(8,'스포츠/레저',1); +insert into category(category_id, name, parent_id) values(9,'여성잡화',1); +insert into category(category_id, name, parent_id) values(10,'여성의류',1); +insert into category(category_id, name, parent_id) values(11,'남성패션/잡화',1); +insert into category(category_id, name, parent_id) values(12,'게임/취미',1); +insert into category(category_id, name, parent_id) values(13,'뷰티/미용',1); +insert into category(category_id, name, parent_id) values(14,'반려동물용품',1); +insert into category(category_id, name, parent_id) values(15,'도서/티켓/음반',1); +insert into category(category_id, name, parent_id) values(16,'식물',1); +insert into category(category_id, name, parent_id) values(17,'기타 중고물품',1); +insert into category(category_id, name, parent_id) values(18,'중고차',1); \ No newline at end of file From c1a5460dcb57b3175bd80fe119224d6b03108cac Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Wed, 8 Feb 2023 04:58:58 +0900 Subject: [PATCH 11/12] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/cha/carrotApi/domain/Category.java | 1 + .../java/com/cha/carrotApi/repository/PostRepository.java | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/cha/carrotApi/domain/Category.java b/src/main/java/com/cha/carrotApi/domain/Category.java index 3787abe..86bbb5e 100644 --- a/src/main/java/com/cha/carrotApi/domain/Category.java +++ b/src/main/java/com/cha/carrotApi/domain/Category.java @@ -13,6 +13,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "CATEGORY") public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "category_id") diff --git a/src/main/java/com/cha/carrotApi/repository/PostRepository.java b/src/main/java/com/cha/carrotApi/repository/PostRepository.java index 68e92e5..e8cdaed 100644 --- a/src/main/java/com/cha/carrotApi/repository/PostRepository.java +++ b/src/main/java/com/cha/carrotApi/repository/PostRepository.java @@ -1,8 +1,7 @@ package com.cha.carrotApi.repository; -import org.springframework.stereotype.Repository; - -@Repository -public class PostRepository { +import com.cha.carrotApi.domain.Post; +import org.springframework.data.jpa.repository.JpaRepository; +public interface PostRepository extends JpaRepository { } From d65b5bc97526ff2e9a2381df508796f193ea3027 Mon Sep 17 00:00:00 2001 From: gutanbug53 Date: Sat, 11 Feb 2023 06:00:58 +0900 Subject: [PATCH 12/12] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1/=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C/=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cha/carrotApi/CarrotApiApplication.java | 1 + .../carrotApi/DTO/category/CategoryDto.java | 3 +- .../com/cha/carrotApi/DTO/post/ImageDto.java | 17 ++ .../cha/carrotApi/DTO/post/PageInfoDto.java | 24 +++ .../carrotApi/DTO/post/PostCreateRequest.java | 29 +++ .../DTO/post/PostCreateResponse.java | 15 ++ .../cha/carrotApi/DTO/post/PostFindAll.java | 19 ++ .../cha/carrotApi/DTO/post/PostListDto.java | 20 ++ .../carrotApi/DTO/post/PostReadCondition.java | 26 +++ .../carrotApi/DTO/post/PostResponseDto.java | 37 ++++ .../cha/carrotApi/DTO/post/PostSimpleDto.java | 22 +++ .../carrotApi/DTO/post/PostUpdateRequest.java | 32 ++++ .../cha/carrotApi/DTO/user/SignUpRequest.java | 5 +- .../controller/CategoryController.java | 2 +- .../carrotApi/controller/PostController.java | 104 ++++++++++ .../carrotApi/controller/UserController.java | 2 +- .../domain/{ => Category}/Category.java | 14 +- .../java/com/cha/carrotApi/domain/Post.java | 55 ------ .../com/cha/carrotApi/domain/Post/Image.java | 57 ++++++ .../carrotApi/domain/Post/InterestedPost.java | 34 ++++ .../cha/carrotApi/domain/Post/LikePost.java | 35 ++++ .../com/cha/carrotApi/domain/Post/Post.java | 139 ++++++++++++++ .../cha/carrotApi/domain/Post/PostStatus.java | 5 + .../com/cha/carrotApi/domain/PostStatus.java | 5 - .../cha/carrotApi/domain/{ => User}/Role.java | 2 +- .../cha/carrotApi/domain/{ => User}/User.java | 10 +- .../exception/FileUploadFailureException.java | 8 + .../InterestedNotFoundException.java | 4 + .../exception/PostNotFoundException.java | 4 + .../UnsupportedImageFormatException.java | 4 + .../exception/UserNotEqualsException.java | 4 + .../exception/UserNotFoundException.java | 4 + .../CustomUserDetailsService.java | 2 +- .../{ => Category}/CategoryRepository.java | 4 +- .../repository/Post/InterestedRepository.java | 15 ++ .../repository/Post/LikeRepository.java | 12 ++ .../repository/Post/PostRepository.java | 14 ++ .../carrotApi/repository/PostRepository.java | 7 - .../repository/{ => User}/UserRepository.java | 4 +- .../{ => Category}/CategoryService.java | 7 +- .../cha/carrotApi/service/FileService.java | 10 + .../carrotApi/service/LocalFileService.java | 42 ++++ .../carrotApi/service/Post/PostService.java | 180 ++++++++++++++++++ .../service/{ => User}/UserService.java | 6 +- src/main/resources/application.yml | 4 +- 45 files changed, 947 insertions(+), 102 deletions(-) create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java create mode 100644 src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java create mode 100644 src/main/java/com/cha/carrotApi/controller/PostController.java rename src/main/java/com/cha/carrotApi/domain/{ => Category}/Category.java (65%) delete mode 100644 src/main/java/com/cha/carrotApi/domain/Post.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Post/Image.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Post/LikePost.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Post/Post.java create mode 100644 src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java delete mode 100644 src/main/java/com/cha/carrotApi/domain/PostStatus.java rename src/main/java/com/cha/carrotApi/domain/{ => User}/Role.java (55%) rename src/main/java/com/cha/carrotApi/domain/{ => User}/User.java (83%) create mode 100644 src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java create mode 100644 src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java rename src/main/java/com/cha/carrotApi/repository/{ => Category}/CategoryRepository.java (79%) create mode 100644 src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java create mode 100644 src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java create mode 100644 src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java delete mode 100644 src/main/java/com/cha/carrotApi/repository/PostRepository.java rename src/main/java/com/cha/carrotApi/repository/{ => User}/UserRepository.java (81%) rename src/main/java/com/cha/carrotApi/service/{ => Category}/CategoryService.java (89%) create mode 100644 src/main/java/com/cha/carrotApi/service/FileService.java create mode 100644 src/main/java/com/cha/carrotApi/service/LocalFileService.java create mode 100644 src/main/java/com/cha/carrotApi/service/Post/PostService.java rename src/main/java/com/cha/carrotApi/service/{ => User}/UserService.java (94%) diff --git a/src/main/java/com/cha/carrotApi/CarrotApiApplication.java b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java index dc1dd44..8f5cdfd 100644 --- a/src/main/java/com/cha/carrotApi/CarrotApiApplication.java +++ b/src/main/java/com/cha/carrotApi/CarrotApiApplication.java @@ -2,6 +2,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing diff --git a/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java b/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java index 275b307..fc680b4 100644 --- a/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java +++ b/src/main/java/com/cha/carrotApi/DTO/category/CategoryDto.java @@ -1,11 +1,10 @@ package com.cha.carrotApi.DTO.category; -import com.cha.carrotApi.domain.Category; +import com.cha.carrotApi.domain.Category.Category; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.sql.Array; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java b/src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java new file mode 100644 index 0000000..c8961d5 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/ImageDto.java @@ -0,0 +1,17 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Image; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ImageDto{ + private int id; + private String originName; + private String uniqueName; + + public static ImageDto toDto(Image image) { + return new ImageDto(image.getId(), image.getOriginName(), image.getUniqueName()); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java new file mode 100644 index 0000000..c017e94 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PageInfoDto.java @@ -0,0 +1,24 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Post; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PageInfoDto { + private int totalPage; + private int nowPage; + private int numberOfElements; + private boolean isNext; + + public PageInfoDto(Page result) { + this.totalPage = result.getTotalPages(); + this.nowPage = result.getNumber(); + this.numberOfElements = result.getNumberOfElements(); + this.isNext = result.hasNext(); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java new file mode 100644 index 0000000..8aa5f2c --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateRequest.java @@ -0,0 +1,29 @@ +package com.cha.carrotApi.DTO.post; + +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiOperation(value = "게시글 생성 요청") +public class PostCreateRequest { + @ApiModelProperty(value = "게시글 제목", notes = "게시글 제목을 입력하세요.", required = true, example = "게시글 제목") + @NotBlank(message = "게시글 제목을 입력하세요.") + private String title; + + @ApiModelProperty(value = "게시글 내용", notes = "게시글 내용을 입력하세요.", required = true, example = "게시글 내용") + @NotBlank(message = "게시글 내용을 입력하세요.") + private String content; + + @ApiModelProperty(value = "이미지", notes = "이미지를 첨부해주세요.") + private List images = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java new file mode 100644 index 0000000..a456259 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostCreateResponse.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@AllArgsConstructor +@NotBlank +public class PostCreateResponse { + private Long id; + private String title; + private String content; +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java b/src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java new file mode 100644 index 0000000..d5745e1 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostFindAll.java @@ -0,0 +1,19 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PostFindAll { + private List posts; + private PageInfoDto pageInfoDto; + + public static PostFindAll toDto(List posts, PageInfoDto pageInfoDto){ + return new PostFindAll(posts, pageInfoDto); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java new file mode 100644 index 0000000..ecbbf0a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostListDto.java @@ -0,0 +1,20 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Data +@AllArgsConstructor +public class PostListDto { + private Integer totalElement; + private Integer totalPages; + private boolean hasNext; + private List postList; + + public static PostListDto toDto(Page page) { + return new PostListDto(page.getTotalPages(), (int)page.getTotalElements(), page.hasNext(), page.getContent()); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java b/src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java new file mode 100644 index 0000000..4fc7dd3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostReadCondition.java @@ -0,0 +1,26 @@ +package com.cha.carrotApi.DTO.post; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import javax.validation.constraints.PositiveOrZero; +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostReadCondition { + @NotNull(message = "페이지 번호를 입력하세요.") + @PositiveOrZero(message = "올바른 페이지 번호를 입력해주세요. (0 이상)") + private int page; + + @NotNull(message = "페이지 크기를 입력하세요.") + @Positive(message = "올바른 페이지 크기를 입력하세요. (1 이상)") + private int size; + + private List userId = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java new file mode 100644 index 0000000..6dc8ea6 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostResponseDto.java @@ -0,0 +1,37 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Post; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostResponseDto { + private Long id; + private String writer_nickname; + private String title; + private String content; + private int likeCount; + private int interested; + private List images; + private LocalDateTime createdAt; + + public static PostResponseDto toDto(Post post, String writer_nickname) { + return new PostResponseDto( + post.getId(), + writer_nickname, + post.getTitle(), + post.getContent(), + post.getLikeCount(), + post.getInterested(), + post.getImages().stream().map(i -> ImageDto.toDto(i)).collect(Collectors.toList()), + post.getCreateDate() + ); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java b/src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java new file mode 100644 index 0000000..17971cb --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostSimpleDto.java @@ -0,0 +1,22 @@ +package com.cha.carrotApi.DTO.post; + +import com.cha.carrotApi.domain.Post.Post; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostSimpleDto { + private Long id; + private String title; + private String content; + private int liked; + private int interested; + + public PostSimpleDto toDto(Post post) { + return new PostSimpleDto(post.getId(), post.getTitle(), post.getUser().getNickname(), post.getLikeCount(), + post.getInterested()); + } +} diff --git a/src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java b/src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java new file mode 100644 index 0000000..19e1b98 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/DTO/post/PostUpdateRequest.java @@ -0,0 +1,32 @@ +package com.cha.carrotApi.DTO.post; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; + +@ApiModel(value = "게시글 수정") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PostUpdateRequest { + @ApiModelProperty(value = "게시글 제목", notes = "게시글 제목을 입력해주세요.") + @NotBlank(message = "게시글 제목을 입력해주세요.") + private String title; + + @ApiModelProperty(value = "게시글 내용", notes = "게시글 내용을 입력해주세요.") + @NotBlank(message = "게시글 내용을 입력해주세요.") + private String content; + + @ApiModelProperty(value = "추가된 이미지", notes = "추가된 이미지를 첨부해주세요.") + private List addedImages = new ArrayList<>(); + + @ApiModelProperty(value = "제거된 이미지 아이디", notes = "제거된 이미지 아이디를 입력해주세요.") + private List deletedImages = new ArrayList<>(); +} diff --git a/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java b/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java index 2eceea8..ed43a8d 100644 --- a/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java +++ b/src/main/java/com/cha/carrotApi/DTO/user/SignUpRequest.java @@ -1,12 +1,11 @@ package com.cha.carrotApi.DTO.user; -import com.cha.carrotApi.domain.User; -import com.cha.carrotApi.domain.Role; +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.domain.User.Role; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.Getter; -import org.hibernate.validator.constraints.Range; import javax.validation.constraints.*; diff --git a/src/main/java/com/cha/carrotApi/controller/CategoryController.java b/src/main/java/com/cha/carrotApi/controller/CategoryController.java index b227174..058ba20 100644 --- a/src/main/java/com/cha/carrotApi/controller/CategoryController.java +++ b/src/main/java/com/cha/carrotApi/controller/CategoryController.java @@ -2,7 +2,7 @@ import com.cha.carrotApi.DTO.category.CategoryCreateRequest; import com.cha.carrotApi.response.Response; -import com.cha.carrotApi.service.CategoryService; +import com.cha.carrotApi.service.Category.CategoryService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; diff --git a/src/main/java/com/cha/carrotApi/controller/PostController.java b/src/main/java/com/cha/carrotApi/controller/PostController.java new file mode 100644 index 0000000..29ae5b9 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/controller/PostController.java @@ -0,0 +1,104 @@ +package com.cha.carrotApi.controller; + +import com.cha.carrotApi.DTO.post.PostCreateRequest; +import com.cha.carrotApi.DTO.post.PostUpdateRequest; +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.exception.UserNotFoundException; +import com.cha.carrotApi.repository.User.UserRepository; +import com.cha.carrotApi.response.Response; +import com.cha.carrotApi.service.Post.PostService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@Slf4j +@Api(value = "Post Controller", tags = "Post") +@RequiredArgsConstructor +public class PostController { + private final PostService postService; + private final UserRepository userRepository; + + @ApiOperation(value = "글 생성", notes = "글을 작성합니다.") + @PostMapping("/posts") + @ResponseStatus(HttpStatus.CREATED) + public Response createBoard(@Valid @ModelAttribute PostCreateRequest req, + @RequestParam(value = "category", defaultValue = "1") int categoryId) { + // http://localhost:8080/boards?category=3 + User user = getPrincipal(); + return Response.success(postService.createPost(req, categoryId, user)); + } + + @ApiOperation(value = "게시글 목록 조회", notes = "게시글 목록을 조회합니다.") + @GetMapping("/posts/all/{categoryId}") + @ResponseStatus(HttpStatus.OK) + public Response findAllPosts(@ApiParam(value = "카테고리 id", required = true) @PathVariable int categoryId, @RequestParam(defaultValue = "0") Integer page) { + // http://localhost:8080/boards/all/{categoryId}?page=0 + return Response.success(postService.findAllPosts(page, categoryId)); + } + + @ApiOperation(value = "게시글 수정", notes = "게시글을 수정합니다.") + @PutMapping("/posts/{id}") + @ResponseStatus(HttpStatus.OK) + public Response editBoard(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id, + @Valid @ModelAttribute PostUpdateRequest req) { + User user = getPrincipal(); + return Response.success(postService.editPost(id, req, user)); + } + + @ApiOperation(value = "게시글 좋아요", notes = "사용자가 게시물 좋아요를 누릅니다.") + @PostMapping("/posts/{id}") + @ResponseStatus(HttpStatus.OK) + public Response likeBoard(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) { + User user = getPrincipal(); + return Response.success(postService.updateLikeOfPost(id, user)); + } + + @ApiOperation(value = "게시글 관심품목", notes = "사용자가 게시물을 관심품목에 등록합니다.") + @PostMapping("posts/{id}/favorites") + @ResponseStatus(HttpStatus.OK) + public Response favoritePost(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) { + User user = getPrincipal(); + return Response.success(postService.updateInterestedPost(id, user)); + } + + @ApiOperation(value = "게시글 삭제", notes = "게시글을 삭제합니다.") + @DeleteMapping("/posts/{id}") + @ResponseStatus(HttpStatus.OK) + public Response deleteBoard(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) { + User user = getPrincipal(); + postService.deletePost(id, user); + return Response.success(); + } + + @ApiOperation(value = "게시글 검색", notes = "게시글을 검색합니다.") + @GetMapping("/posts/search") + @ResponseStatus(HttpStatus.OK) + public Response searchPost(String keyword, + @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC)Pageable pageable) { + // ex) http://localhost:8080/api/boards/search?page=0 + return Response.success(postService.searchPost(keyword, pageable)); + } + + + + + private User getPrincipal() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user = userRepository.findByNickname(authentication.getName()) + .orElseThrow(UserNotFoundException::new); + return user; + } +} diff --git a/src/main/java/com/cha/carrotApi/controller/UserController.java b/src/main/java/com/cha/carrotApi/controller/UserController.java index d3d2908..b033095 100644 --- a/src/main/java/com/cha/carrotApi/controller/UserController.java +++ b/src/main/java/com/cha/carrotApi/controller/UserController.java @@ -1,7 +1,7 @@ package com.cha.carrotApi.controller; import com.cha.carrotApi.DTO.user.SignUpRequest; -import com.cha.carrotApi.service.UserService; +import com.cha.carrotApi.service.User.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/cha/carrotApi/domain/Category.java b/src/main/java/com/cha/carrotApi/domain/Category/Category.java similarity index 65% rename from src/main/java/com/cha/carrotApi/domain/Category.java rename to src/main/java/com/cha/carrotApi/domain/Category/Category.java index 86bbb5e..6edeefe 100644 --- a/src/main/java/com/cha/carrotApi/domain/Category.java +++ b/src/main/java/com/cha/carrotApi/domain/Category/Category.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.domain; +package com.cha.carrotApi.domain.Category; import lombok.AccessLevel; import lombok.Getter; @@ -7,15 +7,14 @@ import org.hibernate.annotations.OnDeleteAction; import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "CATEGORY") public class Category { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "category_id") private int id; @@ -31,10 +30,5 @@ public Category(String name, Category parent) { this.name = name; this.parent = parent; } - - @ManyToMany - @JoinTable(name = "category_post", - joinColumns = @JoinColumn(name = "CATEGORY_ID"), - inverseJoinColumns = @JoinColumn(name = "POST_ID")) - private List category_posts = new ArrayList<>(); } + diff --git a/src/main/java/com/cha/carrotApi/domain/Post.java b/src/main/java/com/cha/carrotApi/domain/Post.java deleted file mode 100644 index 53f862e..0000000 --- a/src/main/java/com/cha/carrotApi/domain/Post.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.cha.carrotApi.domain; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import javax.persistence.*; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Getter @Setter -@AllArgsConstructor -@NoArgsConstructor -@Entity -@Table(name = "POST") -public class Post extends BaseTimeEntity{ - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "POST_ID") - private Long id; - - @Column(name = "BOARD_NUMBER") - private int bno; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "USER_ID") - private User user; - - @Column(name = "content") - private String content; - - @Column(name = "TITLE") - private String title; - - @Column(name = "LIKE_COUNT") - private int likeCount; - - @Column(name = "POST_STATUS") - @Enumerated(EnumType.STRING) - private PostStatus postStatus; - - @Column(name = "CATEGORY") - @ManyToMany(mappedBy = "category_posts") - private List categories = new ArrayList<>(); - - @ManyToMany - @JoinTable(name = "INTEREST_POST", - joinColumns = @JoinColumn(name = "POST_ID"), - inverseJoinColumns = @JoinColumn(name = "USER_ID")) - private List interest_posts = new ArrayList<>(); - - @Column(name = "IMAGE") - private String image; -} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/Image.java b/src/main/java/com/cha/carrotApi/domain/Post/Image.java new file mode 100644 index 0000000..1361e16 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/Image.java @@ -0,0 +1,57 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.exception.UnsupportedImageFormatException; +import lombok.*; + +import javax.persistence.*; +import java.util.Arrays; +import java.util.UUID; + +@Entity +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Image extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Column(nullable = false) + private String uniqueName; + + @Column(nullable = false) + private String originName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "POST_ID", nullable = false) + private Post post; + + private final static String supportedExtension[] = {"jpg", "jpeg", "gif", "bmp", "png"}; + + public Image(String originName) { + this.originName = originName; + this.uniqueName = generateUniqueName(extractExtension(originName)); + } + + public void initPost(Post post) { + if(this.post == null){ + this.post = post; + } + } + + private String generateUniqueName(String extension) { + return UUID.randomUUID().toString() + "." + extension; + } + + private String extractExtension(String originName) { + try { + String ext = originName.substring(originName.lastIndexOf(".") + 1); + if(isSupportedFormat(ext)) return ext; + } catch (StringIndexOutOfBoundsException e) { } + throw new UnsupportedImageFormatException(); + } + + private boolean isSupportedFormat(String ext) { + return Arrays.stream(supportedExtension).anyMatch(e -> e.equalsIgnoreCase(ext)); + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java b/src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java new file mode 100644 index 0000000..cbcfcdf --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/InterestedPost.java @@ -0,0 +1,34 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.User.User; +import lombok.*; + +import javax.persistence.*; + +@Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Entity +public class InterestedPost extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "POST_ID", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID", nullable = false) + private User user; + + @Column(nullable = false) + private boolean status; + + public InterestedPost(Post post, User user) { + this.post = post; + this.user = user; + this.status = true; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/LikePost.java b/src/main/java/com/cha/carrotApi/domain/Post/LikePost.java new file mode 100644 index 0000000..900b57f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/LikePost.java @@ -0,0 +1,35 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.User.User; +import lombok.*; + +import javax.persistence.*; + +@Entity +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter @Setter +@Builder +public class LikePost extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "POST_ID", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID", nullable = false) + private User user; + + @Column(nullable = false) + private boolean status; + //true = 좋아요 + + public LikePost(Post post, User user) { + this.post = post; + this.user = user; + this.status = true; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/Post.java b/src/main/java/com/cha/carrotApi/domain/Post/Post.java new file mode 100644 index 0000000..e6f8387 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/Post.java @@ -0,0 +1,139 @@ +package com.cha.carrotApi.domain.Post; + +import com.cha.carrotApi.DTO.post.PostUpdateRequest; +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.Category.Category; +import com.cha.carrotApi.domain.User.User; +import lombok.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.*; + +@Getter @Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "POST") +public class Post extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "POST_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "USER_ID") + private User user; + + @Column(name = "TITLE") + private String title; + + @Column(name = "content") + @Lob + private String content; + + @Column(name = "LIKE_COUNT") + private int likeCount; + + @Column(name = "INTEREST_COUNT") + private int interested; + + @Column(name = "POST_STATUS") + @Enumerated(EnumType.STRING) + private PostStatus postStatus; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id", nullable = false) + private Category category; + +// @ManyToMany +// @JoinTable(name = "INTEREST_POST", +// joinColumns = @JoinColumn(name = "POST_ID"), +// inverseJoinColumns = @JoinColumn(name = "USER_ID")) +// private List interest_posts = new ArrayList<>(); + + @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, orphanRemoval = true) + private List images; + + public Post(User user, String title, String content, Category category, List images) { + this.user = user; + this.title = title; + this.content = content; + this.likeCount = 0; + this.interested = 0; + this.postStatus = PostStatus.ITEM_SELLING; + this.category = category; + this.images = new ArrayList<>(); + addImages(images); + } + + + public ImageUpdatedResult update(PostUpdateRequest req) { + this.title = title; + this.content = content; + ImageUpdatedResult result = findImageUpdatedResult(req.getAddedImages(), req.getDeletedImages()); + addImages(result.getAddedImages()); + deleteImages(result.getDeletedImages()); + return result; + } + + private void deleteImages(List deleted) { + deleted.stream().forEach(di -> this.images.remove(di)); + } + + private void addImages(List added) { + added.stream().forEach(i -> { + images.add(i); + i.initPost(this); + }); + } + + private ImageUpdatedResult findImageUpdatedResult(List addedImagesFiles, List deletedImagesIds) { + List addedImages = convertImageFilesToImages(addedImagesFiles); + List deletedImages = convertImageIdsToImages(deletedImagesIds); + return new ImageUpdatedResult(addedImagesFiles, addedImages, deletedImages); + } + + private List convertImageIdsToImages(List imageIds) { + return imageIds.stream() + .map(id -> convertImageIdToImages(id)) + .filter(i -> i.isPresent()) + .map(i -> i.get()) + .collect(toList()); + } + + private Optional convertImageIdToImages(int id) { + return this.images.stream().filter(i -> i.getId() == (id)).findAny(); + } + + + private List convertImageFilesToImages(List imageFiles) { + return imageFiles.stream().map(imageFile -> new Image(imageFile.getOriginalFilename())).collect(toList()); + } + + @Getter + @AllArgsConstructor + public static class ImageUpdatedResult { + private List addedImageFiles; + private List addedImages; + private List deletedImages; + } + + public void increaseLikeCount() { + this.likeCount += 1; + } + public void decreaseLikeCount() { + this.likeCount -= 1; + } + + public void increaseInterested() { + this.interested += 1; + } + public void decreaseInterested() { + this.interested -= 1; + } +} diff --git a/src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java b/src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java new file mode 100644 index 0000000..2c12792 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/domain/Post/PostStatus.java @@ -0,0 +1,5 @@ +package com.cha.carrotApi.domain.Post; + +public enum PostStatus { + ITEM_SELLING, ITEM_RESERVED, ITEM_SOLD +} diff --git a/src/main/java/com/cha/carrotApi/domain/PostStatus.java b/src/main/java/com/cha/carrotApi/domain/PostStatus.java deleted file mode 100644 index 83c9d88..0000000 --- a/src/main/java/com/cha/carrotApi/domain/PostStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.cha.carrotApi.domain; - -public enum PostStatus { - ITEM_RESERVED, ITEM_SOLD -} diff --git a/src/main/java/com/cha/carrotApi/domain/Role.java b/src/main/java/com/cha/carrotApi/domain/User/Role.java similarity index 55% rename from src/main/java/com/cha/carrotApi/domain/Role.java rename to src/main/java/com/cha/carrotApi/domain/User/Role.java index d09ca3b..ab862c0 100644 --- a/src/main/java/com/cha/carrotApi/domain/Role.java +++ b/src/main/java/com/cha/carrotApi/domain/User/Role.java @@ -1,4 +1,4 @@ -package com.cha.carrotApi.domain; +package com.cha.carrotApi.domain.User; public enum Role { USER, MANAGER, ADMIN; diff --git a/src/main/java/com/cha/carrotApi/domain/User.java b/src/main/java/com/cha/carrotApi/domain/User/User.java similarity index 83% rename from src/main/java/com/cha/carrotApi/domain/User.java rename to src/main/java/com/cha/carrotApi/domain/User/User.java index 0c655db..b816d97 100644 --- a/src/main/java/com/cha/carrotApi/domain/User.java +++ b/src/main/java/com/cha/carrotApi/domain/User/User.java @@ -1,5 +1,7 @@ -package com.cha.carrotApi.domain; +package com.cha.carrotApi.domain.User; +import com.cha.carrotApi.domain.BaseTimeEntity; +import com.cha.carrotApi.domain.Post.Post; import lombok.*; import org.springframework.security.crypto.password.PasswordEncoder; @@ -39,9 +41,9 @@ public class User extends BaseTimeEntity { @OneToMany(mappedBy = "user") private List posts = new ArrayList<>(); - @ManyToMany(mappedBy = "interest_posts") - @Column(name = "INTEREST_POST") - private List interests = new ArrayList<>(); +// @ManyToMany(mappedBy = "interest_posts") +// @Column(name = "INTEREST_POST") +// private List interests = new ArrayList<>(); @Builder private User(String email, String nickname, String password, String phone_number) { diff --git a/src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java b/src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java new file mode 100644 index 0000000..24b2bc8 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/FileUploadFailureException.java @@ -0,0 +1,8 @@ +package com.cha.carrotApi.exception; + +import java.io.IOException; + +public class FileUploadFailureException extends RuntimeException{ + public FileUploadFailureException(IOException e) { + } +} diff --git a/src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java new file mode 100644 index 0000000..539db87 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/InterestedNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class InterestedNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java new file mode 100644 index 0000000..5d4bbc7 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/PostNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class PostNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java b/src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java new file mode 100644 index 0000000..80a8de4 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/UnsupportedImageFormatException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class UnsupportedImageFormatException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java b/src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java new file mode 100644 index 0000000..ca24ca3 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/UserNotEqualsException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class UserNotEqualsException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java b/src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java new file mode 100644 index 0000000..e869e49 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/exception/UserNotFoundException.java @@ -0,0 +1,4 @@ +package com.cha.carrotApi.exception; + +public class UserNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java index 84981fc..66fdb42 100644 --- a/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java +++ b/src/main/java/com/cha/carrotApi/jwt_security/CustomUserDetailsService.java @@ -1,6 +1,6 @@ package com.cha.carrotApi.jwt_security; -import com.cha.carrotApi.repository.UserRepository; +import com.cha.carrotApi.repository.User.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; diff --git a/src/main/java/com/cha/carrotApi/repository/CategoryRepository.java b/src/main/java/com/cha/carrotApi/repository/Category/CategoryRepository.java similarity index 79% rename from src/main/java/com/cha/carrotApi/repository/CategoryRepository.java rename to src/main/java/com/cha/carrotApi/repository/Category/CategoryRepository.java index bc50a71..d47a22d 100644 --- a/src/main/java/com/cha/carrotApi/repository/CategoryRepository.java +++ b/src/main/java/com/cha/carrotApi/repository/Category/CategoryRepository.java @@ -1,6 +1,6 @@ -package com.cha.carrotApi.repository; +package com.cha.carrotApi.repository.Category; -import com.cha.carrotApi.domain.Category; +import com.cha.carrotApi.domain.Category.Category; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java b/src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java new file mode 100644 index 0000000..8b00384 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Post/InterestedRepository.java @@ -0,0 +1,15 @@ +package com.cha.carrotApi.repository.Post; + +import com.cha.carrotApi.domain.Post.InterestedPost; +import com.cha.carrotApi.domain.Post.Post; +import com.cha.carrotApi.domain.User.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface InterestedRepository extends JpaRepository { + Optional findInterestedByPost(Post post); + Optional findByPostAndUser(Post post, User user); + List findAllByUser(User user); +} diff --git a/src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java b/src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java new file mode 100644 index 0000000..b66468e --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Post/LikeRepository.java @@ -0,0 +1,12 @@ +package com.cha.carrotApi.repository.Post; + +import com.cha.carrotApi.domain.Post.LikePost; +import com.cha.carrotApi.domain.Post.Post; +import com.cha.carrotApi.domain.User.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LikeRepository extends JpaRepository { + Optional findByPostAndUser(Post post, User user); +} diff --git a/src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java b/src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java new file mode 100644 index 0000000..50cb879 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/repository/Post/PostRepository.java @@ -0,0 +1,14 @@ +package com.cha.carrotApi.repository.Post; + +import com.cha.carrotApi.domain.Post.Post; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { + Page findByTitle(String keyword, Pageable pageable); + Page findAll(Pageable pageable); + Page findAllByCategoryId(Pageable pageable, int categoryId); + + Page findByTitleContaining(String keyword, Pageable pageable); +} diff --git a/src/main/java/com/cha/carrotApi/repository/PostRepository.java b/src/main/java/com/cha/carrotApi/repository/PostRepository.java deleted file mode 100644 index e8cdaed..0000000 --- a/src/main/java/com/cha/carrotApi/repository/PostRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.cha.carrotApi.repository; - -import com.cha.carrotApi.domain.Post; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PostRepository extends JpaRepository { -} diff --git a/src/main/java/com/cha/carrotApi/repository/UserRepository.java b/src/main/java/com/cha/carrotApi/repository/User/UserRepository.java similarity index 81% rename from src/main/java/com/cha/carrotApi/repository/UserRepository.java rename to src/main/java/com/cha/carrotApi/repository/User/UserRepository.java index ac661ab..cdcd1e3 100644 --- a/src/main/java/com/cha/carrotApi/repository/UserRepository.java +++ b/src/main/java/com/cha/carrotApi/repository/User/UserRepository.java @@ -1,6 +1,6 @@ -package com.cha.carrotApi.repository; +package com.cha.carrotApi.repository.User; -import com.cha.carrotApi.domain.User; +import com.cha.carrotApi.domain.User.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/cha/carrotApi/service/CategoryService.java b/src/main/java/com/cha/carrotApi/service/Category/CategoryService.java similarity index 89% rename from src/main/java/com/cha/carrotApi/service/CategoryService.java rename to src/main/java/com/cha/carrotApi/service/Category/CategoryService.java index f6bedbd..7ab78a1 100644 --- a/src/main/java/com/cha/carrotApi/service/CategoryService.java +++ b/src/main/java/com/cha/carrotApi/service/Category/CategoryService.java @@ -1,11 +1,10 @@ -package com.cha.carrotApi.service; +package com.cha.carrotApi.service.Category; import com.cha.carrotApi.DTO.category.CategoryCreateRequest; import com.cha.carrotApi.DTO.category.CategoryDto; -import com.cha.carrotApi.domain.Category; +import com.cha.carrotApi.domain.Category.Category; import com.cha.carrotApi.exception.CategoryNotFoundException; -import com.cha.carrotApi.repository.CategoryRepository; -import com.cha.carrotApi.repository.UserRepository; +import com.cha.carrotApi.repository.Category.CategoryRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/cha/carrotApi/service/FileService.java b/src/main/java/com/cha/carrotApi/service/FileService.java new file mode 100644 index 0000000..020770a --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/FileService.java @@ -0,0 +1,10 @@ +package com.cha.carrotApi.service; + +import org.springframework.context.annotation.Primary; +import org.springframework.web.multipart.MultipartFile; + +@Primary +public interface FileService { + void upload(MultipartFile file, String filename); + void delete(String filename); +} diff --git a/src/main/java/com/cha/carrotApi/service/LocalFileService.java b/src/main/java/com/cha/carrotApi/service/LocalFileService.java new file mode 100644 index 0000000..428d154 --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/LocalFileService.java @@ -0,0 +1,42 @@ +package com.cha.carrotApi.service; + +import com.cha.carrotApi.exception.FileUploadFailureException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.IOException; + +@Service +@Slf4j +@PropertySource("classpath:application.yml") +public class LocalFileService implements FileService{ + @Value("${itemImgLocation}") + private String location; + + @PostConstruct + void postConstruct() { + File dir = new File(location); + if (!dir.exists()) { + dir.mkdir(); + } + } + + @Override + public void upload(MultipartFile file, String filename) { + try { + file.transferTo(new File(location + filename)); + } catch(IOException e) { + throw new FileUploadFailureException(e); + } + } + + @Override + public void delete(String filename) { + new File(location + filename).delete(); + } +} diff --git a/src/main/java/com/cha/carrotApi/service/Post/PostService.java b/src/main/java/com/cha/carrotApi/service/Post/PostService.java new file mode 100644 index 0000000..e67703f --- /dev/null +++ b/src/main/java/com/cha/carrotApi/service/Post/PostService.java @@ -0,0 +1,180 @@ +package com.cha.carrotApi.service.Post; + +import com.cha.carrotApi.DTO.post.*; +import com.cha.carrotApi.domain.Category.Category; +import com.cha.carrotApi.domain.Post.Image; +import com.cha.carrotApi.domain.Post.InterestedPost; +import com.cha.carrotApi.domain.Post.LikePost; +import com.cha.carrotApi.domain.Post.Post; +import com.cha.carrotApi.domain.User.User; +import com.cha.carrotApi.exception.CategoryNotFoundException; +import com.cha.carrotApi.exception.InterestedNotFoundException; +import com.cha.carrotApi.exception.PostNotFoundException; +import com.cha.carrotApi.exception.UserNotEqualsException; +import com.cha.carrotApi.repository.Category.CategoryRepository; +import com.cha.carrotApi.repository.Post.InterestedRepository; +import com.cha.carrotApi.repository.Post.LikeRepository; +import com.cha.carrotApi.repository.Post.PostRepository; +import com.cha.carrotApi.service.FileService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.util.stream.Collectors.*; + +@Service +@RequiredArgsConstructor +public class PostService { + private final static String SUCCESS_LIKE_POST = "좋아요 처리"; + private final static String SUCCESS_UNLIKE_POST = "좋아요 취소"; + private final static String SUCCESS_INTEREST_POST = "관심글 처리"; + private final static String SUCCESS_NOT_INTEREST_POST = "관심글 취소"; + + private final PostRepository postRepository; + private final FileService fileService; + private final LikeRepository likeRepository; + private final InterestedRepository interestedRepository; + private final int RECOMMEND_SET_COUNT = 10; + private final CategoryRepository categoryRepository; + + //글 생성 + @Transactional + public PostCreateResponse createPost(PostCreateRequest req, int categoryId, User user) { + List images = req.getImages().stream() + .map(i -> new Image(i.getOriginalFilename())) + .collect(toList()); + Category category = categoryRepository.findById(categoryId).orElseThrow(CategoryNotFoundException::new); + Post post = postRepository.save(new Post(user, req.getTitle(), req.getContent(), category, images)); + uploadImages(post.getImages(), req.getImages()); + return new PostCreateResponse(post.getId(), post.getTitle(), post.getContent()); + } + + //전체 글 조회 + @Transactional(readOnly = true) + public PostFindAll findAllPosts(Integer page, int categoryId) { + Page posts = makePagePosts(page, categoryId); + return responsePagingPosts(posts); + } + + private PostFindAll responsePagingPosts(Page posts) { + List postSimpleDtoList = posts.stream() + .map(i -> new PostSimpleDto().toDto(i)) + .collect(toList()); + return PostFindAll.toDto(postSimpleDtoList, new PageInfoDto(posts)); + } + + private Page makePagePosts(Integer page, int categoryId) { + PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("id").descending()); + Page posts = postRepository.findAllByCategoryId(pageRequest, categoryId); + return posts; + } + + private void uploadImages(List images, List fileImages) { + IntStream.range(0, images.size()) + .forEach(i -> fileService.upload(fileImages.get(i), images.get(i).getUniqueName())); + } + + private void deleteImages(List images) { + images.forEach(i -> fileService.delete(i.getUniqueName())); + } + + @Transactional(readOnly = true) + public PostResponseDto findPost(Long id) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + User user = post.getUser(); + return PostResponseDto.toDto(post, user.getNickname()); + } + + @Transactional + public String updateLikeOfPost(Long id, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + if (!likeRepository.findByPostAndUser(post, user).isPresent()) { + post.increaseLikeCount(); + return createLikePost(post, user); + } + post.decreaseLikeCount(); + return removeLikePost(post, user); + } + @Transactional + public String updateInterestedPost(Long id, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + if (!interestedRepository.findByPostAndUser(post, user).isPresent()) { + post.increaseInterested(); + return createInterestedPost(post, user); + } + post.decreaseInterested(); + return removeInterestedPost(post, user); + } + + @Transactional + public PostResponseDto editPost(Long id, PostUpdateRequest req, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + validatePostOwner(user, post); + Post.ImageUpdatedResult result = post.update(req); + uploadImages(result.getAddedImages(), result.getAddedImageFiles()); + deleteImages(result.getDeletedImages()); + return PostResponseDto.toDto(post, user.getNickname()); + } + + @Transactional + public void deletePost(Long id, User user) { + Post post = postRepository.findById(id).orElseThrow(PostNotFoundException::new); + validatePostOwner(user, post); + postRepository.delete(post); + } + + @Transactional + public List searchPost(String keyword, Pageable pageable) { + Page posts = postRepository.findByTitleContaining(keyword, pageable); + List postSimpleDtoList = posts.stream() + .map(i -> new PostSimpleDto().toDto(i)) + .collect(toList()); + return postSimpleDtoList; + } + + + private void validatePostOwner(User user, Post post) { + if (!user.equals(post.getUser())) { + throw new UserNotEqualsException(); + } + } + + public String createLikePost(Post board, User user) { + LikePost likePost = new LikePost(board, user); // true 처리 + likeRepository.save(likePost); + return SUCCESS_LIKE_POST; + } + + private String removeLikePost(Post post, User user) { + LikePost likePost = likeRepository.findByPostAndUser(post, user).orElseThrow(() -> { + throw new IllegalArgumentException("'좋아요' 기록을 찾을 수 없습니다."); + }); + likeRepository.delete(likePost); + return SUCCESS_UNLIKE_POST; + } + + private String createInterestedPost(Post post, User user) { + InterestedPost interestedPost = new InterestedPost(post, user); + interestedRepository.save(interestedPost); + return SUCCESS_INTEREST_POST; + } + + private String removeInterestedPost(Post post, User user) { + InterestedPost interestedPost = interestedRepository.findByPostAndUser(post, user) + .orElseThrow(InterestedNotFoundException::new); + interestedRepository.delete(interestedPost); + return SUCCESS_NOT_INTEREST_POST; + } + + + +} diff --git a/src/main/java/com/cha/carrotApi/service/UserService.java b/src/main/java/com/cha/carrotApi/service/User/UserService.java similarity index 94% rename from src/main/java/com/cha/carrotApi/service/UserService.java rename to src/main/java/com/cha/carrotApi/service/User/UserService.java index a260140..80fabb8 100644 --- a/src/main/java/com/cha/carrotApi/service/UserService.java +++ b/src/main/java/com/cha/carrotApi/service/User/UserService.java @@ -1,10 +1,10 @@ -package com.cha.carrotApi.service; +package com.cha.carrotApi.service.User; -import com.cha.carrotApi.domain.User; +import com.cha.carrotApi.domain.User.User; import com.cha.carrotApi.exception.CustomException; import com.cha.carrotApi.exception.ErrorCode; import com.cha.carrotApi.jwt_security.JwtTokenProvider; -import com.cha.carrotApi.repository.UserRepository; +import com.cha.carrotApi.repository.User.UserRepository; import com.cha.carrotApi.DTO.user.SignUpRequest; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0c50977..e26d141 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,4 @@ + spring: datasource: url: jdbc:h2:tcp://localhost/~/carrotapi @@ -34,4 +35,5 @@ server: include-exception: false include-message: always include-stacktrace: on_param - whitelabel.enabled: true \ No newline at end of file + whitelabel.enabled: true +itemImgLocation: C:/Users/cha_hammin/Desktop/image \ No newline at end of file