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 @@ -16,8 +16,9 @@ public enum SuccessStatus implements BaseStatus {
LOGOUT_SUCCESS(HttpStatus.OK, "AUTH_200", "로그아웃 성공"),
REISSUE_TOKEN_SUCCESS(HttpStatus.OK, "AUTH_200", "토큰 재발급 성공"),
WITHDRAW_SUCCESS(HttpStatus.OK, "AUTH_200", "회원탈퇴 성공"),
SIGNUP_SUCCESS(HttpStatus.CREATED, "AUTH_201", "회원가입 성공");
SIGNUP_SUCCESS(HttpStatus.CREATED, "AUTH_201", "회원가입 성공"),

PANELTY_CHECK_SUCCESS(HttpStatus.OK, "PANELTY_200", "징계내역 조회 성공");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

오타: PANELTYPENALTY

PANELTY_CHECK_SUCCESS와 코드 "PANELTY_200" 모두 "PENALTY"의 오타입니다. API 응답 코드에 포함되는 값이므로 수정이 필요합니다.

🐛 수정 제안
-    PANELTY_CHECK_SUCCESS(HttpStatus.OK, "PANELTY_200", "징계내역 조회 성공");
+    PENALTY_CHECK_SUCCESS(HttpStatus.OK, "PENALTY_200", "징계내역 조회 성공");
📝 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
PANELTY_CHECK_SUCCESS(HttpStatus.OK, "PANELTY_200", "징계내역 조회 성공");
PENALTY_CHECK_SUCCESS(HttpStatus.OK, "PENALTY_200", "징계내역 조회 성공");
🤖 Prompt for AI Agents
In `@src/main/java/ssurent/ssurentbe/common/status/SuccessStatus.java` at line 21,
Rename the enum constant PANELTY_CHECK_SUCCESS to PENALTY_CHECK_SUCCESS and fix
the associated code string "PANELTY_200" to "PENALTY_200" in SuccessStatus;
update all usages/references of PANELTY_CHECK_SUCCESS throughout the codebase
(imports, switch/case, tests, JSON mapping, API docs) to the new
PENALTY_CHECK_SUCCESS identifier so compilation and external responses continue
to work.

private final HttpStatus httpStatus;
private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import ssurent.ssurentbe.common.base.BaseResponse;
import ssurent.ssurentbe.common.status.SuccessStatus;
import ssurent.ssurentbe.domain.users.controller.docs.AuthApiDocs;
import ssurent.ssurentbe.domain.users.dto.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.TokenResponse;
import ssurent.ssurentbe.domain.users.dto.request.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.request.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.response.TokenResponse;
import ssurent.ssurentbe.domain.users.service.AuthService;

@RestController
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

우리 CQRS 패턴으로 작업중이라서 Request/Response Dto 패키지 따로 만들어서 관리해야 해.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ssurent.ssurentbe.domain.users.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import ssurent.ssurentbe.common.base.BaseResponse;
import ssurent.ssurentbe.common.status.SuccessStatus;
import ssurent.ssurentbe.domain.users.dto.request.UpdatePhoneNumberRequest;
import ssurent.ssurentbe.domain.users.dto.response.UserInfoResponse;
import ssurent.ssurentbe.domain.users.dto.response.UserPenaltyResponse;
import ssurent.ssurentbe.domain.users.service.UserQueryService;
import ssurent.ssurentbe.domain.users.service.UserCommandService;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/api/users")
public class UserController {
private final UserQueryService userQueryService;
private final UserCommandService userCommandService;

@GetMapping
public UserInfoResponse getMyInfo(@AuthenticationPrincipal UserDetails userDetails) {
return userQueryService.getMyInfo(userDetails.getUsername());
}
Comment on lines +26 to +29
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getMyInfo 응답이 BaseResponse로 래핑되지 않았습니다.

getMyPenaltiesAuthController의 다른 엔드포인트들은 모두 BaseResponse로 래핑하여 반환하는데, getMyInfoUserInfoResponse를 직접 반환합니다. API 응답 형식의 일관성을 위해 동일한 패턴을 적용하는 것이 좋습니다.

♻️ 수정 제안
     `@GetMapping`
-    public UserInfoResponse getMyInfo(`@AuthenticationPrincipal` UserDetails userDetails) {
-        return userQueryService.getMyInfo(userDetails.getUsername());
+    public ResponseEntity<BaseResponse<UserInfoResponse>> getMyInfo(`@AuthenticationPrincipal` UserDetails userDetails) {
+        UserInfoResponse data = userQueryService.getMyInfo(userDetails.getUsername());
+        SuccessStatus status = SuccessStatus.COMM_SUCCESS_STATUS;
+        return ResponseEntity.status(status.getHttpStatus())
+                .body(BaseResponse.success(status, data));
     }
🤖 Prompt for AI Agents
In `@src/main/java/ssurent/ssurentbe/domain/users/controller/UserController.java`
around lines 26 - 29, The getMyInfo endpoint in UserController currently returns
UserInfoResponse directly; change it to return the consistent wrapper type
(BaseResponse<UserInfoResponse>) and wrap the service result
(userQueryService.getMyInfo(userDetails.getUsername())) in a BaseResponse
success payload (use the same static factory or constructor used by other
endpoints like getMyPenalties/AuthController) and update the method signature
and return statement accordingly so the controller consistently returns
BaseResponse across endpoints.


@PatchMapping("/phone-number")
public ResponseEntity<Void> updatePhoneNumber(
@AuthenticationPrincipal UserDetails userDetails,
@Valid @RequestBody UpdatePhoneNumberRequest request
) {
userCommandService.updatePhoneNumber(
userDetails.getUsername(),
request.phoneNum()
);
return ResponseEntity.noContent().build();
}
@GetMapping("/penalties")
public ResponseEntity<BaseResponse<List<UserPenaltyResponse>>> getMyPenalties(
@AuthenticationPrincipal UserDetails userDetails
) {
List<UserPenaltyResponse> data = userQueryService.getMyPenalties(userDetails.getUsername());
SuccessStatus status = SuccessStatus.PANELTY_CHECK_SUCCESS;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

PANELTY_CHECK_SUCCESS 오타 참조

SuccessStatus.java에서 PANELTYPENALTY 수정 시 이 참조도 함께 변경해야 합니다.

🤖 Prompt for AI Agents
In `@src/main/java/ssurent/ssurentbe/domain/users/controller/UserController.java`
at line 47, In UserController.java replace the misspelled enum constant
reference SuccessStatus.PANELTY_CHECK_SUCCESS with the corrected
SuccessStatus.PENALTY_CHECK_SUCCESS (and update any other occurrences in this
class) so it matches the renamed constant in SuccessStatus; ensure
imports/compilation and run a quick search for PANELTY_* usages to update them
similarly.

return ResponseEntity.status(status.getHttpStatus())
.body(BaseResponse.success(status, data));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import ssurent.ssurentbe.common.base.BaseResponse;
import ssurent.ssurentbe.domain.users.dto.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.TokenResponse;
import ssurent.ssurentbe.domain.users.dto.TokenResponseWrapper;
import ssurent.ssurentbe.domain.users.dto.request.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.request.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.response.TokenResponse;
import ssurent.ssurentbe.domain.users.dto.response.TokenResponseWrapper;

@Tag(name = "Auth", description = "인증 API")
public interface AuthApiDocs {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ssurent.ssurentbe.domain.users.dto;
package ssurent.ssurentbe.domain.users.dto.request;

public record LoginRequest(
String studentNum,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ssurent.ssurentbe.domain.users.dto;
package ssurent.ssurentbe.domain.users.dto.request;

public record SignupRequest(
String studentNum,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ssurent.ssurentbe.domain.users.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public record UpdatePhoneNumberRequest(
@NotBlank(message = "전화번호는 필수 입력값입니다.")
@Pattern(regexp = "^01[016789]-?\\d{3,4}-?\\d{4}$", message = "올바른 전화번호 형식이 아닙니다.")
String phoneNum
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ssurent.ssurentbe.domain.users.dto;
package ssurent.ssurentbe.domain.users.dto.response;

public record TokenResponse(
String accessToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ssurent.ssurentbe.domain.users.dto;
package ssurent.ssurentbe.domain.users.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import ssurent.ssurentbe.common.base.BaseResponse;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ssurent.ssurentbe.domain.users.dto.response;

import ssurent.ssurentbe.domain.users.entity.Users;
import ssurent.ssurentbe.domain.users.enums.Role;
import ssurent.ssurentbe.domain.users.enums.Status;

public record UserInfoResponse(
String studentNum,
String name,
String phoneNum,
Status status,
Role role
) {
public static UserInfoResponse from(Users user) {
return new UserInfoResponse(
user.getStudentNum(),
user.getName(),
user.getPhoneNum(),
user.getStatus(),
user.getRole()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
package ssurent.ssurentbe.domain.users.dto.response;

import ssurent.ssurentbe.domain.item.entity.Items;
import ssurent.ssurentbe.domain.users.entity.UserPenaltyLog;
import ssurent.ssurentbe.domain.users.enums.PenaltyTypes;

import java.time.LocalDateTime;

public record UserPenaltyResponse(
Long userPenaltyId,
LocalDateTime createdAt,
String itemName,
String penaltyType
Long penaltyId,
PenaltyTypes penaltyType,
Long itemId,
Long rentalHistoryId,
LocalDateTime createdAt
) {
public static UserPenaltyResponse from(UserPenaltyLog userPenaltyLog) {
Items item = userPenaltyLog.getItemsId();
String itemInfo = item.getName() + " (" + item.getItemNum() + ")";

public static UserPenaltyResponse from(UserPenaltyLog log) {
return new UserPenaltyResponse(
userPenaltyLog.getId(),
userPenaltyLog.getCreatedAt(),
itemInfo,
userPenaltyLog.getPenaltyType().getDescription()
log.getId(),
log.getPenaltyType(),
log.getItemsId() != null ? log.getItemsId().getId() : null,
log.getRentalHistoryId() != null ? log.getRentalHistoryId().getId() : null,
log.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ public void withdraw(){
this.phoneNum = null;
this.studentNum = null;
}
public void updatePhoneNumber(String phoneNum) {
this.phoneNum = phoneNum;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ssurent.ssurentbe.domain.users.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import ssurent.ssurentbe.domain.users.entity.UserPenaltyLog;
import ssurent.ssurentbe.domain.users.entity.Users;

import java.util.List;

public interface UserPenaltyLogRepository extends JpaRepository<UserPenaltyLog, Long> {

List<UserPenaltyLog> findByUserIdOrderByCreatedAtDesc(Users user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
public interface UserRepository extends JpaRepository<Users, Long> {
Optional<Users> findByStudentNum(String studentNum);
boolean existsByStudentNum(String studentNum);
Optional<Users> findByStudentNumAndDeletedFalse(String studentNum);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import ssurent.ssurentbe.common.exception.GeneralException;
import ssurent.ssurentbe.common.jwt.JwtTokenProvider;
import ssurent.ssurentbe.common.status.ErrorStatus;
import ssurent.ssurentbe.domain.users.dto.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.TokenResponse;
import ssurent.ssurentbe.domain.users.dto.request.LoginRequest;
import ssurent.ssurentbe.domain.users.dto.request.SignupRequest;
import ssurent.ssurentbe.domain.users.dto.response.TokenResponse;
import ssurent.ssurentbe.domain.users.entity.Users;
import ssurent.ssurentbe.domain.users.enums.Role;
import ssurent.ssurentbe.domain.users.enums.Status;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ssurent.ssurentbe.domain.users.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ssurent.ssurentbe.common.exception.GeneralException;
import ssurent.ssurentbe.common.status.ErrorStatus;
import ssurent.ssurentbe.domain.users.entity.Users;
import ssurent.ssurentbe.domain.users.repository.UserRepository;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserCommandService {
private final UserRepository userRepository;

public Users getUserInfo(String username) {
return userRepository.findByStudentNumAndDeletedFalse(username)
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
}
@Transactional
public void updatePhoneNumber(String username, String phoneNum) {
Users user = getUserInfo(username);

user.updatePhoneNumber(phoneNum);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

서비스도 CQRS 패턴 적용해서
UserCommandService(쓰기)랑
UserQueryService(읽기)로 나누어서 관리해야 함.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ssurent.ssurentbe.domain.users.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ssurent.ssurentbe.common.exception.GeneralException;
import ssurent.ssurentbe.common.status.ErrorStatus;
import ssurent.ssurentbe.domain.users.dto.response.UserInfoResponse;
import ssurent.ssurentbe.domain.users.dto.response.UserPenaltyResponse;
import ssurent.ssurentbe.domain.users.entity.UserPenaltyLog;
import ssurent.ssurentbe.domain.users.entity.Users;
import ssurent.ssurentbe.domain.users.repository.UserPenaltyLogRepository;
import ssurent.ssurentbe.domain.users.repository.UserRepository;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserQueryService {
private final UserRepository userRepository;
private final UserPenaltyLogRepository userPenaltyLogRepository;

public Users getUserInfo(String username) {
return userRepository.findByStudentNumAndDeletedFalse(username)
.orElseThrow(() -> new GeneralException(ErrorStatus.USER_NOT_FOUND));
}
public UserInfoResponse getMyInfo(String username) {
Users user = getUserInfo(username);
return UserInfoResponse.from(user);
}
public List<UserPenaltyResponse> getMyPenalties(String username) {
Users user = getUserInfo(username);

List<UserPenaltyLog> logs = userPenaltyLogRepository.findByUserIdOrderByCreatedAtDesc(user);

return logs.stream()
.map(UserPenaltyResponse::from)
.collect(Collectors.toList());
}
}
21 changes: 10 additions & 11 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
# Server
server:
port: 8080
port: 8080

# Profiles
spring:
profiles:
active: prod
profiles:
active: prod
Comment on lines +7 to +8
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

기본 프로파일이 prod로 하드코딩되어 있습니다.

application.yml에서 active: prod를 기본값으로 설정하면, 로컬 개발 및 CI 환경에서도 프로덕션 설정(DB, 외부 서비스 등)으로 연결됩니다. 실수로 프로덕션 데이터를 변경하거나 장애를 유발할 수 있습니다.

기본값은 local 또는 dev로 설정하고, 프로덕션 배포 시 환경 변수(SPRING_PROFILES_ACTIVE=prod) 또는 CLI 인자로 오버라이드하는 것을 권장합니다.

🛡️ 수정 제안
 spring:
   profiles:
-    active: prod
+    active: local
📝 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
profiles:
active: prod
profiles:
active: local
🤖 Prompt for AI Agents
In `@src/main/resources/application.yml` around lines 7 - 8, The application.yml
currently hardcodes the Spring profile to prod (profiles.active: prod); change
this to a safe default (e.g., profiles.active: local or dev) or remove the
active key entirely so the profile is driven by SPRING_PROFILES_ACTIVE/CLI at
runtime, and ensure deployment/CI sets SPRING_PROFILES_ACTIVE=prod for
production; update the "profiles.active" entry in application.yml and verify any
bootstrap/start scripts or CI pipelines explicitly set the production profile
when deploying.


# Swagger (Springdoc OpenAPI)
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
tagsSorter: alpha
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operations-sorter: method

# JWT
jwt:
access-token-validity: 3600000
refresh-token-validity: 604800000
access-token-validity: 3600000
refresh-token-validity: 604800000