Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions src/main/java/com/example/redunm/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
Expand All @@ -16,30 +17,24 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http

.csrf(csrf -> csrf.disable())
.csrf(AbstractHttpConfigurer::disable)

.cors(Customizer.withDefaults())

// (3) 권한 설정
.authorizeHttpRequests(auth -> auth
// 인증 없이 열어둘 경로를 명시합니다.
.requestMatchers(
"/api/auth/signup/**", // REST API 회원가입
"/auth/signup/**", // 혹시 /auth/signup/** 로도 쓰고 있다면
"/models/**",
"/api/auth/signup/**", // 회원가입
"/api/auth/login/**", // 로그인
"/auth/signup/**", // 추가적인 회원가입 경로
"/css/**",
"/js/**"
"/js/**",
"/models/**"
).permitAll()

// 그 외 모든 요청은 인증 필요
.anyRequest().authenticated()
)

// (4) 폼 로그인 기능 자체를 비활성화 (자동 리다이렉트X, 401 Unauthorized 응답)
.formLogin(form -> form.disable())

// (5) 로그아웃 설정 (필요 시)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.redunm.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.HttpRequestMethodNotSupportedException;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<?> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body("지원되지 않는 HTTP 메서드입니다. " + ex.getMethod() + "이 엔드포인트에서 지원되지 않습니다.");
}

}
31 changes: 25 additions & 6 deletions src/main/java/com/example/redunm/login/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import com.example.redunm.entity.User;
import com.example.redunm.service.UserService;
import com.example.redunm.login.LoginRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
Expand All @@ -17,22 +19,31 @@ public class LoginController {
@Autowired
private UserService userService;

private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

// POST 요청
@PostMapping
public ResponseEntity<?> login(@RequestBody Map<String, String> requestBody,
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest,
HttpSession session) {
String username = requestBody.get("username");
String password = requestBody.get("password");
String email = loginRequest.getEmail();
String password = loginRequest.getPassword();

if (email == null || password == null) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("이메일과 비밀번호를 모두 입력해주세요.");
}

var optionalUser = userService.findByUsername(username);
var optionalUser = userService.findByEmail(email);
if (optionalUser.isEmpty()) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("존재하지 않는 아이디입니다.");
.body("존재하지 않는 이메일입니다.");
}

User user = optionalUser.get();

