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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.Builder;
Expand Down Expand Up @@ -52,19 +51,12 @@ public class MemberProfile extends BaseTimeEntity {
@JoinColumn(name = "affiliation_id")
private Group group;

@Column(name = "default_monthly_limit")
private BigDecimal defaultMonthlyLimit;

@Column(name = "default_daily_limit")
private BigDecimal defaultDailyLimit;


@Builder
public MemberProfile(Member member, String nickName, List<AddressEntity> addressHistory, MemberType type,
Group group) {
linkMember(member);
this.nickName = nickName;
this.addressHistory = addressHistory;
this.addressHistory = (addressHistory == null) ? new java.util.ArrayList<>() : addressHistory;
this.type = type;
this.group = group;
}
Comment on lines 55 to 62
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

member 파라미터에 대한 NPE 가능성 존재

@Builder로 생성될 때 member 인자가 null이면 linkMember(null) 호출에서 NullPointerException 이 발생합니다.
엔티티 생성 시점에 외부에서 member를 반드시 주입하지 않는 패스가 있다면 널 체크를 한 뒤 예외를 던지거나, 별도의 팩토리 메서드로 책임을 분리하는 편이 안전합니다.

-        linkMember(member);
+        if (member == null) {
+            throw new IllegalArgumentException("member 는 null 일 수 없습니다");
+        }
+        linkMember(member);
📝 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 MemberProfile(Member member, String nickName, List<AddressEntity> addressHistory, MemberType type,
Group group) {
linkMember(member);
this.nickName = nickName;
this.addressHistory = addressHistory;
this.addressHistory = (addressHistory == null) ? new java.util.ArrayList<>() : addressHistory;
this.type = type;
this.group = group;
}
public MemberProfile(Member member, String nickName, List<AddressEntity> addressHistory, MemberType type,
Group group) {
if (member == null) {
throw new IllegalArgumentException("member 는 null 일 수 없습니다");
}
linkMember(member);
this.nickName = nickName;
this.addressHistory = (addressHistory == null) ? new java.util.ArrayList<>() : addressHistory;
this.type = type;
this.group = group;
}
🤖 Prompt for AI Agents
In src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java
around lines 55 to 62, the constructor calls linkMember(member) without checking
if member is null, which can cause a NullPointerException when member is null.
Add a null check for the member parameter at the start of the constructor; if
member is null, throw an IllegalArgumentException with a clear message. This
ensures that the constructor fails fast and prevents NPEs. Alternatively,
consider creating a separate factory method that enforces non-null member
injection.

Expand Down Expand Up @@ -134,8 +126,4 @@ public void changeGroup(Group newGroup) {
this.group = newGroup;
}

public void registerDefaultBudgets(BigDecimal defaultDailyLimit, BigDecimal defaultMonthlyLimit) {
this.defaultDailyLimit = defaultDailyLimit;
this.defaultMonthlyLimit = defaultMonthlyLimit;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ public interface MemberProfileRepository extends JpaRepository<MemberProfile, Lo

void deleteMemberProfileByMember(Member member);

@EntityGraph(attributePaths = {"member", "addressHistory, group"})
@EntityGraph(attributePaths = {"member", "addressHistory", "group"})
Optional<MemberProfile> findMemberProfileEntityGraphById(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -61,10 +62,11 @@ public void registerDefaultDailyBudgetBy(Long profileId, Long dailyLimit, LocalD
MemberProfile profile = memberProfileRepository.findById(profileId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 프로필로 접근"));
// 일일 예산은 오늘 ~ 이번달 말일까지 디폴트 daily Limit로 여러 개 생성해준다.
List<DailyBudget> budgets = new ArrayList<>();
for (LocalDate date = startDate; date.getMonth() == startDate.getMonth(); date = date.plusDays(1)) {
DailyBudget dailyBudget = new DailyBudget(profile, BigDecimal.valueOf(dailyLimit), date);
budgetRepository.save(dailyBudget);
budgets.add(new DailyBudget(profile, BigDecimal.valueOf(dailyLimit), date));
}
budgetRepository.saveAll(budgets);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.stcom.smartmealtable.repository.GroupRepository;
import com.stcom.smartmealtable.repository.MemberProfileRepository;
import com.stcom.smartmealtable.repository.MemberRepository;
import java.math.BigDecimal;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -104,11 +103,4 @@ public void deleteAddress(Long profileId, Long addressEntityId) {
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다."));
profile.removeAddress(addressEntity);
}

@Transactional
public void registerDefaultBudgets(Long profileId, Long dailyLimit, Long monthlyLimit) {
MemberProfile profile = memberProfileRepository.findById(profileId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 프로필입니다"));
profile.registerDefaultBudgets(BigDecimal.valueOf(dailyLimit), BigDecimal.valueOf(monthlyLimit));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,7 @@ public class MemberService {
private final MemberProfileRepository memberProfileRepository;
private final SocialAccountRepository socialAccountRepository;
private final AddressEntityRepository addressEntityRepository;

/**
* 이메일 중복 검사를 수행합니다. 동일한 이메일을 가진 회원이 이미 존재하면 예외를 발생시킵니다.
*
* @param email 중복 검사할 이메일
* @throws IllegalArgumentException 이메일이 이미 존재하는 경우
*/

public void validateDuplicatedEmail(String email) {
memberRepository.findByEmail(email).ifPresent(member -> {
throw new IllegalArgumentException("이미 존재하는 이메일 입니다");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.stcom.smartmealtable.web.controller;

import com.stcom.smartmealtable.domain.member.Member;
import com.stcom.smartmealtable.exception.PasswordPolicyException;
import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto;
import com.stcom.smartmealtable.security.JwtTokenService;
import com.stcom.smartmealtable.service.MemberService;
import com.stcom.smartmealtable.service.TermService;
import com.stcom.smartmealtable.service.dto.MemberDto;
import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto;
import com.stcom.smartmealtable.web.argumentresolver.UserContext;
import com.stcom.smartmealtable.web.dto.ApiResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

/**
* 인증(로그인, 회원가입) 관련 API.
*/
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class AuthController {

private final MemberService memberService;
private final JwtTokenService jwtTokenService;
private final TermService termService;

@GetMapping("/email/check")
public ResponseEntity<ApiResponse<Void>> checkEmail(@Email @RequestParam String email) {
memberService.validateDuplicatedEmail(email);
return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent());
}

@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/signup")
public ApiResponse<JwtTokenResponseDto> signUp(@Valid @RequestBody SignUpRequest request)
throws PasswordPolicyException {
memberService.validateDuplicatedEmail(request.getEmail());
memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword());

Member member = Member.builder()
.fullName(request.getFullName())
.email(request.getEmail())
.rawPassword(request.getPassword())
.build();

memberService.saveMember(member);
JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId(), null);
tokenDto.setNewUser(true);
return ApiResponse.createSuccess(tokenDto);
}
Comment on lines +52 to +67
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

비밀번호 평문 저장 가능성

Member.builder().rawPassword(request.getPassword()) 로 전달­된 값이 서비스 계층에서 암호화된다는 보장이 코드상 명확하지 않습니다. 저장 전 반드시 BCrypt 등으로 인코딩하고 평문을 어디에도 보존하지 않도록 확인하세요.

🤖 Prompt for AI Agents
In src/main/java/com/stcom/smartmealtable/web/controller/AuthController.java
between lines 52 and 67, the password is passed as rawPassword to the Member
builder without clear encryption. To fix this, ensure the password is encoded
using BCrypt or a similar encoder before setting it in the Member object, so no
plaintext password is stored or passed beyond this point.


@PostMapping("/signup/terms")
public ApiResponse<Void> agreeTerms(@UserContext MemberDto memberDto,
@RequestBody List<TermAgreementRequest> agreements) {
termService.agreeTerms(
memberDto.getMemberId(),
agreements.stream()
.map(dto -> new TermAgreementRequestDto(dto.getTermId(), dto.getIsAgreed()))
.toList()
);
return ApiResponse.createSuccessWithNoContent();
}
Comment on lines +69 to +79
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

약관 동의 로직의 입력 검증 부족

agreements 리스트에 대한 @Valid·@NotNull 검증이 없습니다. null 리스트 혹은 요소 누락 시 의도치 않은 DB 상태가 발생할 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/com/stcom/smartmealtable/web/controller/AuthController.java
around lines 69 to 79, the agreeTerms method lacks input validation for the
agreements list. Add @Valid and @NotNull annotations to the agreements parameter
to ensure the list is not null and its elements are validated. This prevents
unintended database states caused by null or incomplete agreement data.


@DeleteMapping("/signup")
public ApiResponse<Void> cancelSignUp(@UserContext MemberDto memberDto) {
memberService.deleteByMemberId(memberDto.getMemberId());
return ApiResponse.createSuccessWithNoContent();
}

@Data
@AllArgsConstructor
public static class SignUpRequest {
@Email
private String email;
private String password;
private String confirmPassword;
private String fullName;
}

@Data
@AllArgsConstructor
public static class TermAgreementRequest {
private Long termId;
private Boolean isAgreed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.stcom.smartmealtable.web.controller;

import com.stcom.smartmealtable.infrastructure.SocialAuthService;
import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto;
import com.stcom.smartmealtable.infrastructure.dto.TokenDto;
import com.stcom.smartmealtable.security.JwtBlacklistService;
import com.stcom.smartmealtable.security.JwtTokenService;
import com.stcom.smartmealtable.service.LoginService;
import com.stcom.smartmealtable.service.dto.AuthResultDto;
import com.stcom.smartmealtable.service.dto.MemberDto;
import com.stcom.smartmealtable.web.argumentresolver.UserContext;
import com.stcom.smartmealtable.web.dto.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 인증 토큰 관련 API (로그인 / 로그아웃 / 토큰 리프레시 / 소셜 로그인).
*/
@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class AuthTokenController {

private final LoginService loginService;
private final JwtTokenService jwtTokenService;
private final JwtBlacklistService jwtBlacklistService;
private final SocialAuthService socialAuthService;

@PostMapping("/login")
public ApiResponse<JwtTokenResponseDto> login(@RequestBody EmailLoginRequest request) {
AuthResultDto authResultDto = loginService.loginWithEmail(request.getEmail(), request.getPassword());
JwtTokenResponseDto jwtDto = jwtTokenService.createTokenDto(authResultDto.getMemberId(),
authResultDto.getProfileId());
if (authResultDto.isNewUser()) {
jwtDto.setNewUser(true);
}
return ApiResponse.createSuccess(jwtDto);
}

@PostMapping("/logout")
public ApiResponse<Void> logout(HttpServletRequest request) {
String jwt = request.getHeader("Authorization");
if (Objects.nonNull(jwt)) {
jwtBlacklistService.addToBlacklist(jwt);
}
return ApiResponse.createSuccessWithNoContent();
}
Comment on lines +51 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bearer 토큰 파싱 오류 가능성

Authorization 헤더 전체를 블랙리스트에 저장하면 "Bearer " 접두사가 포함돼 검증 시 매치에 실패할 수 있습니다. 접두사를 제거하고 공백·대소문자를 방어적으로 처리하세요.

-String jwt = request.getHeader("Authorization");
-if (Objects.nonNull(jwt)) {
-    jwtBlacklistService.addToBlacklist(jwt);
-}
+String bearer = request.getHeader("Authorization");
+if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
+    String jwt = bearer.substring(7);
+    jwtBlacklistService.addToBlacklist(jwt);
+}
📝 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
@PostMapping("/logout")
public ApiResponse<Void> logout(HttpServletRequest request) {
String jwt = request.getHeader("Authorization");
if (Objects.nonNull(jwt)) {
jwtBlacklistService.addToBlacklist(jwt);
}
return ApiResponse.createSuccessWithNoContent();
}
@PostMapping("/logout")
public ApiResponse<Void> logout(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
String jwt = bearer.substring(7);
jwtBlacklistService.addToBlacklist(jwt);
}
return ApiResponse.createSuccessWithNoContent();
}
🤖 Prompt for AI Agents
In
src/main/java/com/stcom/smartmealtable/web/controller/AuthTokenController.java
around lines 51 to 58, the logout method adds the entire Authorization header
value to the blacklist, including the "Bearer " prefix. To fix this, parse the
header to remove the "Bearer " prefix in a case-insensitive manner and trim any
whitespace before adding the token to the blacklist. This ensures the stored
token matches the format used during validation.


@PostMapping("/oauth2/code")
public ApiResponse<JwtTokenResponseDto> socialLogin(@RequestBody SocialLoginRequest request) {
TokenDto token = socialAuthService.getTokenResponse(request.getProvider().toLowerCase(),
request.getAuthorizationCode());
AuthResultDto authResultDto = loginService.socialLogin(token);
JwtTokenResponseDto jwtDto = jwtTokenService.createTokenDto(authResultDto.getMemberId(),
authResultDto.getProfileId());
if (authResultDto.isNewUser()) {
jwtDto.setNewUser(true);
}
return ApiResponse.createSuccess(jwtDto);
}

@PostMapping("/token/refresh")
public ApiResponse<AccessTokenRefreshResponse> refreshAccessToken(@UserContext MemberDto memberDto,
@RequestBody RefreshTokenRequest request) {
String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId());
return ApiResponse.createSuccess(new AccessTokenRefreshResponse(accessToken, 3600, "Bearer"));
}
Comment on lines +73 to +78
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

리프레시 토큰 검증이 전혀 이루어지지 않습니다

refreshAccessToken 는 전달된 리프레시 토큰을 무시하고 새 액세스 토큰을 발급합니다. 악성 사용자가 만료된/위조된 토큰으로도 무제한 재발급이 가능해집니다.

-@PostMapping("/token/refresh")
-public ApiResponse<AccessTokenRefreshResponse> refreshAccessToken(@UserContext MemberDto memberDto,
-                                                                  @RequestBody RefreshTokenRequest request) {
-    String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId());
+@PostMapping("/token/refresh")
+public ApiResponse<AccessTokenRefreshResponse> refreshAccessToken(@RequestBody RefreshTokenRequest request) {
+    jwtTokenService.validateRefreshToken(request.getRefreshToken());   // 서명·만료 검증
+    String accessToken = jwtTokenService.createAccessTokenFromRefresh(request.getRefreshToken());
     return ApiResponse.createSuccess(new AccessTokenRefreshResponse(accessToken, 3600, "Bearer"));
 }
📝 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
@PostMapping("/token/refresh")
public ApiResponse<AccessTokenRefreshResponse> refreshAccessToken(@UserContext MemberDto memberDto,
@RequestBody RefreshTokenRequest request) {
String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId());
return ApiResponse.createSuccess(new AccessTokenRefreshResponse(accessToken, 3600, "Bearer"));
}
@PostMapping("/token/refresh")
public ApiResponse<AccessTokenRefreshResponse> refreshAccessToken(@RequestBody RefreshTokenRequest request) {
jwtTokenService.validateRefreshToken(request.getRefreshToken()); // 서명·만료 검증
String accessToken = jwtTokenService.createAccessTokenFromRefresh(request.getRefreshToken());
return ApiResponse.createSuccess(new AccessTokenRefreshResponse(accessToken, 3600, "Bearer"));
}
🤖 Prompt for AI Agents
In
src/main/java/com/stcom/smartmealtable/web/controller/AuthTokenController.java
around lines 73 to 78, the refreshAccessToken method does not validate the
provided refresh token before issuing a new access token. To fix this, add logic
to verify the refresh token's validity and authenticity using jwtTokenService or
equivalent before creating and returning a new access token. If the refresh
token is invalid or expired, respond with an appropriate error instead of
issuing a new token.


@Data
@AllArgsConstructor
public static class EmailLoginRequest {
@NotEmpty
@Email
private String email;
@NotEmpty
private String password;
}

@Data
@AllArgsConstructor
public static class SocialLoginRequest {
@NotEmpty
private String provider;
@NotEmpty
private String authorizationCode;
}

@Data
public static class RefreshTokenRequest {
@NotEmpty
private String refreshToken;
}

@Data
@AllArgsConstructor
public static class AccessTokenRefreshResponse {
private String accessToken;
private int expiresIn;
private String tokenType;
}
}
Loading