From 06f6d1a200a9cd3789b1ee15eb4fe1a77606ba46 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 17:46:55 +0900 Subject: [PATCH 1/7] feat: Get Me Profile API #10 --- .../user/controller/UserController.java | 24 +++++++++++++++ .../user/dto/response/GetMeResponseDto.java | 29 +++++++++++++++++++ .../sopt/comfit/user/service/UserService.java | 23 +++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/main/java/sopt/comfit/user/controller/UserController.java create mode 100644 src/main/java/sopt/comfit/user/dto/response/GetMeResponseDto.java create mode 100644 src/main/java/sopt/comfit/user/service/UserService.java diff --git a/src/main/java/sopt/comfit/user/controller/UserController.java b/src/main/java/sopt/comfit/user/controller/UserController.java new file mode 100644 index 0000000..05b6afe --- /dev/null +++ b/src/main/java/sopt/comfit/user/controller/UserController.java @@ -0,0 +1,24 @@ +package sopt.comfit.user.controller; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sopt.comfit.global.annotation.LoginUser; +import sopt.comfit.user.dto.response.GetMeResponseDto; +import sopt.comfit.user.service.UserService; + +@RestController +@RequestMapping("/api/v1/me") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping + @SecurityRequirement(name = "JWT") + public GetMeResponseDto getMe (@LoginUser Long userId){ + return userService.getMe(userId); + } +} diff --git a/src/main/java/sopt/comfit/user/dto/response/GetMeResponseDto.java b/src/main/java/sopt/comfit/user/dto/response/GetMeResponseDto.java new file mode 100644 index 0000000..93f35eb --- /dev/null +++ b/src/main/java/sopt/comfit/user/dto/response/GetMeResponseDto.java @@ -0,0 +1,29 @@ +package sopt.comfit.user.dto.response; + +import sopt.comfit.global.enums.EIndustry; +import sopt.comfit.user.domain.EEducationLevel; +import sopt.comfit.user.domain.EJob; +import sopt.comfit.user.domain.User; + +public record GetMeResponseDto( + + String name, + + String email, + + EEducationLevel educationLevel, + + EIndustry firstIndustry, + + EJob fistJob +) { + public static GetMeResponseDto from(User user) { + return new GetMeResponseDto( + user.getName(), + user.getEmail(), + user.getEducationLevel(), + user.getFirstIndustry(), + user.getFirstJob() + ); + } +} diff --git a/src/main/java/sopt/comfit/user/service/UserService.java b/src/main/java/sopt/comfit/user/service/UserService.java new file mode 100644 index 0000000..9867ccf --- /dev/null +++ b/src/main/java/sopt/comfit/user/service/UserService.java @@ -0,0 +1,23 @@ +package sopt.comfit.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import sopt.comfit.global.exception.BaseException; +import sopt.comfit.user.domain.User; +import sopt.comfit.user.domain.UserRepository; +import sopt.comfit.user.dto.response.GetMeResponseDto; +import sopt.comfit.user.exception.UserErrorCode; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public GetMeResponseDto getMe (Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> BaseException.type(UserErrorCode.USER_NOT_FOUND)); + + return GetMeResponseDto.from(user); + } +} From ae73452a1e3c02cad41ec02c5b2c9a1839100427 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 19:40:41 +0900 Subject: [PATCH 2/7] feat: CompanyBookmark Create Delete API #10 --- .../company/domain/CompanyRepository.java | 6 ++++ .../company/exception/CompanyErrorCode.java | 17 ++++++++++ .../global/advice/GlobalExceptionHandler.java | 12 +++++++ .../global/exception/CommonErrorCode.java | 1 + .../report/domain/AIReportRepository.java | 9 +++++ .../user/controller/UserController.java | 22 +++++++++++-- .../sopt/comfit/user/domain/UserCompany.java | 16 +++++++++ .../user/domain/UserCompanyRepository.java | 9 +++++ .../user/exception/UserCompanyErrorCode.java | 17 ++++++++++ .../comfit/user/exception/UserErrorCode.java | 4 +-- .../sopt/comfit/user/service/UserService.java | 33 +++++++++++++++++++ 11 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/main/java/sopt/comfit/company/domain/CompanyRepository.java create mode 100644 src/main/java/sopt/comfit/company/exception/CompanyErrorCode.java create mode 100644 src/main/java/sopt/comfit/report/domain/AIReportRepository.java create mode 100644 src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java create mode 100644 src/main/java/sopt/comfit/user/exception/UserCompanyErrorCode.java diff --git a/src/main/java/sopt/comfit/company/domain/CompanyRepository.java b/src/main/java/sopt/comfit/company/domain/CompanyRepository.java new file mode 100644 index 0000000..47b44cb --- /dev/null +++ b/src/main/java/sopt/comfit/company/domain/CompanyRepository.java @@ -0,0 +1,6 @@ +package sopt.comfit.company.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CompanyRepository extends JpaRepository { +} diff --git a/src/main/java/sopt/comfit/company/exception/CompanyErrorCode.java b/src/main/java/sopt/comfit/company/exception/CompanyErrorCode.java new file mode 100644 index 0000000..4f1feb3 --- /dev/null +++ b/src/main/java/sopt/comfit/company/exception/CompanyErrorCode.java @@ -0,0 +1,17 @@ +package sopt.comfit.company.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import sopt.comfit.global.exception.ErrorCode; + +@Getter +@RequiredArgsConstructor +public enum CompanyErrorCode implements ErrorCode { + + COMPANY_NOT_FOUND(HttpStatus.NOT_FOUND, "COMPANY_404_001", "해당하는 회사를 찾을 수 없습니다"); + + private final HttpStatus status; + private final String prefix; + private final String message; +} diff --git a/src/main/java/sopt/comfit/global/advice/GlobalExceptionHandler.java b/src/main/java/sopt/comfit/global/advice/GlobalExceptionHandler.java index d11e5a8..8fad32b 100644 --- a/src/main/java/sopt/comfit/global/advice/GlobalExceptionHandler.java +++ b/src/main/java/sopt/comfit/global/advice/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import org.springframework.beans.TypeMismatchException; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; @@ -169,6 +170,17 @@ public ResponseEntity handleMediaTypeNotSupported(HttpMedia return convert(CommonErrorCode.NOT_SUPPORTED_MEDIA_TYPE_ERROR); } + /** + *데이터 정합성 예외 + *unique key, FK, NOT NULL 위반 + */ + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation(DataIntegrityViolationException e) { + + log.warn("Data integrity violation: {}", e.getMessage()); + return convert(CommonErrorCode.DATA_INTEGRITY_VIOLATION); + } + /** * 예상치 못한 서버 오류 처리 (500) * 위의 핸들러들로 처리되지 않은 모든 RuntimeException diff --git a/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java b/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java index c6e24fc..f7324ec 100644 --- a/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java +++ b/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java @@ -16,6 +16,7 @@ public enum CommonErrorCode implements ErrorCode { NOT_FOUND_URI(HttpStatus.NOT_FOUND, "COMMON_006", "존재하지 않는 URI입니다."), NOT_SUPPORTED_METHOD_ERROR(HttpStatus.METHOD_NOT_ALLOWED, "COMMON_007", "지원하지 않는 HTTP 메서드입니다."), NOT_SUPPORTED_MEDIA_TYPE_ERROR(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "COMMON_008", "지원하지 않는 미디어 타입입니다."), + DATA_INTEGRITY_VIOLATION(HttpStatus.BAD_REQUEST, "COMMON_009", "데이터 정합성 오류입니다"), // ==== 인증/인가 에러 (4xx) ==== // ==== 인증 에러 (4xx) ==== diff --git a/src/main/java/sopt/comfit/report/domain/AIReportRepository.java b/src/main/java/sopt/comfit/report/domain/AIReportRepository.java new file mode 100644 index 0000000..7d27643 --- /dev/null +++ b/src/main/java/sopt/comfit/report/domain/AIReportRepository.java @@ -0,0 +1,9 @@ +package sopt.comfit.report.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AIReportRepository extends JpaRepository { + boolean existsByCompanyIdAndUserId(Long companyId, Long userId); +} diff --git a/src/main/java/sopt/comfit/user/controller/UserController.java b/src/main/java/sopt/comfit/user/controller/UserController.java index 05b6afe..90f7a8b 100644 --- a/src/main/java/sopt/comfit/user/controller/UserController.java +++ b/src/main/java/sopt/comfit/user/controller/UserController.java @@ -2,9 +2,8 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; import sopt.comfit.global.annotation.LoginUser; import sopt.comfit.user.dto.response.GetMeResponseDto; import sopt.comfit.user.service.UserService; @@ -21,4 +20,21 @@ public class UserController { public GetMeResponseDto getMe (@LoginUser Long userId){ return userService.getMe(userId); } + + @PostMapping("/companies/{companyId}") + @SecurityRequirement(name = "JWT") + @ResponseStatus(HttpStatus.CREATED) + public Long addBookmark(@LoginUser Long userId, + @PathVariable Long companyId){ + return userService.addBookmark(userId, companyId); + } + + @DeleteMapping("/companies/{companyId}") + @SecurityRequirement(name = "JWT") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void removeBookmark(@LoginUser Long userId, + @PathVariable Long companyId) { + userService.removeBookmark(userId, companyId); + } + } diff --git a/src/main/java/sopt/comfit/user/domain/UserCompany.java b/src/main/java/sopt/comfit/user/domain/UserCompany.java index d4779ed..62e4576 100644 --- a/src/main/java/sopt/comfit/user/domain/UserCompany.java +++ b/src/main/java/sopt/comfit/user/domain/UserCompany.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import sopt.comfit.company.domain.Company; @@ -26,4 +27,19 @@ public class UserCompany extends BaseTimeEntity { @Column(name = "is_connected", nullable = false) private boolean isConnected; + + @Builder(access = AccessLevel.PRIVATE) + private UserCompany(final User user, + final Company company, + final boolean isConnected) { + this.user = user; + this.company = company; + this.isConnected = isConnected; + } + + public static UserCompany create(final User user, + final Company company, + final boolean isConnected) { + return new UserCompany(user, company, isConnected); + } } diff --git a/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java b/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java new file mode 100644 index 0000000..4e1989a --- /dev/null +++ b/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java @@ -0,0 +1,9 @@ +package sopt.comfit.user.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserCompanyRepository extends JpaRepository { + Optional findByCompanyIdAndUserId(Long companyId, Long userId); +} diff --git a/src/main/java/sopt/comfit/user/exception/UserCompanyErrorCode.java b/src/main/java/sopt/comfit/user/exception/UserCompanyErrorCode.java new file mode 100644 index 0000000..0e9650a --- /dev/null +++ b/src/main/java/sopt/comfit/user/exception/UserCompanyErrorCode.java @@ -0,0 +1,17 @@ +package sopt.comfit.user.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import sopt.comfit.global.exception.ErrorCode; + +@Getter +@RequiredArgsConstructor +public enum UserCompanyErrorCode implements ErrorCode { + + USER_COMPANY_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_COMPANY_404_001", "해당하는 북마크 기업을 찾을 수 없습니다"); + + private final HttpStatus status; + private final String prefix; + private final String message; +} diff --git a/src/main/java/sopt/comfit/user/exception/UserErrorCode.java b/src/main/java/sopt/comfit/user/exception/UserErrorCode.java index 40805dd..32a9eda 100644 --- a/src/main/java/sopt/comfit/user/exception/UserErrorCode.java +++ b/src/main/java/sopt/comfit/user/exception/UserErrorCode.java @@ -9,8 +9,8 @@ @Getter public enum UserErrorCode implements ErrorCode { - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_001", "해당하는 유저를 찾을 수 없습니다."), - INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "USER_002", "비밀번호가 일치하지 않습니다."),; + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_404_001", "해당하는 유저를 찾을 수 없습니다."), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "USER_400_002", "비밀번호가 일치하지 않습니다."),; diff --git a/src/main/java/sopt/comfit/user/service/UserService.java b/src/main/java/sopt/comfit/user/service/UserService.java index 9867ccf..68a4410 100644 --- a/src/main/java/sopt/comfit/user/service/UserService.java +++ b/src/main/java/sopt/comfit/user/service/UserService.java @@ -2,10 +2,18 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.comfit.company.domain.Company; +import sopt.comfit.company.domain.CompanyRepository; +import sopt.comfit.company.exception.CompanyErrorCode; import sopt.comfit.global.exception.BaseException; +import sopt.comfit.report.domain.AIReportRepository; import sopt.comfit.user.domain.User; +import sopt.comfit.user.domain.UserCompany; +import sopt.comfit.user.domain.UserCompanyRepository; import sopt.comfit.user.domain.UserRepository; import sopt.comfit.user.dto.response.GetMeResponseDto; +import sopt.comfit.user.exception.UserCompanyErrorCode; import sopt.comfit.user.exception.UserErrorCode; @Service @@ -13,11 +21,36 @@ public class UserService { private final UserRepository userRepository; + private final CompanyRepository companyRepository; + private final UserCompanyRepository userCompanyRepository; + private final AIReportRepository aIReportRepository; + @Transactional(readOnly = true) public GetMeResponseDto getMe (Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> BaseException.type(UserErrorCode.USER_NOT_FOUND)); return GetMeResponseDto.from(user); } + + @Transactional + public Long addBookmark(Long userId, Long companyId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> BaseException.type(UserErrorCode.USER_NOT_FOUND)); + Company company = companyRepository.findById(companyId) + .orElseThrow(() -> BaseException.type(CompanyErrorCode.COMPANY_NOT_FOUND)); + + boolean isConnected = aIReportRepository.existsByCompanyIdAndUserId(companyId, userId); + UserCompany userCompany = userCompanyRepository.save(UserCompany.create(user, company, isConnected)); + + return userCompany.getId(); + } + + @Transactional + public void removeBookmark(Long userId, Long companyId) { + UserCompany userCompany = userCompanyRepository.findByCompanyIdAndUserId(companyId, userId) + .orElseThrow(() -> BaseException.type(UserCompanyErrorCode.USER_COMPANY_NOT_FOUND)); + + userCompanyRepository.delete(userCompany); + } } From b7b0709d7a1f6104cc22c9677d8b488ac265d2f9 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 20:23:09 +0900 Subject: [PATCH 3/7] feat: Get BookmarkCompany API #10 --- .../global/exception/CommonErrorCode.java | 1 + .../user/controller/UserController.java | 16 +++++++++++++ .../user/domain/UserCompanyRepository.java | 8 +++++++ .../user/dto/response/GetBookmarkCompany.java | 24 +++++++++++++++++++ .../sopt/comfit/user/service/UserService.java | 19 +++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java diff --git a/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java b/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java index f7324ec..ac2de60 100644 --- a/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java +++ b/src/main/java/sopt/comfit/global/exception/CommonErrorCode.java @@ -17,6 +17,7 @@ public enum CommonErrorCode implements ErrorCode { NOT_SUPPORTED_METHOD_ERROR(HttpStatus.METHOD_NOT_ALLOWED, "COMMON_007", "지원하지 않는 HTTP 메서드입니다."), NOT_SUPPORTED_MEDIA_TYPE_ERROR(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "COMMON_008", "지원하지 않는 미디어 타입입니다."), DATA_INTEGRITY_VIOLATION(HttpStatus.BAD_REQUEST, "COMMON_009", "데이터 정합성 오류입니다"), + NOT_SUPPORTED_SORT_TYPE(HttpStatus.BAD_REQUEST, "COMMON_010", "지원하지 않는 정렬 타입입니다."), // ==== 인증/인가 에러 (4xx) ==== // ==== 인증 에러 (4xx) ==== diff --git a/src/main/java/sopt/comfit/user/controller/UserController.java b/src/main/java/sopt/comfit/user/controller/UserController.java index 90f7a8b..edd53ec 100644 --- a/src/main/java/sopt/comfit/user/controller/UserController.java +++ b/src/main/java/sopt/comfit/user/controller/UserController.java @@ -2,9 +2,16 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import sopt.comfit.global.annotation.LoginUser; +import sopt.comfit.global.dto.PageDto; +import sopt.comfit.global.enums.ESort; +import sopt.comfit.user.dto.response.GetBookmarkCompany; import sopt.comfit.user.dto.response.GetMeResponseDto; import sopt.comfit.user.service.UserService; @@ -37,4 +44,13 @@ public void removeBookmark(@LoginUser Long userId, userService.removeBookmark(userId, companyId); } + @GetMapping("/companies") + @SecurityRequirement(name = "JWT") + public PageDto getBookmarkCompany(@LoginUser Long userId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "LATEST") ESort sort){ + Pageable pageable = PageRequest.of(page, 6); + return userService.getBookmarkCompany(userId, sort, pageable); + } + } diff --git a/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java b/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java index 4e1989a..a4f36b8 100644 --- a/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java +++ b/src/main/java/sopt/comfit/user/domain/UserCompanyRepository.java @@ -1,9 +1,17 @@ package sopt.comfit.user.domain; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserCompanyRepository extends JpaRepository { Optional findByCompanyIdAndUserId(Long companyId, Long userId); + + Page findByUserIdOrderByCreatedAtAsc(Long userId, Pageable pageable); + + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); + + Page findByUserIdOrderByCompanyName(Long companyId, Pageable pageable); } diff --git a/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java b/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java new file mode 100644 index 0000000..ccc21db --- /dev/null +++ b/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java @@ -0,0 +1,24 @@ +package sopt.comfit.user.dto.response; + +import sopt.comfit.user.domain.UserCompany; + +import java.time.LocalDate; + +public record GetBookmarkCompany( + Long id, + + String name, + + LocalDate createdAt, + + boolean isConnected +) { + public static GetBookmarkCompany from(UserCompany userCompany){ + return new GetBookmarkCompany( + userCompany.getId(), + userCompany.getCompany().getName(), + userCompany.getCreatedAt().toLocalDate(), + userCompany.isConnected() + ); + } +} diff --git a/src/main/java/sopt/comfit/user/service/UserService.java b/src/main/java/sopt/comfit/user/service/UserService.java index 68a4410..ed4a745 100644 --- a/src/main/java/sopt/comfit/user/service/UserService.java +++ b/src/main/java/sopt/comfit/user/service/UserService.java @@ -1,17 +1,23 @@ package sopt.comfit.user.service; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sopt.comfit.company.domain.Company; import sopt.comfit.company.domain.CompanyRepository; import sopt.comfit.company.exception.CompanyErrorCode; +import sopt.comfit.global.dto.PageDto; +import sopt.comfit.global.enums.ESort; import sopt.comfit.global.exception.BaseException; +import sopt.comfit.global.exception.CommonErrorCode; import sopt.comfit.report.domain.AIReportRepository; import sopt.comfit.user.domain.User; import sopt.comfit.user.domain.UserCompany; import sopt.comfit.user.domain.UserCompanyRepository; import sopt.comfit.user.domain.UserRepository; +import sopt.comfit.user.dto.response.GetBookmarkCompany; import sopt.comfit.user.dto.response.GetMeResponseDto; import sopt.comfit.user.exception.UserCompanyErrorCode; import sopt.comfit.user.exception.UserErrorCode; @@ -53,4 +59,17 @@ public void removeBookmark(Long userId, Long companyId) { userCompanyRepository.delete(userCompany); } + + @Transactional(readOnly = true) + public PageDto getBookmarkCompany (Long userId, ESort sort, Pageable pageable) { + + Page page = switch (sort) { + case LATEST -> userCompanyRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable); + case OLDEST -> userCompanyRepository.findByUserIdOrderByCreatedAtAsc(userId, pageable); + case NAME -> userCompanyRepository.findByUserIdOrderByCompanyName(userId, pageable); + default -> throw BaseException.type(CommonErrorCode.NOT_SUPPORTED_SORT_TYPE); + }; + + return PageDto.from(page.map(GetBookmarkCompany::from)); + } } From f921d90f714fced790e93ce1804733542b0f1327 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 20:34:17 +0900 Subject: [PATCH 4/7] docs: UserSwagger Docs Write #10 --- .../user/controller/UserController.java | 25 ++-- .../comfit/user/controller/UserSwagger.java | 135 ++++++++++++++++++ 2 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 src/main/java/sopt/comfit/user/controller/UserSwagger.java diff --git a/src/main/java/sopt/comfit/user/controller/UserController.java b/src/main/java/sopt/comfit/user/controller/UserController.java index edd53ec..99d3f02 100644 --- a/src/main/java/sopt/comfit/user/controller/UserController.java +++ b/src/main/java/sopt/comfit/user/controller/UserController.java @@ -1,13 +1,12 @@ package sopt.comfit.user.controller; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; -import lombok.extern.java.Log; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import sopt.comfit.global.annotation.LoginUser; import sopt.comfit.global.dto.PageDto; import sopt.comfit.global.enums.ESort; @@ -18,34 +17,28 @@ @RestController @RequestMapping("/api/v1/me") @RequiredArgsConstructor -public class UserController { +public class UserController implements UserSwagger { private final UserService userService; - @GetMapping - @SecurityRequirement(name = "JWT") + @Override public GetMeResponseDto getMe (@LoginUser Long userId){ return userService.getMe(userId); } - @PostMapping("/companies/{companyId}") - @SecurityRequirement(name = "JWT") - @ResponseStatus(HttpStatus.CREATED) + @Override public Long addBookmark(@LoginUser Long userId, @PathVariable Long companyId){ return userService.addBookmark(userId, companyId); } - @DeleteMapping("/companies/{companyId}") - @SecurityRequirement(name = "JWT") - @ResponseStatus(HttpStatus.NO_CONTENT) + @Override public void removeBookmark(@LoginUser Long userId, @PathVariable Long companyId) { userService.removeBookmark(userId, companyId); } - @GetMapping("/companies") - @SecurityRequirement(name = "JWT") + @Override public PageDto getBookmarkCompany(@LoginUser Long userId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "LATEST") ESort sort){ diff --git a/src/main/java/sopt/comfit/user/controller/UserSwagger.java b/src/main/java/sopt/comfit/user/controller/UserSwagger.java new file mode 100644 index 0000000..811acea --- /dev/null +++ b/src/main/java/sopt/comfit/user/controller/UserSwagger.java @@ -0,0 +1,135 @@ +package sopt.comfit.user.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import sopt.comfit.global.annotation.LoginUser; +import sopt.comfit.global.dto.CommonApiResponse; +import sopt.comfit.global.dto.CustomErrorResponse; +import sopt.comfit.global.dto.PageDto; +import sopt.comfit.global.enums.ESort; +import sopt.comfit.user.dto.response.GetBookmarkCompany; +import sopt.comfit.user.dto.response.GetMeResponseDto; + +@Tag(name = "me", description = "사용자 관련 API") +public interface UserSwagger { + + @Operation( + summary = "사용자 프로필 조회 API", + description = "사용자 프로필 조회 API입니다" + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "프로필 조회 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CommonApiResponse.class))), + + @ApiResponse(responseCode = "403", description = "권한 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "헤더값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "사용자 id 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))) + }) + @GetMapping + @SecurityRequirement(name = "JWT") + GetMeResponseDto getMe (@LoginUser Long userId); + + + @Operation( + summary = "관심 기업 북마크 API", + description = "관심 기업 북마크 API입니다" + ) + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "관심 기업 북마크 추가 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CommonApiResponse.class))), + + @ApiResponse(responseCode = "403", description = "권한 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "헤더값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "사용자 id 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "회사 id 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "400", description = "중복 저장 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))) + + }) + @PostMapping("/companies/{companyId}") + @SecurityRequirement(name = "JWT") + @ResponseStatus(HttpStatus.CREATED) + Long addBookmark(@LoginUser Long userId, + @PathVariable Long companyId); + + @Operation( + summary = "관심 기업 북마크 삭제 API", + description = "관심 기업 북마크 삭제 API입니다" + ) + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "관심 기업 북마크 삭제 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CommonApiResponse.class))), + + @ApiResponse(responseCode = "403", description = "권한 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "헤더값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "사용자 id 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "회사 id 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))) + }) + @DeleteMapping("/companies/{companyId}") + @SecurityRequirement(name = "JWT") + @ResponseStatus(HttpStatus.NO_CONTENT) + void removeBookmark(@LoginUser Long userId, + @PathVariable Long companyId); + + @Operation( + summary = "관심 기업 북마크 조회 리스트 API", + description = "관심 기업 북마크 조회 리스트 API입니다" + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "북마크 기업 리스트 조회 성공", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CommonApiResponse.class))), + + @ApiResponse(responseCode = "403", description = "권한 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "401", description = "헤더값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "404", description = "사용자 id 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))), + @ApiResponse(responseCode = "400", description = "지원하지 않는 정렬 값 오류", + content = @Content(mediaType = "application/json", + schema = @Schema(implementation = CustomErrorResponse.class))) + + }) + @GetMapping("/companies") + @SecurityRequirement(name = "JWT") + PageDto getBookmarkCompany(@LoginUser Long userId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "LATEST") ESort sort); +} From 0455c15c1c09d5be17e2eaaf403d8a43b9d60b44 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 20:38:28 +0900 Subject: [PATCH 5/7] chore: Add Loggin #10 --- .../java/sopt/comfit/user/service/UserService.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/sopt/comfit/user/service/UserService.java b/src/main/java/sopt/comfit/user/service/UserService.java index ed4a745..a92bb2b 100644 --- a/src/main/java/sopt/comfit/user/service/UserService.java +++ b/src/main/java/sopt/comfit/user/service/UserService.java @@ -1,6 +1,7 @@ package sopt.comfit.user.service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ import sopt.comfit.user.exception.UserCompanyErrorCode; import sopt.comfit.user.exception.UserErrorCode; +@Slf4j @Service @RequiredArgsConstructor public class UserService { @@ -41,6 +43,8 @@ public GetMeResponseDto getMe (Long userId) { @Transactional public Long addBookmark(Long userId, Long companyId) { + + log.info("북마크 추가 userId:{} companyId:{}", userId, companyId); User user = userRepository.findById(userId) .orElseThrow(() -> BaseException.type(UserErrorCode.USER_NOT_FOUND)); Company company = companyRepository.findById(companyId) @@ -48,12 +52,12 @@ public Long addBookmark(Long userId, Long companyId) { boolean isConnected = aIReportRepository.existsByCompanyIdAndUserId(companyId, userId); UserCompany userCompany = userCompanyRepository.save(UserCompany.create(user, company, isConnected)); - return userCompany.getId(); } @Transactional public void removeBookmark(Long userId, Long companyId) { + log.info("북마크 삭제 userId:{} companyId:{}", userId, companyId); UserCompany userCompany = userCompanyRepository.findByCompanyIdAndUserId(companyId, userId) .orElseThrow(() -> BaseException.type(UserCompanyErrorCode.USER_COMPANY_NOT_FOUND)); @@ -67,7 +71,10 @@ public PageDto getBookmarkCompany (Long userId, ESort sort, case LATEST -> userCompanyRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable); case OLDEST -> userCompanyRepository.findByUserIdOrderByCreatedAtAsc(userId, pageable); case NAME -> userCompanyRepository.findByUserIdOrderByCompanyName(userId, pageable); - default -> throw BaseException.type(CommonErrorCode.NOT_SUPPORTED_SORT_TYPE); + default -> { + log.warn("잘못된 정렬 타입 값입니다 type : {}", sort); + throw BaseException.type(CommonErrorCode.NOT_SUPPORTED_SORT_TYPE); + } }; return PageDto.from(page.map(GetBookmarkCompany::from)); From 3c00ce8606caae7053e0badde8e401d1001ee3b0 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 20:44:02 +0900 Subject: [PATCH 6/7] fix: Company Id Represent #10 --- .../sopt/comfit/user/dto/response/GetBookmarkCompany.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java b/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java index ccc21db..0bf8173 100644 --- a/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java +++ b/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java @@ -5,7 +5,7 @@ import java.time.LocalDate; public record GetBookmarkCompany( - Long id, + Long companyId, String name, @@ -15,7 +15,7 @@ public record GetBookmarkCompany( ) { public static GetBookmarkCompany from(UserCompany userCompany){ return new GetBookmarkCompany( - userCompany.getId(), + userCompany.getCompany().getId(), userCompany.getCompany().getName(), userCompany.getCreatedAt().toLocalDate(), userCompany.isConnected() From c52aa723039763527aceb6ed3659d0d15f540db0 Mon Sep 17 00:00:00 2001 From: oOccasio Date: Thu, 8 Jan 2026 20:45:19 +0900 Subject: [PATCH 7/7] refactor: UserComapny Id Add #10 --- .../comfit/user/dto/response/GetBookmarkCompany.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java b/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java index 0bf8173..3e1fe29 100644 --- a/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java +++ b/src/main/java/sopt/comfit/user/dto/response/GetBookmarkCompany.java @@ -5,16 +5,20 @@ import java.time.LocalDate; public record GetBookmarkCompany( - Long companyId, - String name, + Long id, - LocalDate createdAt, + Long companyId, - boolean isConnected + String name, + + LocalDate createdAt, + + boolean isConnected ) { public static GetBookmarkCompany from(UserCompany userCompany){ return new GetBookmarkCompany( + userCompany.getId(), userCompany.getCompany().getId(), userCompany.getCompany().getName(), userCompany.getCreatedAt().toLocalDate(),