diff --git a/build.gradle b/build.gradle index 43232a4..8617aea 100644 --- a/build.gradle +++ b/build.gradle @@ -40,9 +40,6 @@ dependencies { runtimeOnly 'com.h2database:h2' - // Jakarta Validation API 의존성 -// implementation("jakarta.validation:jakarta.validation-api") // 최신 버전 사용 권장 - // Spring Security 사용 시 필요한 의존성 implementation("org.springframework.boot:spring-boot-starter-security") @@ -58,6 +55,11 @@ dependencies { // enable production implementation ("org.springframework.boot:spring-boot-starter-actuator") + // modelmapper + implementation 'org.modelmapper:modelmapper:2.4.4' + + // swagger3 + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' } diff --git a/screenshots/Oauth2.png b/screenshots/Oauth2.png new file mode 100644 index 0000000..a825fc6 Binary files /dev/null and b/screenshots/Oauth2.png differ diff --git a/src/main/java/com/chukapoka/server/common/authority/AppConfig.java b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java new file mode 100644 index 0000000..46f442c --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/AppConfig.java @@ -0,0 +1,22 @@ +package com.chukapoka.server.common.authority; + + +import lombok.RequiredArgsConstructor; +import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class AppConfig { + @Bean + public ModelMapper modelMapper() { + ModelMapper modelMapper = new ModelMapper(); + /** 연결 전략 : 같은 타입의 필드명이 같은 경우만 동작 */ + modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE).setSkipNullEnabled(true).setFieldMatchingEnabled(true) + .setAmbiguityIgnored(true) // id속성을 매핑에서 제외 + .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE); + return modelMapper; + } +} diff --git a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java index bfb6556..713b09a 100644 --- a/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java +++ b/src/main/java/com/chukapoka/server/common/authority/SecurityConfig.java @@ -1,9 +1,11 @@ package com.chukapoka.server.common.authority; +import com.chukapoka.server.common.authority.jwt.JwtAuthenticationFilter; +import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; import com.chukapoka.server.common.enums.Authority; import com.chukapoka.server.common.repository.TokenRepository; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -11,6 +13,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -18,33 +22,37 @@ @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { /** * Spring Security 6.1.0부터는 메서드 체이닝의 사용을 지양하고 람다식을 통해 함수형으로 설정하게 지향함 */ - @Autowired - private JwtTokenProvider jwtTokenProvider; - - @Autowired - private TokenRepository tokenRepository; + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + /** rest api 설정 */ http - .httpBasic(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable) - .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests((authorizeRequests) -> { - authorizeRequests - .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber", "/api/health").anonymous() - - .requestMatchers("/api/user/logout", "api/user/reissue").hasRole(Authority.USER.getAuthority());// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 - } - + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // h2 요소를 사용 비활성화 + .httpBasic(AbstractHttpConfigurer::disable) // 기본 인증 로그인 비활성화 + .logout(AbstractHttpConfigurer::disable) // 기본 로그아웃 비활성화 + .formLogin(AbstractHttpConfigurer::disable) // 기본 로그인 비활성화 + .csrf(AbstractHttpConfigurer::disable) // csrf 비활성화 -> cookie를 사용하지 않으면 꺼도 된다. (cookie를 사용할 경우 httpOnly(XSS 방어), sameSite(CSRF 방어)로 방어해야 한다.) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션관리 정책을 STATELESS(세션이 있으면 쓰지도 않고, 없으면 만들지도 않는다) + .addFilterAfter(new JwtAuthenticationFilter(jwtTokenProvider, tokenRepository), UsernamePasswordAuthenticationFilter.class); + /** request 인증, 인가 설정 */ + http + .authorizeHttpRequests((authorizeRequests) -> { + authorizeRequests + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**","/h2-console/**").permitAll() + .requestMatchers("/api/user/emailCheck", "/api/user", "/api/user/authNumber").anonymous() + .requestMatchers("/api/user/logout", "api/user/reissue", "api/tree/**","api/treeItem/**").hasRole(Authority.USER.getAuthority()// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음 - ); - + ); + }); return http.build(); } diff --git a/src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java b/src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java new file mode 100644 index 0000000..d96a6ff --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/authority/SwaggerConfig.java @@ -0,0 +1,32 @@ +package com.chukapoka.server.common.authority; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** swagger 의존성만 설정해도 자동 적용되지만, jwt 토큰값을 확인하기 위한 설정 */ +@Configuration +public class SwaggerConfig { + + + /** SwaggerConfig의 openAPI 함수에 security schemes를 추가 + * addList 부분과 addSecuritySchemes의 이름 부분은 변경이 가능하지만 둘 다 같은 이름이어야 함 */ + @Bean + public OpenAPI openAPI(){ + return new OpenAPI().addSecurityItem(new SecurityRequirement().addList("JWT")) + .components(new Components().addSecuritySchemes("JWT", createAPIKeyScheme())) + .info(new Info().title("Chukapoka API") + .description("This is Chukapoka API") + .version("v2.2.0")); + } + /** JWT를 적용하려면 confugure에 JWT SecurityScheme이 필요 */ + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("bearer"); + } +} diff --git a/src/main/java/com/chukapoka/server/common/authority/JwtAuthenticationFilter.java b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java similarity index 95% rename from src/main/java/com/chukapoka/server/common/authority/JwtAuthenticationFilter.java rename to src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java index 2668701..ab4c7be 100644 --- a/src/main/java/com/chukapoka/server/common/authority/JwtAuthenticationFilter.java +++ b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package com.chukapoka.server.common.authority; +package com.chukapoka.server.common.authority.jwt; import com.chukapoka.server.common.entity.Token; import com.chukapoka.server.common.repository.TokenRepository; @@ -25,7 +25,6 @@ public class JwtAuthenticationFilter extends GenericFilterBean { public static final String AUTHORIZATION_HEADER = "Authorization"; public static final String BEARER_PREFIX = "Bearer"; - private final JwtTokenProvider jwtTokenProvider; private final TokenRepository tokenRepository; @@ -36,8 +35,6 @@ public class JwtAuthenticationFilter extends GenericFilterBean { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 1. Request Header 에서 토큰을 꺼냄 String accessToken = resolveToken((HttpServletRequest) request); -// String data = tokenRepository.getAccessToken(token); -// System.out.println("data = " + data); // 2. validateToken 으로 토큰 유효성 검사 // 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장 if (StringUtils.hasText(accessToken)) { diff --git a/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java similarity index 81% rename from src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java rename to src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java index d1f83d6..df5383f 100644 --- a/src/main/java/com/chukapoka/server/common/authority/JwtTokenProvider.java +++ b/src/main/java/com/chukapoka/server/common/authority/jwt/JwtTokenProvider.java @@ -1,9 +1,10 @@ -package com.chukapoka.server.common.authority; +package com.chukapoka.server.common.authority.jwt; - -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; +import com.chukapoka.server.user.entity.User; +import com.chukapoka.server.user.repository.UserRepository; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; @@ -19,8 +20,10 @@ import java.security.Key; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @@ -35,12 +38,14 @@ public class JwtTokenProvider { private static final long ACCESS_EXPIRATION_MILLISECONDS = 1000 * 60 * 30; // Refresh Token 만료 시간 상수 (7일) private static final long REFRESH_EXPIRATION_MILLISECONDS = 1000L * 60 * 60 * 24 * 7; + private UserRepository userRepository; private final Key key; // 비밀 키를 Base64 디코딩한 값으로 초기화 @Autowired - public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, UserRepository userRepository) { byte[] keyBytes = Decoders.BASE64.decode(secretKey); this.key = Keys.hmacShaKeyFor(keyBytes); + this.userRepository = userRepository; } /** @@ -63,7 +68,7 @@ public TokenDto createToken(Authentication authentication) { String accessToken = Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) // 권한 - .claim(USER_KEY, ((CustomUser) authentication.getPrincipal()).getUserId()) + .claim(USER_KEY, ((CustomUserDetails) authentication.getPrincipal()).getUserId()) .setIssuedAt(now) .setExpiration(accessTokenExpiresIn) // 토큰이 만료될시간 .signWith(key, SignatureAlgorithm.HS256) // 비밀키, 암호화 알고리즘이름 @@ -74,7 +79,7 @@ public TokenDto createToken(Authentication authentication) { String refreshToken = Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) // 권한 - .claim(USER_KEY, ((CustomUser) authentication.getPrincipal()).getUserId()) // user id + .claim(USER_KEY, ((CustomUserDetails) authentication.getPrincipal()).getUserId()) // user id .setIssuedAt(now) .setExpiration(refreshExpiration) .signWith(key, SignatureAlgorithm.HS256) @@ -83,8 +88,9 @@ public TokenDto createToken(Authentication authentication) { return TokenDto.builder() .grantType(BEARER_TYPE) .accessToken(accessToken) - .accessTokenExpiresIn(accessTokenExpiresIn.getTime()) .refreshToken(refreshToken) + .atExpiration(formatDate(accessTokenExpiresIn)) + .rtExpiration(formatDate(refreshExpiration)) .build(); } @@ -94,7 +100,6 @@ public TokenDto createToken(Authentication authentication) { /** * JWT 토큰에서 사용자 정보를 추출하여 인증 객체를 반환하는 메서드 */ - public Authentication getAuthentication(String token) { Claims claims = parseClaims(token); @@ -112,8 +117,14 @@ public Authentication getAuthentication(String token) { .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); + // 데이터베이스에서 사용자 정보 조회 + Optional userOptional = userRepository.findById(userId); + if (userOptional.isEmpty()) { + throw new RuntimeException("User not found for id: " + userId); + } + User user = userOptional.get(); // UserDetails 객체 생성 - UserDetails principal = new CustomUser(userId, claims.getSubject(), authorities); + UserDetails principal = new CustomUserDetails(user); // UsernamePasswordAuthenticationToken을 사용하여 Authentication 객체 반환 return new UsernamePasswordAuthenticationToken(principal, "", authorities); @@ -156,6 +167,11 @@ public boolean isTokenExpired(String token) { Date expirationDate = claims.getExpiration(); return expirationDate != null && expirationDate.before(new Date()); } + /** 토큰 만료기한 날짜 포맷메서드 */ + private String formatDate(Date date) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); + return dateFormat.format(date); + } } \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUser.java b/src/main/java/com/chukapoka/server/common/dto/CustomUser.java deleted file mode 100644 index e4832d5..0000000 --- a/src/main/java/com/chukapoka/server/common/dto/CustomUser.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.chukapoka.server.common.dto; - -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.User; - -import java.util.Collection; - -/** - * CustomUser 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 - * 주로 사용자의 고유한 식별자(ID)를 추가로 저장하고자 할 때 사용 - */ -@Getter -public class CustomUser extends User { - private final Long userId; - - public CustomUser(Long userId, String password, Collection authorities) { - super(String.valueOf(userId), password, authorities); - this.userId = userId; - } - -} diff --git a/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java new file mode 100644 index 0000000..da48094 --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/dto/CustomUserDetails.java @@ -0,0 +1,78 @@ +package com.chukapoka.server.common.dto; + +import com.chukapoka.server.user.entity.User; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +/** + * CustomUserDetails 클래스는 Spring Security에서 제공하는 User 클래스를 확장하여 추가적인 사용자 정보를 저장하기 위한 클래스 + * 주로 사용자의 고유한 식별자(ID)를 추가로 저장하고자 할 때 사용 + */ +@Getter +public class CustomUserDetails implements UserDetails { + + private final User user; + + /** 일반 로그인 */ + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + if (user == null) { + return Collections.emptyList(); // user가 null인 경우 빈 권한 목록 반환 + } + return Collections.singleton(new SimpleGrantedAuthority(user.getAuthorities())); + } + + public String getEmail() { + return user.getEmail(); + } + + public Long getUserId() { + return user.getId(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + if (user != null) { + return user.getId().toString(); + } + return null; // 사용자 객체가 null인 경우 null 반환 + } + + + /** 계정의 만료 여부 반환 (기한이 없으므로 항상 true 반환) */ + @Override + public boolean isAccountNonExpired() { + return true; + }; + /** 계정의 잠금 여부 반환 (잠금되지 않았으므로 항상 true 반환)*/ + @Override + public boolean isAccountNonLocked() { + return true; + } + /** 자격 증명의 만료 여부 반환 (기한이 없으므로 항상 true 반환)*/ + @Override + public boolean isCredentialsNonExpired() { + return true; + } + /** 계정의 활성화 여부 반환 (활성화된 계정이므로 항상 true 반환)*/ + @Override + public boolean isEnabled() { + return true; + } + + +} diff --git a/src/main/java/com/chukapoka/server/common/dto/TokenDto.java b/src/main/java/com/chukapoka/server/common/dto/TokenDto.java index 32d2b5e..d3f150d 100644 --- a/src/main/java/com/chukapoka/server/common/dto/TokenDto.java +++ b/src/main/java/com/chukapoka/server/common/dto/TokenDto.java @@ -13,6 +13,7 @@ public class TokenDto { private String grantType; // JWT에 대한 인증 타입. 여기서는 Bearer를 사용. 이후 HTTP 헤더에 prefix로 붙여주는 타입 private String accessToken; private String refreshToken; - private Long accessTokenExpiresIn; + private String atExpiration; + private String rtExpiration; } diff --git a/src/main/java/com/chukapoka/server/common/entity/Token.java b/src/main/java/com/chukapoka/server/common/entity/Token.java index f819e77..406e3eb 100644 --- a/src/main/java/com/chukapoka/server/common/entity/Token.java +++ b/src/main/java/com/chukapoka/server/common/entity/Token.java @@ -10,6 +10,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import javax.crypto.KEM; + @Getter @NoArgsConstructor @Data @@ -26,23 +28,25 @@ public class Token { @Column(name = "rt_value") private String rtValue; // refresh token - // TODO: 현진 access token, refresh token 만료시간 컬럼 추가 + + // 만료 시간을 나타내는 컬럼 추가 + @Column(name = "at_expiration") + private String atExpiration; // access token 만료 시간 + + @Column(name = "rt_expiration") + private String rtExpiration; // refresh token 만료 시간 + @Builder - public Token(String key, String atValue, String rtValue) { + public Token(String key, String atValue, String rtValue, String atExpiration, String rtExpiration) { this.key = key; this.atValue = atValue; this.rtValue = rtValue; + this.atExpiration = atExpiration; + this.rtExpiration = rtExpiration; } - - public Token updateValues(String accessToken, String refreshToken) { - this.atValue = accessToken; - this.rtValue = refreshToken; - return this; - } - + public TokenResponseDto toResponseDto(){ return new TokenResponseDto(this.atValue); } - } diff --git a/src/main/java/com/chukapoka/server/common/enums/TreeType.java b/src/main/java/com/chukapoka/server/common/enums/TreeType.java new file mode 100644 index 0000000..f95fa85 --- /dev/null +++ b/src/main/java/com/chukapoka/server/common/enums/TreeType.java @@ -0,0 +1,30 @@ +package com.chukapoka.server.common.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter +public enum TreeType { + MINE("내트리"), + NOT_YET_SEND("미부여 트리"); + + private final String description; + private static final Map lookup = new HashMap<>(); + + static { + for (TreeType treeType : TreeType.values()) { + lookup.put(treeType.getDescription(), treeType); + } + } + + TreeType(String description) { + this.description = description; + } + + public static TreeType getByDescription(String description) { + return lookup.get(description); + } + +} diff --git a/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java b/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java index 0d58a39..52d3b3c 100644 --- a/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java +++ b/src/main/java/com/chukapoka/server/common/repository/TokenRepository.java @@ -10,5 +10,4 @@ public interface TokenRepository extends JpaRepository { Optional findByKey(String key); Optional findByAtValue(String atValue); -// String getAccessToken(String token); } diff --git a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java index 47ba13c..c979180 100644 --- a/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java +++ b/src/main/java/com/chukapoka/server/common/service/CustomUserDetailsService.java @@ -1,22 +1,15 @@ package com.chukapoka.server.common.service; -import com.chukapoka.server.common.dto.CustomUser; -import com.chukapoka.server.common.enums.Authority; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.user.entity.User; import com.chukapoka.server.user.repository.UserRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; 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 java.util.Collection; -import java.util.Collections; - - @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { @@ -32,10 +25,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep } private UserDetails createUserDetails(User user) { - return new CustomUser(user.getId(), user.getPassword(), getAuthorities()); + return new CustomUserDetails(user); } - private Collection getAuthorities() { - return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + Authority.USER.getAuthority())); - } } diff --git a/src/main/java/com/chukapoka/server/tree/controller/TreeController.java b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java new file mode 100644 index 0000000..e319445 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/controller/TreeController.java @@ -0,0 +1,71 @@ +package com.chukapoka.server.tree.controller; + +import com.chukapoka.server.common.dto.BaseResponse; +import com.chukapoka.server.common.dto.CustomUserDetails; +import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.tree.dto.TreeDetailResponseDto; +import com.chukapoka.server.tree.dto.TreeListResponseDto; +import com.chukapoka.server.tree.dto.TreeCreateRequestDto; +import com.chukapoka.server.tree.dto.TreeModifyRequestDto; +import com.chukapoka.server.tree.service.TreeService; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + + + +@RestController +@AllArgsConstructor +@RequestMapping("/api/tree") +public class TreeController { + + @Autowired + private final TreeService treeService ; + + /**트리 생성 */ + @PostMapping + public BaseResponsecreateTree(@Valid @RequestBody TreeCreateRequestDto treeRequestDto) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeDetailResponseDto responseDto = treeService.createTree(treeRequestDto, userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리리스트 목록 */ + @GetMapping + public BaseResponse treeList() { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeListResponseDto responseDto = treeService.treeList(userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리상세 정보 */ + @GetMapping("/{treeId}") + private BaseResponse treeDetail(@PathVariable("treeId") String treeId) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeDetailResponseDto responseDto = treeService.treeDetail(treeId, userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리 수정 */ + @PutMapping("/{treeId}") + public BaseResponse treeModify(@PathVariable("treeId") String treeId, + @Valid @RequestBody TreeModifyRequestDto treeModifyDto) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeDetailResponseDto responseDto = treeService.treeModify(treeId,userId ,treeModifyDto); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + + /** 트리 삭제 */ + @DeleteMapping("/{treeId}") + public BaseResponse treeDelete(@PathVariable("treeId") String treeId) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + treeService.treeDelete(treeId, userId); + return new BaseResponse<>(ResultType.SUCCESS, null); + } + + +} + diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java new file mode 100644 index 0000000..ffea5be --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeCreateRequestDto.java @@ -0,0 +1,43 @@ +package com.chukapoka.server.tree.dto; + +import com.chukapoka.server.common.annotation.ValidEnum; +import com.chukapoka.server.common.enums.TreeType; +import com.chukapoka.server.tree.entity.Tree; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.util.UUID; + +@Data +public class TreeCreateRequestDto { + @NotBlank(message = "title is null") + private String title; + @NotBlank(message = "treeType is null") + @ValidEnum(enumClass = TreeType.class, message = "TreeType must be MINE or NOT_YET_SEND") + private String type; + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; + // 클라이언트에서는 입력받을 필요없음 ( TreeServicelmpl.createTree 에서 처리 ) + + /** Create Tree Build*/ + public Tree toEntity(TreeCreateRequestDto treeRequestDto, long userId) { + UUID linkId = UUID.randomUUID(); + UUID sendId = UUID.randomUUID(); + return Tree.builder() + .title(treeRequestDto.title) + .type(treeRequestDto.type) + .linkId(linkId + "-link-"+ userId ) + .sendId(sendId + "-send-"+ userId) + .treeBgColor(treeRequestDto.treeBgColor) + .groundColor(treeRequestDto.groundColor) + .treeTopColor(treeRequestDto.treeTopColor) + .treeItemColor(treeRequestDto.treeItemColor) + .treeBottomColor(treeRequestDto.treeBottomColor) + .updatedBy(userId) + .build(); + + } +} \ No newline at end of file diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java new file mode 100644 index 0000000..14248ca --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeDetailResponseDto.java @@ -0,0 +1,67 @@ +package com.chukapoka.server.tree.dto; + +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.treeItem.entity.TreeItem; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TreeDetailResponseDto { + + /** 트리상세 정보 */ + private String treeId; + private String title; + private String type; // MINE or NOT_YEN_SEND + private String linkId; + private String sendId; + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; + private Long updatedBy; + private LocalDateTime updatedAt; + + /** treeItem 목록 */ + private List treeItem; + + + /** 트리 생성, 수정 constructor */ + public TreeDetailResponseDto(Tree tree) { + this.treeId = tree.getTreeId(); + this.title = tree.getTitle(); + this.type = tree.getType(); + this.linkId = tree.getLinkId(); + this.sendId = tree.getSendId(); + this.treeBgColor = tree.getTreeBgColor(); + this.groundColor = tree.getGroundColor(); + this.treeTopColor = tree.getTreeTopColor(); + this.treeItemColor = tree.getTreeItemColor(); + this.treeBottomColor = tree.getTreeBottomColor(); + this.updatedBy = tree.getUpdatedBy(); + this.updatedAt = tree.getUpdatedAt(); + } + + /** 트리 상세정보 constructor */ + public TreeDetailResponseDto(Tree tree, List treeItem) { + this.treeId = tree.getTreeId(); + this.title = tree.getTitle(); + this.type = tree.getType(); + this.linkId = tree.getLinkId(); + this.sendId = tree.getSendId(); + this.treeBgColor = tree.getTreeBgColor(); + this.groundColor = tree.getGroundColor(); + this.treeTopColor = tree.getTreeTopColor(); + this.treeItemColor = tree.getTreeItemColor(); + this.treeBottomColor = tree.getTreeBottomColor(); + this.updatedBy = tree.getUpdatedBy(); + this.updatedAt = tree.getUpdatedAt(); + this.treeItem = treeItem; + } +} diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeList.java b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java new file mode 100644 index 0000000..defffd3 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeList.java @@ -0,0 +1,24 @@ +package com.chukapoka.server.tree.dto; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class TreeList { + + /** 트리 리스트정보 */ + private String treeId; + private String title; + private String type; // MINE or NOT_YEN_SEND + private String linkId; + private String sendId; + private Long updatedBy; + private LocalDateTime updatedAt; + /** 트리색상 부분 추가 가능 */ + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; + +} diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java new file mode 100644 index 0000000..8947a13 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeListResponseDto.java @@ -0,0 +1,17 @@ +package com.chukapoka.server.tree.dto; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeListResponseDto { + + /** 트리 리스트 목록 */ + private List treeList; +} + + diff --git a/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java new file mode 100644 index 0000000..9e9cc19 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/dto/TreeModifyRequestDto.java @@ -0,0 +1,24 @@ +package com.chukapoka.server.tree.dto; + +import com.chukapoka.server.common.annotation.ValidEnum; +import com.chukapoka.server.common.enums.TreeType; +import jakarta.validation.constraints.NotBlank; + +import lombok.Data; + +@Data +public class TreeModifyRequestDto { + + /** 트리에 관련된 것만 수정할것인지 ?*/ + private String title; + @NotBlank(message = "treeType is null") + @ValidEnum(enumClass = TreeType.class, message = "TreeType must be MINE or NOT_YET_SEND") + private String type; + private String treeBgColor; + private String groundColor; + private String treeTopColor; + private String treeItemColor; + private String treeBottomColor; + + +} diff --git a/src/main/java/com/chukapoka/server/tree/entity/Tree.java b/src/main/java/com/chukapoka/server/tree/entity/Tree.java new file mode 100644 index 0000000..96d5599 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/entity/Tree.java @@ -0,0 +1,82 @@ +package com.chukapoka.server.tree.entity; + +import jakarta.persistence.*; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; + +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@DynamicUpdate // 데이터의 변경사항이 있는 것만 수정 +@Builder +@Table(name = "tb_tree") +public class Tree { + @Id + @Column(name = "treeId", unique = true, nullable = false) + private String treeId; + + /** 트리제목 */ + @Column(name = "title") + private String title; + + /** 내트리 || 미부여 트리 */ + @Column(name = "type") + private String type; + + /** 트리 링크를 특정하기 위한 id*/ + @Column(name = "linkId", nullable = false, unique = true, length = 200) + private String linkId; + + /** 타인에게 트리를 전달할 때 트리를 특정하기 위한 id */ + @Column(name = "sendId", unique = true, length = 200) + private String sendId; + + /** 트라 관련 색상은 String -> enum type으로 상수로 바꿔야 관리가 더 편할것같음 */ + @Column(name = "treeBgColor", nullable = true) + private String treeBgColor; + + @Column(name = "groundColor", nullable = true) + private String groundColor; + + @Column(name = "treeTopColor", nullable = true) + private String treeTopColor; + + @Column(name = "treeItemColor", nullable = true) + private String treeItemColor; + + @Column(name = "treeBottomColor", nullable = true) + private String treeBottomColor; + + /** userId가 값임 */ + @Column(name = "updatedBy") + private Long updatedBy; + + /** 생성 시간 */ + @Column(name = "updatedAt", nullable = false) + @LastModifiedDate + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + this.updatedAt = LocalDateTime.now(); + if(this.treeId == null) { + this.treeId = TreeId(); + } + + } + + private static final AtomicInteger counter = new AtomicInteger(0); + private static String TreeId() { + return "treeId" + counter.incrementAndGet(); + } + +} diff --git a/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java new file mode 100644 index 0000000..47124b0 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/repository/TreeRepository.java @@ -0,0 +1,23 @@ +package com.chukapoka.server.tree.repository; + +import com.chukapoka.server.tree.entity.Tree; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TreeRepository extends JpaRepository { + Optional findByTreeIdAndUpdatedBy(String treeId, long userId); + + List findAllByUpdatedBy(Long updatedBy); + /** treeList 조회 어떤 방법으로 할지 1. jpa @Query로 직접 찾기 2. jpa로 모두 찾은후 modelmapper로 맵핑할지 + * @Query("SELECT new com.chukapoka.server.tree.dto.TreeList(tree.treeId, tree.title, tree.type, tree.linkId, tree.sendId, tree.updatedBy, tree.updatedAt) FROM Tree tree") + * List findAllTrees(); + */ + + + +} diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeService.java b/src/main/java/com/chukapoka/server/tree/service/TreeService.java new file mode 100644 index 0000000..d71f568 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/service/TreeService.java @@ -0,0 +1,25 @@ +package com.chukapoka.server.tree.service; + +import com.chukapoka.server.tree.dto.TreeDetailResponseDto; +import com.chukapoka.server.tree.dto.TreeListResponseDto; +import com.chukapoka.server.tree.dto.TreeCreateRequestDto; +import com.chukapoka.server.tree.dto.TreeModifyRequestDto; + +public interface TreeService { + + /** 트리 저장 */ + TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto, long userId); + + /** 트리리스트 조회(리스트용 모델) */ + TreeListResponseDto treeList(long userId); + + /** 트리 상세 정보 조회 (상세정보 모델) */ + TreeDetailResponseDto treeDetail(String treeId, long userId); + + /** 트리 수정 */ + TreeDetailResponseDto treeModify(String treeId,long userId, TreeModifyRequestDto treeModifyDto); + + /** 트리 삭제 */ + void treeDelete(String treeId,long userId); + +} diff --git a/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java new file mode 100644 index 0000000..706d013 --- /dev/null +++ b/src/main/java/com/chukapoka/server/tree/service/TreeServiceImpl.java @@ -0,0 +1,88 @@ +package com.chukapoka.server.tree.service; + + +import com.chukapoka.server.tree.dto.*; +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.tree.repository.TreeRepository; +import com.chukapoka.server.treeItem.entity.TreeItem; +import com.chukapoka.server.treeItem.repository.TreeItemRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.AllArgsConstructor; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@AllArgsConstructor +public class TreeServiceImpl implements TreeService{ + + private final TreeRepository treeRepository; + private final TreeItemRepository treeItemRepository; + private final ModelMapper modelMapper; + + /** 트리생성 */ + @Override + @Transactional + public TreeDetailResponseDto createTree(TreeCreateRequestDto treeRequestDto, long userId) { + // 클라이언트에서 입력 받을 필요없이 토큰으로 접속후 권한id로 셋팅 + Tree tree = treeRequestDto.toEntity(treeRequestDto, userId); + treeRepository.save(tree); + return new TreeDetailResponseDto(tree); + } + + /** 사용자 트리 리스트 조회(리스트용 모델) */ + @Override + public TreeListResponseDto treeList(long userId) { + // modelMapper로 리스트 조회후 맵핑하는방법 + List trees = treeRepository.findAllByUpdatedBy(userId); + List treeLists = trees.stream() + .map(tree -> modelMapper.map(tree, TreeList.class)) + .collect(Collectors.toList()); + return new TreeListResponseDto(treeLists); + } + + /** 트리 상세 정보 조회 (상세정보 모델) */ + @Override + public TreeDetailResponseDto treeDetail(String treeId, long userId) { + Tree tree = findTreeByIdOrThrow(treeId, userId); + // 트리에 속한 모든 TreeItem을 가져오기 + List treeItems = treeItemRepository.findByTreeId(tree.getTreeId()); + // 트리와 트리아이템 전체목록 반환 + return new TreeDetailResponseDto(tree, treeItems); + } + + /** 트리수정 */ + @Override + @Transactional + public TreeDetailResponseDto treeModify(String treeId, long userId, TreeModifyRequestDto treeModifyDto) { + // 트리 아이디로 트리를 찾음 + Tree tree = findTreeByIdOrThrow(treeId, userId); + // TreeModifyRequestDto를 Tree 엔티티로 변환하여 엔티티에 적용 + modelMapper.map(treeModifyDto, tree); + tree.setUpdatedAt(LocalDateTime.now()); + // 변경된 트리 저장 + treeRepository.save(tree); + // 변경된 트리 상세 정보 반환 + return modelMapper.map(tree, TreeDetailResponseDto.class); + } + + /** 트리 삭제 */ + @Override + @Transactional + public void treeDelete(String treeId, long userId) { + Tree tree = findTreeByIdOrThrow(treeId, userId); + treeRepository.delete(tree); + } + + + /** treeId Exception 처리 메서드 */ + private Tree findTreeByIdOrThrow(String treeId, long userId) { + return treeRepository.findByTreeIdAndUpdatedBy(treeId, userId) + .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); + } +} diff --git a/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java new file mode 100644 index 0000000..353e0d9 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/controller/TreeItemController.java @@ -0,0 +1,63 @@ +package com.chukapoka.server.treeItem.controller; + +import com.chukapoka.server.common.dto.BaseResponse; +import com.chukapoka.server.common.dto.CustomUserDetails; +import com.chukapoka.server.common.enums.ResultType; +import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; +import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemListResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemModifyRequestDto; +import com.chukapoka.server.treeItem.service.TreeItemService; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@AllArgsConstructor +@RequestMapping("api/treeItem") +public class TreeItemController { + + private final TreeItemService treeItemService; + /** 트리아이템 생성 */ + @PostMapping + public BaseResponse createTreeItem(@Valid @RequestBody TreeItemCreateRequestDto treeItemCreateRequestDto) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemDetailResponseDto responseDto = treeItemService.createTreeItem(treeItemCreateRequestDto, userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리리스트 목록 */ + @GetMapping + public BaseResponse treeItemList() { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemListResponseDto responseDto = treeItemService.treeList(userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리상세 정보 */ + @GetMapping("/{treeItemId}") + private BaseResponse treeItemDetail(@PathVariable("treeItemId") String treeItemId) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemDetailResponseDto responseDto = treeItemService.treeDetail(treeItemId, userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + /** 트리 수정 */ + @PutMapping("/{treeItemId}") + public BaseResponse treeItemModify(@PathVariable("treeItemId") String treeItemId, + @Valid @RequestBody TreeItemModifyRequestDto treeItemModifyDto) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + TreeItemDetailResponseDto responseDto = treeItemService.treeModify(treeItemId, treeItemModifyDto, userId); + return new BaseResponse<>(ResultType.SUCCESS, responseDto); + } + + + /** 트리 삭제 */ + @DeleteMapping("/{treeItemId}") + public BaseResponse treeItemDelete(@PathVariable("treeItemId") String treeItemId) { + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + treeItemService.treeItemDelete(treeItemId, userId); + return new BaseResponse<>(ResultType.SUCCESS, null); + } +} diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java new file mode 100644 index 0000000..95b0268 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemCreateRequestDto.java @@ -0,0 +1,37 @@ +package com.chukapoka.server.treeItem.dto; + +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.treeItem.entity.TreeItem; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class TreeItemCreateRequestDto { + + @NotNull(message = "treeId is null") + private String treeId; + @NotBlank(message = "title is null") + private String title; + @NotBlank(message = "content is null") + private String content; + @NotBlank(message = "treeItemColor is null") + private String treeItemColor; + + + public TreeItem toEntity(Tree tree, TreeItemCreateRequestDto treeItemCreateRequestDto, long userId) { + return TreeItem.builder() + .treeId(tree.getTreeId()) + .title(treeItemCreateRequestDto.getTitle()) + .content(treeItemCreateRequestDto.getContent()) + .treeItemColor(treeItemCreateRequestDto.getTreeItemColor()) + .updatedBy(userId) + .updatedAt(LocalDateTime.now()) + .build(); + } + +} + + diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java new file mode 100644 index 0000000..1c8028d --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemDetailResponseDto.java @@ -0,0 +1,33 @@ +package com.chukapoka.server.treeItem.dto; + +import com.chukapoka.server.treeItem.entity.TreeItem; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeItemDetailResponseDto { + + /** 트리 아이템 상세정보 */ + private String id; + private String treeId; + private String title; + private String content; + private String treeItemColor; + private Long updatedBy; + private LocalDateTime updatedAt; + + public TreeItemDetailResponseDto(TreeItem treeItem) { + this.id = treeItem.getId(); + this.treeId = treeItem.getTreeId(); + this.title = treeItem.getTitle(); + this.content = treeItem.getContent(); + this.treeItemColor = treeItem.getTreeItemColor(); + this.updatedBy = treeItem.getUpdatedBy(); + this.updatedAt = treeItem.getUpdatedAt(); + } +} diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java new file mode 100644 index 0000000..5f5068f --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemListResponseDto.java @@ -0,0 +1,14 @@ +package com.chukapoka.server.treeItem.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TreeItemListResponseDto { + private List treeItem; +} diff --git a/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java new file mode 100644 index 0000000..68e965b --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/dto/TreeItemModifyRequestDto.java @@ -0,0 +1,11 @@ +package com.chukapoka.server.treeItem.dto; + +import lombok.Data; + +@Data +public class TreeItemModifyRequestDto { + + private String title; + private String content; + private String treeItemColor; +} diff --git a/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java new file mode 100644 index 0000000..50784e0 --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/entity/TreeItem.java @@ -0,0 +1,62 @@ +package com.chukapoka.server.treeItem.entity; + + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; + +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@DynamicUpdate // 데이터의 변경사항이 있는 것만 수정 +@Table(name = "tb_treeItem") +public class TreeItem { + @Id + @Column(name = "treeItemId", unique = true, nullable = false) + private String id; + + @Column(name = "treeId") + private String treeId; + + /** 편지 제목 */ + @Column(name = "title") + private String title; + + /** 편지 내용*/ + @Column(name = "content") + private String content; + + /** 트리아이템 색상 */ + @Column(name = "treeItemColor", nullable = true) + private String treeItemColor; + + /** userId가 값임 */ + @Column(name = "updatedBy") + private Long updatedBy; + + /** 생성 시간 */ + @Column(name = "updatedAt", nullable = false) + @LastModifiedDate + private LocalDateTime updatedAt; + + @PrePersist // JPA에서는 엔티티의 생명주기 중 하나의 이벤트에 대해 하나의 @PrePersist 메서드만을 허용 + public void prePersist() { + if (this.id == null) { + this.id = TreeItemId(); + } + } + private static final AtomicInteger counter = new AtomicInteger(0); + private static String TreeItemId() { + return "treeItem" + counter.incrementAndGet(); + } + +} diff --git a/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java new file mode 100644 index 0000000..364846f --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/repository/TreeItemRepository.java @@ -0,0 +1,12 @@ +package com.chukapoka.server.treeItem.repository; + +import com.chukapoka.server.treeItem.entity.TreeItem; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface TreeItemRepository extends JpaRepository { + List findByTreeId(String treeId); + Optional findByIdAndUpdatedBy(String treeItemId, long userId); +} diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java new file mode 100644 index 0000000..5811bbe --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemService.java @@ -0,0 +1,23 @@ +package com.chukapoka.server.treeItem.service; +import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; +import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemListResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemModifyRequestDto; + +public interface TreeItemService { + + /** 트리 아이템 생성 */ + TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto, long userId); + + /** 트리아이템리스트 조회(리스트용 모델) */ + TreeItemListResponseDto treeList(long userId); + + /** 트리아이템 상세 정보 조회 (상세정보 모델) */ + TreeItemDetailResponseDto treeDetail(String treeItemId, long userId); + + /** 트리아이템 수정 */ + TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto, long userId); + + /** 트리아이템 삭제 */ + void treeItemDelete(String treeItemId, long userId); +} diff --git a/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java new file mode 100644 index 0000000..9dd36ce --- /dev/null +++ b/src/main/java/com/chukapoka/server/treeItem/service/TreeItemServiceImpl.java @@ -0,0 +1,98 @@ +package com.chukapoka.server.treeItem.service; + +import com.chukapoka.server.tree.entity.Tree; +import com.chukapoka.server.tree.repository.TreeRepository; +import com.chukapoka.server.treeItem.dto.TreeItemCreateRequestDto; +import com.chukapoka.server.treeItem.dto.TreeItemDetailResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemListResponseDto; +import com.chukapoka.server.treeItem.dto.TreeItemModifyRequestDto; +import com.chukapoka.server.treeItem.entity.TreeItem; +import com.chukapoka.server.treeItem.repository.TreeItemRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.AllArgsConstructor; +import org.modelmapper.ModelMapper; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class TreeItemServiceImpl implements TreeItemService{ + + private final TreeItemRepository treeItemRepository; + private final TreeRepository treeRepository; + private final ModelMapper modelMapper; + + /** 트리 아이템 생성 */ + @Override + @Transactional + public TreeItemDetailResponseDto createTreeItem(TreeItemCreateRequestDto treeItemCreateRequestDto, long userId) { + String treeId = treeItemCreateRequestDto.getTreeId(); + return saveTreeItem(treeId, userId, treeItemCreateRequestDto); + } + + + /** 트리아이템 (리스트) */ + @Override + public TreeItemListResponseDto treeList(long userId) { + List treeItems = treeItemRepository.findAll(); + List treeItemDetailResponseDtos = treeItems.stream() + .map(treeItem -> modelMapper.map(treeItem, TreeItemDetailResponseDto.class)) + .collect(Collectors.toList()); + return new TreeItemListResponseDto(treeItemDetailResponseDtos); + + } + + /** 트라이이템 (상세정보) */ + @Override + public TreeItemDetailResponseDto treeDetail(String treeItemId, long userId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId, userId); + return new TreeItemDetailResponseDto(treeItem); + } + + /** 트리아이템 수정 */ + @Override + @Transactional + public TreeItemDetailResponseDto treeModify(String treeItemId, TreeItemModifyRequestDto treeItemModifyDto, long userId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId, userId); + modelMapper.map(treeItemModifyDto, treeItem); + treeItem.setUpdatedAt(LocalDateTime.now()); + // 변경된 트리아이템 저장 + treeItemRepository.save(treeItem); + // 변경된 트리아이템 상세 정보 반환 + return modelMapper.map(treeItem, TreeItemDetailResponseDto.class); + } + + /** 트리아이템 삭제 */ + @Override + @Transactional + public void treeItemDelete(String treeItemId, long userId) { + TreeItem treeItem = findTreeItemIdOrThrow(treeItemId, userId); + treeItemRepository.delete(treeItem); + } + + /** 트라이이템 저장 메서드 */ + private TreeItemDetailResponseDto saveTreeItem(String treeId, long userId, TreeItemCreateRequestDto treeItemCreateRequestDto) { + // 트리 객체 조회 + Tree tree = treeRepository.findById(treeId).orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeId + "입니다.")); + // 트리 아이템 생성 및 저장 + TreeItem treeItem = treeItemCreateRequestDto.toEntity(tree , treeItemCreateRequestDto, userId); + treeItemRepository.save(treeItem); + return new TreeItemDetailResponseDto(treeItem); + } + + /** treeItemId Exception 처리 메서드 */ + private TreeItem findTreeItemIdOrThrow(String treeItemId, long userId) { + return treeItemRepository.findByIdAndUpdatedBy(treeItemId, userId) + .orElseThrow(() -> new EntityNotFoundException("등록되지 않은 " + treeItemId + "입니다.")); + } +} + + + + + diff --git a/src/main/java/com/chukapoka/server/user/controller/HealthController.java b/src/main/java/com/chukapoka/server/user/controller/HealthController.java deleted file mode 100644 index bed8451..0000000 --- a/src/main/java/com/chukapoka/server/user/controller/HealthController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.chukapoka.server.user.controller; - - -import com.chukapoka.server.common.dto.BaseResponse; -import com.chukapoka.server.common.dto.CustomUser; -import com.chukapoka.server.common.dto.TokenResponseDto; -import com.chukapoka.server.common.enums.NextActionType; -import com.chukapoka.server.common.enums.ResultType; -import com.chukapoka.server.user.dto.*; -import com.chukapoka.server.user.sevice.AuthNumberService; -import com.chukapoka.server.user.sevice.UserService; -import jakarta.mail.MessagingException; -import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.*; - -import java.io.UnsupportedEncodingException; - -@RestController -@RequestMapping("/api") -public class HealthController { - - - /** 인증번호 요청 API */ - @GetMapping("/health") - public BaseResponse authNumber() { - return new BaseResponse<>(ResultType.SUCCESS, "health"); - } - - - -} - diff --git a/src/main/java/com/chukapoka/server/user/controller/UserController.java b/src/main/java/com/chukapoka/server/user/controller/UserController.java index 6cdc92b..e20c78d 100644 --- a/src/main/java/com/chukapoka/server/user/controller/UserController.java +++ b/src/main/java/com/chukapoka/server/user/controller/UserController.java @@ -59,7 +59,7 @@ public BaseResponse authNumber(@RequestParam("email") Str /** 토큰 재발급(refresh token 유효한 상태) */ @PostMapping("/reissue") public BaseResponse reissue() { - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); TokenResponseDto tokenDto = userService.reissue(userId); return new BaseResponse<>(ResultType.SUCCESS, tokenDto); } @@ -68,7 +68,7 @@ public BaseResponse reissue() { @PostMapping("/logout") public BaseResponse logout() { // 인증된 사용자 Id - long userId = ((CustomUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); + long userId = ((CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUserId(); // 사용자의 ID를 기반으로 로그아웃 수행 ResultType logout = userService.logout(userId); return new BaseResponse<>(ResultType.SUCCESS, logout); diff --git a/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java b/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java index 0d63ba0..edeed49 100644 --- a/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java +++ b/src/main/java/com/chukapoka/server/user/dto/UserResponseDto.java @@ -21,9 +21,4 @@ public class UserResponseDto { private Long userId; // unique_userid private TokenResponseDto token; // JWT 토큰 - public UserResponseDto(ResultType result, String email, Long userId) { - this.result = result; - this.email = email; - this.userId = userId; - } } diff --git a/src/main/java/com/chukapoka/server/user/entity/User.java b/src/main/java/com/chukapoka/server/user/entity/User.java index b3317af..3cfc437 100644 --- a/src/main/java/com/chukapoka/server/user/entity/User.java +++ b/src/main/java/com/chukapoka/server/user/entity/User.java @@ -24,19 +24,19 @@ public class User { @Column(name = "userId") private Long id; - @Column(nullable = false, unique = true) + @Column(nullable = false, name = "email", unique = true) private String email; - @Column(nullable = false) + @Column(nullable = false, name = "emailType") private String emailType; - @Column(nullable = false) + @Column(nullable = false, name = "password") private String password; - @Column(nullable = false) + @Column(nullable = false, name = "updateAt") private LocalDateTime updatedAt; - @Transient + @Column private String authorities; // 권한 ROLE_USER || ROLE_ADMIN @Builder diff --git a/src/main/java/com/chukapoka/server/user/repository/UserRepository.java b/src/main/java/com/chukapoka/server/user/repository/UserRepository.java index 9853b04..6c8bedb 100644 --- a/src/main/java/com/chukapoka/server/user/repository/UserRepository.java +++ b/src/main/java/com/chukapoka/server/user/repository/UserRepository.java @@ -20,10 +20,10 @@ public interface UserRepository extends JpaRepository { Optional findByEmailAndEmailType(String email, String emailType); - @Override - Optional findById(Long aLong); + Optional findById(Long id); // 이메일이 등록되어있는지 이메일과 이메일타입 확인 boolean existsByEmailAndEmailType(String email, String emailType); + } diff --git a/src/main/java/com/chukapoka/server/user/sevice/UserService.java b/src/main/java/com/chukapoka/server/user/sevice/UserService.java index 964f307..d4515f7 100644 --- a/src/main/java/com/chukapoka/server/user/sevice/UserService.java +++ b/src/main/java/com/chukapoka/server/user/sevice/UserService.java @@ -2,9 +2,8 @@ -import com.chukapoka.server.common.authority.JwtTokenProvider; - -import com.chukapoka.server.common.dto.CustomUser; +import com.chukapoka.server.common.authority.jwt.JwtTokenProvider; +import com.chukapoka.server.common.dto.CustomUserDetails; import com.chukapoka.server.common.dto.TokenDto; import com.chukapoka.server.common.dto.TokenResponseDto; import com.chukapoka.server.common.entity.Token; @@ -152,12 +151,7 @@ public User findUser(UserRequestDto userRequestDto) { /** 유저정보에 따른 Authentication 생성 */ public Authentication getAuthentication(User user) { return new UsernamePasswordAuthenticationToken( - new CustomUser( - user.getId(), - user.getPassword(), - List.of( - new SimpleGrantedAuthority("ROLE" + Authority.USER.getAuthority())) - ), + new CustomUserDetails(user), null, List.of( new SimpleGrantedAuthority("ROLE_" + Authority.USER.getAuthority()) @@ -173,6 +167,8 @@ public TokenResponseDto saveToken(Authentication authentication){ .key(authentication.getName()) .atValue(jwtToken.getAccessToken()) .rtValue(jwtToken.getRefreshToken()) + .atExpiration(jwtToken.getAtExpiration()) + .rtExpiration(jwtToken.getRtExpiration()) .build(); return tokenRepository.save(token).toResponseDto(); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index e401720..462daff 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -3,4 +3,4 @@ spring: # active: prod active: dev # active: dev-db -# active: local +# active: local \ No newline at end of file diff --git a/swagger.yaml b/swagger.yaml deleted file mode 100644 index c1c77f6..0000000 --- a/swagger.yaml +++ /dev/null @@ -1,90 +0,0 @@ -openapi: 3.0.3 -info: - title: Vue-Springboot-Memo Api - description: |- - Vue Springboot client와 server 테스트 - https://github.com/doyou1/vue-spring-sampling - version: 1.0.0 -servers: - - url: http://jh-memo-env.eba-khreh2xt.ap-northeast-1.elasticbeanstalk.com/api -tags: - - name: /api/memo - description: about memo -paths: - /api/memo: - get: - tags: - - /api/memo - summary: get memo list - description: get memo list - responses: - '200': - description: Successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Memo' - post: - tags: - - /api/memo - summary: add a Memo item - description: add a Memo item - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - /api/memo/{id}: - put: - tags: - - /api/memo - summary: update a Memo item - description: update a Memo item - parameters: - - name: id - in: path - required: true - description: id to update the Memo item - schema: - type : integer - format: int64 - example: 10 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Memo' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' -components: - schemas: - Memo: - properties: - id: - type: integer - format: int64 - example: 10 - content: - type: string - example: memo content - isDone: - type: boolean - ApiResponse: - type: object - properties: - id: - type: integer - format: int32 - example: 10 - isSuccess: - type: boolean \ No newline at end of file