if (!user.getPassword().equals(password)) {
if (!passwordEncoder.matches(password, user.getPassword())) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("비밀번호가 일치하지 않습니다.");
Expand All @@ -43,7 +54,15 @@ public ResponseEntity<?> login(@RequestBody Map<String, String> requestBody,
return ResponseEntity.ok("로그인 성공");
}

// GET 요청에 대한 처리 추가
@GetMapping
public ResponseEntity<?> handleGetLogin() {
return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body("GET 메서드는 /api/auth/login 엔드포인트에서 지원되지 않습니다. POST 메서드를 사용하세요.");
}

// 로그아웃 처리 (POST 방식)
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpSession session) {
session.invalidate();
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/example/redunm/login/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.redunm.login;

public class LoginRequest {
private String email;
private String password;
Comment on lines +4 to +5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security warning: Storing passwords in plain text
Consider securing passwords via hashing before storing or transmitting them.


// Getters and Setters

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.example.redunm.modellist;

import com.example.redunm.modellist.ModelCartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.*;
import java.util.List;

@RestController
@RequestMapping("/api/data-models")
Expand All @@ -12,14 +13,10 @@ public class DataModelCartController {
@Autowired
private DataModelRepository dataModelRepository;

/**
* [임시] 사용자별 장바구니 목록을 저장할 Map
* - key: 사용자 ID (문자열)
* - value: 해당 사용자가 장바구니에 담은 DataModel 리스트
*/
private Map<String, List<DataModel>> userCartMap = new HashMap<>();
@Autowired
private ModelCartService cartService; // 새로 만든 CartService

//모델 생성,삭제,추가,수정
// 1) DataModel 기본 CRUD
@GetMapping
public List<DataModel> getAll() {
return dataModelRepository.findAll();
Expand All @@ -46,27 +43,37 @@ public void delete(@PathVariable String id) {
dataModelRepository.deleteById(id);
}

//장바구니 기능
// 2) 장바구니 관련 API
@PostMapping("/cart/{userId}/{modelId}")
public List<DataModel> addToCart(@PathVariable String userId,
@PathVariable String modelId) {
DataModel selectedModel = dataModelRepository.findById(modelId).orElse(null);
if (selectedModel == null) {
return Collections.emptyList();
// DB에서 modelId로 DataModel 조회
DataModel foundModel = dataModelRepository.findById(modelId).orElse(null);
if (foundModel == null) {
// 없는 모델이면 빈 목록 반환
return List.of();
}

List<DataModel> cartList = userCartMap.getOrDefault(userId, new ArrayList<>());

cartList.add(selectedModel);

userCartMap.put(userId, cartList);

return cartList;
// CartService를 통해 장바구니에 추가
return cartService.addToCart(userId, foundModel);
}

//장바구니 목록 조회
// (B) 특정 사용자의 장바구니 조회
@GetMapping("/cart/{userId}")
public List<DataModel> getCart(@PathVariable String userId) {
return userCartMap.getOrDefault(userId, Collections.emptyList());
return cartService.getCart(userId);
}

// (C) 장바구니 전체 비우기 (옵션)
@DeleteMapping("/cart/{userId}")
public void clearCart(@PathVariable String userId) {
cartService.clearCart(userId);
}

// (D) 장바구니에서 특정 모델 제거 (옵션)
@DeleteMapping("/cart/{userId}/{modelId}")
public List<DataModel> removeModelFromCart(@PathVariable String userId,
@PathVariable String modelId) {
return cartService.removeModel(userId, modelId);
}
}
31 changes: 20 additions & 11 deletions src/main/java/com/example/redunm/modellist/DataModelController.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
package com.example.redunm.modellist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.redunm.entity.User;
import jakarta.servlet.http.HttpSession;

import java.util.List;

@Controller
@RestController
@RequestMapping("/api/models")
public class DataModelController {

@Autowired
private DataModelRepository DataModelRepository;
private DataModelRepository dataModelRepository;

@GetMapping("/models")
public String getAllModels(Model model) {
// MongoDB에서 모든 모델 데이터를 조회
List<DataModel> models = DataModelRepository.findAll();
model.addAttribute("models", models); // 데이터를 뷰로 전달
return "modellist"; // modellist.html 렌더링
@GetMapping
public ResponseEntity<?> getAllModels(HttpSession session) {
User loggedInUser = (User) session.getAttribute("loggedInUser");
if (loggedInUser == null) {
// 로그인이 안돼있으면 401 오류 뜨게하기
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("로그인이 필요합니다.");
}

// 로그인 된 경우 mongoDB에서 model 조회
List<DataModel> models = dataModelRepository.findAll();
return ResponseEntity.ok(models);
}
}
68 changes: 68 additions & 0 deletions src/main/java/com/example/redunm/modellist/ModelCartService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.example.redunm.modellist;

import com.example.redunm.modellist.DataModel;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
* 사용자별 장바구니(Cart)를
* 서버 메모리 내부의 Map에 저장해두는 임시 서비스
*/
@Service
public class ModelCartService {

/**
* 사용자별 장바구니 목록을 저장할 Map
* key: 사용자 ID (문자열)
* value: 해당 사용자가 장바구니에 담은 DataModel 리스트
*
* - ConcurrentHashMap: 멀티쓰레드 환경에서 안전성 보장(간단 예시)
* - 실무에서는 Redis나 DB에 저장하는 방식을 권장
*/
private final Map<String, List<DataModel>> userCartMap = new ConcurrentHashMap<>();

/**
* 특정 사용자(userId)의 장바구니 목록 조회
*/
public List<DataModel> getCart(String userId) {
return userCartMap.getOrDefault(userId, Collections.emptyList());
}
Comment on lines +29 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Returning a mutable list
Since getCart returns the actual list, external modifications while still referencing the same list can cause inconsistent states. Consider returning an unmodifiable list or a defensive copy.

-public List<DataModel> getCart(String userId) {
-    return userCartMap.getOrDefault(userId, Collections.emptyList());
+public List<DataModel> getCart(String userId) {
+    List<DataModel> cart = userCartMap.getOrDefault(userId, Collections.emptyList());
+    return List.copyOf(cart); // returns an unmodifiable copy
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public List<DataModel> getCart(String userId) {
return userCartMap.getOrDefault(userId, Collections.emptyList());
}
public List<DataModel> getCart(String userId) {
List<DataModel> cart = userCartMap.getOrDefault(userId, Collections.emptyList());
return List.copyOf(cart); // returns an unmodifiable copy
}


/**
* 장바구니에 모델 추가
* @return 추가 후의 장바구니 전체 목록
*/
public List<DataModel> addToCart(String userId, DataModel model) {
// userId가 가진 장바구니 목록 가져오기 (없으면 새 리스트)
List<DataModel> cartList = userCartMap.getOrDefault(userId, new ArrayList<>());
cartList.add(model);

// 갱신된 목록을 다시 put
userCartMap.put(userId, cartList);

return cartList;
}
Comment on lines +38 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Race condition risk in cart operations due to non-thread-safe ArrayList

The verification confirms the concern. While the service uses ConcurrentHashMap for thread-safe map operations, it uses a regular ArrayList for the cart items which is not thread-safe. This can lead to race conditions when multiple threads attempt to modify the same user's cart simultaneously.

Specific issues:

  • The getOrDefault() and subsequent put() operations are not atomic
  • The ArrayList operations (add()) are not thread-safe
  • Multiple threads could read and modify the same list concurrently

To fix this, consider:

  • Using Collections.synchronizedList() to wrap the ArrayList
  • Using CopyOnWriteArrayList for better concurrent read performance
  • Implementing proper synchronization around the cart modification operations
🔗 Analysis chain

Concurrent updates
Adding models to the cart can be subject to race conditions. While ConcurrentHashMap is thread-safe, the list itself can be a regular ArrayList, which is not thread-safe.


Let me check the implementation of ModelCartService to understand the concurrency handling better.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching for usage of `addToCart` in the codebase to confirm concurrency safety or synchronization approach
rg -A 5 "addToCart"

Length of output: 2071


Script:

#!/bin/bash
# Check the ModelCartService class implementation for ConcurrentHashMap and any synchronization
rg -A 10 "class ModelCartService"

Length of output: 1094


/**
* 장바구니 전체 삭제 (옵션)
*/
public void clearCart(String userId) {
userCartMap.remove(userId);
}

/**
* 장바구니에서 특정 모델 삭제 (옵션)
*/
public List<DataModel> removeModel(String userId, String modelId) {
List<DataModel> cartList = userCartMap.get(userId);
if (cartList == null) {
return Collections.emptyList();
}

cartList.removeIf(m -> m.getId().equals(modelId));
userCartMap.put(userId, cartList);
return cartList;
}
}
Loading