diff --git a/src/main/java/com/lgcns/Docking/planet/controller/PlanetController.java b/src/main/java/com/lgcns/Docking/planet/controller/PlanetController.java new file mode 100644 index 0000000..8729d21 --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/controller/PlanetController.java @@ -0,0 +1,62 @@ +package com.lgcns.Docking.planet.controller; + +import com.lgcns.Docking.planet.dto.PlanetListResponseDto; +import com.lgcns.Docking.planet.dto.PlanetRequestDto; +import com.lgcns.Docking.planet.dto.PlanetResponseDto; +import com.lgcns.Docking.planet.entity.Letter; +import com.lgcns.Docking.planet.service.LetterService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/planet") +@RequiredArgsConstructor +public class PlanetController { + + private final LetterService letterService; + + // 1. 행성 선택 (편지에 행성 이미지 URL 저장) + @PutMapping("/{letterId}") + public ResponseEntity choosePlanet( + @PathVariable Long letterId, + @RequestBody PlanetRequestDto request + ) { + Letter updated = letterService.savePlanetToLetter(letterId, request.getPlanetUrl()); + return ResponseEntity.ok(new PlanetResponseDto( + updated.getId(), + updated.getPlanet(), + "성공입니다" + )); + } + + @GetMapping("/main") + public ResponseEntity> getPlanetList( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "15") int size + ) { + Page resultPage = letterService.getPlanetList(page, size); + + Map response = new HashMap<>(); + response.put("isSuccess", true); + response.put("code", "COMMON200"); + response.put("message", "성공입니다."); + + Map result = new HashMap<>(); + result.put("content", resultPage.getContent()); + result.put("page", resultPage.getNumber()); + result.put("size", resultPage.getSize()); + result.put("totalPages", resultPage.getTotalPages()); + result.put("totalElements", resultPage.getTotalElements()); + + response.put("result", result); + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/lgcns/Docking/planet/dto/PlanetListResponseDto.java b/src/main/java/com/lgcns/Docking/planet/dto/PlanetListResponseDto.java new file mode 100644 index 0000000..57f5ad1 --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/dto/PlanetListResponseDto.java @@ -0,0 +1,10 @@ +package com.lgcns.Docking.planet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PlanetListResponseDto { + private String planetUrl; +} diff --git a/src/main/java/com/lgcns/Docking/planet/dto/PlanetRequestDto.java b/src/main/java/com/lgcns/Docking/planet/dto/PlanetRequestDto.java new file mode 100644 index 0000000..b921478 --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/dto/PlanetRequestDto.java @@ -0,0 +1,8 @@ +package com.lgcns.Docking.planet.dto; + +import lombok.Getter; + +@Getter +public class PlanetRequestDto { + private String planetUrl; +} diff --git a/src/main/java/com/lgcns/Docking/planet/dto/PlanetResponseDto.java b/src/main/java/com/lgcns/Docking/planet/dto/PlanetResponseDto.java new file mode 100644 index 0000000..282eccc --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/dto/PlanetResponseDto.java @@ -0,0 +1,12 @@ +package com.lgcns.Docking.planet.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class PlanetResponseDto { + private Long letterId; + private String planetUrl; + private String message; +} diff --git a/src/main/java/com/lgcns/Docking/planet/entity/Letter.java b/src/main/java/com/lgcns/Docking/planet/entity/Letter.java new file mode 100644 index 0000000..932fd46 --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/entity/Letter.java @@ -0,0 +1,35 @@ +package com.lgcns.Docking.planet.entity; + +import com.lgcns.Docking.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "letter") +@Getter +@Setter +@NoArgsConstructor +public class Letter { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String description; + + @Column + private String planet; + + @Column(nullable = false) + private String sticker; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} diff --git a/src/main/java/com/lgcns/Docking/planet/repository/LetterRepository.java b/src/main/java/com/lgcns/Docking/planet/repository/LetterRepository.java new file mode 100644 index 0000000..436747c --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/repository/LetterRepository.java @@ -0,0 +1,10 @@ +package com.lgcns.Docking.planet.repository; + +import com.lgcns.Docking.planet.entity.Letter; +import com.lgcns.Docking.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LetterRepository extends JpaRepository { + boolean existsByUser(User user); +} + diff --git a/src/main/java/com/lgcns/Docking/planet/service/LetterService.java b/src/main/java/com/lgcns/Docking/planet/service/LetterService.java new file mode 100644 index 0000000..867a28b --- /dev/null +++ b/src/main/java/com/lgcns/Docking/planet/service/LetterService.java @@ -0,0 +1,28 @@ +package com.lgcns.Docking.planet.service; + +import com.lgcns.Docking.planet.dto.PlanetListResponseDto; +import com.lgcns.Docking.planet.entity.Letter; +import com.lgcns.Docking.planet.repository.LetterRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LetterService { + + private final LetterRepository letterRepository; + + public Letter savePlanetToLetter(Long letterId, String planetUrl) { + Letter letter = letterRepository.findById(letterId) + .orElseThrow(() -> new IllegalArgumentException("편지를 찾을 수 없습니다.")); + letter.setPlanet(planetUrl); + return letterRepository.save(letter); + } + + public Page getPlanetList(int page, int size) { + Page letters = letterRepository.findAll(PageRequest.of(page, size)); + return letters.map(letter -> new PlanetListResponseDto(letter.getPlanet())); + } +} diff --git a/src/main/java/com/lgcns/Docking/security/config/SecurityConfig.java b/src/main/java/com/lgcns/Docking/security/config/SecurityConfig.java index c827ac7..e44039b 100644 --- a/src/main/java/com/lgcns/Docking/security/config/SecurityConfig.java +++ b/src/main/java/com/lgcns/Docking/security/config/SecurityConfig.java @@ -11,6 +11,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -57,8 +58,11 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { // 경로별 인가 설정 http.authorizeHttpRequests(auth -> auth .requestMatchers("/").permitAll() - .requestMatchers("/news/post", "/admin/users/news/","/api/comment/").authenticated() - .requestMatchers("/swagger-ui","/news/**","/api/comment/**","/auth/logout").permitAll() + .requestMatchers("/planet/**").authenticated() + .requestMatchers("/oauth2/**", "/login/**").permitAll() + .requestMatchers("/images/**", "/css/**", "/js/**", "/favicon.ico").permitAll() + + .anyRequest().authenticated() ); @@ -66,9 +70,16 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { http.formLogin(form -> form.disable()); http.httpBasic(basic -> basic.disable()); + /* + // JWT 필터 추가 + http.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class); + */ + + // JWT 필터 추가 http.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); + // OAuth2 로그인 설정 http.oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) @@ -79,7 +90,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { http.exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint((request, response, authException) -> { // 로그인되지 않은 사용자는 네이버 로그인 페이지로 이동 - response.sendRedirect("oauth2/authorization/kakao"); + response.sendRedirect("/oauth2/authorization/kakao"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { // 로그인은 했지만 ADMIN 권한이 없는 경우 403 응답 diff --git a/src/main/java/com/lgcns/Docking/security/jwt/JWTFilter.java b/src/main/java/com/lgcns/Docking/security/jwt/JWTFilter.java index ab3efc8..05e4d9e 100644 --- a/src/main/java/com/lgcns/Docking/security/jwt/JWTFilter.java +++ b/src/main/java/com/lgcns/Docking/security/jwt/JWTFilter.java @@ -2,10 +2,8 @@ import com.lgcns.Docking.user.dto.CustomOAuth2User; import com.lgcns.Docking.user.dto.UserDTO; - -import jakarta.servlet.*; +import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @@ -13,8 +11,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; - import java.io.IOException; + @Slf4j public class JWTFilter extends OncePerRequestFilter { @@ -25,63 +23,47 @@ public JWTFilter(JWTUtil jwtUtil) { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - - //cookie들을 불러온 뒤 Authorization Key에 담긴 쿠키를 찾음 - - // 헤더 검증 - String authorization = null; - Cookie[] cookies = request.getCookies(); - for (Cookie cookie : cookies) { - - System.out.println(cookie.getName()); - if (cookie.getName().equals("Authorization")) { - - authorization = cookie.getValue(); - } - } + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { - //Authorization 헤더 검증 - if (authorization == null) { + String authHeader = request.getHeader("Authorization"); - System.out.println("token null"); + // JWT가 헤더에 없거나 Bearer로 시작하지 않으면 다음 필터로 넘김 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + log.debug("Authorization header missing or malformed"); filterChain.doFilter(request, response); - - //조건이 해당되면 메소드 종료 return; } - //토큰 - String token = authorization; + // Bearer 제거하고 순수 토큰 추출 + String token = authHeader.substring(7); - //토큰 소멸 시간 검증 + // 만료된 토큰 if (jwtUtil.isExpired(token)) { - - System.out.println("token expired"); - filterChain.doFilter(request, response); - - //조건이 해당되면 메소드 종료 (필수) + log.debug("Token expired"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write("{\"error\": \"토큰이 만료되었습니다.\"}"); return; } - //토큰에서 username과 role 획득 - String password = jwtUtil.getUsername(token); + // 토큰에서 사용자 정보 추출 + String username = jwtUtil.getUsername(token); String id = jwtUtil.getId(token); - //userDTO를 생성하여 값 set UserDTO userDTO = new UserDTO(); - userDTO.setUsername(password); + userDTO.setUsername(username); userDTO.setId(id); - //UserDetails에 회원 정보 객체 담기 - CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO); - - //스프링 시큐리티 인증 토큰 생성 - Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities()); + CustomOAuth2User customUser = new CustomOAuth2User(userDTO); + Authentication authToken = new UsernamePasswordAuthenticationToken( + customUser, null, customUser.getAuthorities()); - //세션에 사용자 등록 SecurityContextHolder.getContext().setAuthentication(authToken); - logger.info("jwtFilter success"); + log.info("JWTFilter 인증 완료: {}", username); + filterChain.doFilter(request, response); } -} \ No newline at end of file +} diff --git a/src/main/java/com/lgcns/Docking/security/oauth2/CustomSuccessHandler.java b/src/main/java/com/lgcns/Docking/security/oauth2/CustomSuccessHandler.java index e29ff88..75895f8 100644 --- a/src/main/java/com/lgcns/Docking/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/com/lgcns/Docking/security/oauth2/CustomSuccessHandler.java @@ -1,7 +1,10 @@ package com.lgcns.Docking.security.oauth2; +import com.lgcns.Docking.planet.repository.LetterRepository; import com.lgcns.Docking.security.jwt.JWTUtil; import com.lgcns.Docking.user.dto.CustomOAuth2User; +import com.lgcns.Docking.user.entity.User; +import com.lgcns.Docking.user.repository.UserRepository; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -19,9 +22,15 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JWTUtil jwtUtil; + private final LetterRepository letterRepository; + private final UserRepository userRepository; - public CustomSuccessHandler(JWTUtil jwtUtil) { + + public CustomSuccessHandler(JWTUtil jwtUtil, LetterRepository letterRepository, UserRepository userRepository) { this.jwtUtil = jwtUtil; + this.letterRepository = letterRepository; + this.userRepository = userRepository; + } @Override @@ -47,8 +56,16 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo System.out.println(token); + // 유저 조회 + User user = userRepository.findById(Long.valueOf(id)) + .orElseThrow(() -> new RuntimeException("유저를 찾을 수 없습니다.")); + + boolean hasSubmitted = letterRepository.existsByUser(user); + // 쿠키 방식 전달 + 프론트에 리다이렉트 response.addCookie(createCookie("Authorization", token)); + response.addCookie(createCookie("hasSubmittedLetter", String.valueOf(hasSubmitted))); + response.sendRedirect("http://localhost/"); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 66338f9..88b19d0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -21,7 +21,7 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.properties.hibernate.show_sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl spring.jpa.properties.hibernate.default_batch_fetch_size=1000 diff --git a/src/main/resources/static/images/img.png b/src/main/resources/static/images/img.png new file mode 100644 index 0000000..b035545 Binary files /dev/null and b/src/main/resources/static/images/img.png differ