From ce70d50f5d128a38c49fcd607d95eaea88e5fc11 Mon Sep 17 00:00:00 2001 From: hayejoon Date: Mon, 11 Aug 2025 19:03:25 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=80=EA=B2=8C=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team404/controller/StoreController.java | 89 +++++++++++++++---- .../dto/request/StoreFilterRequest.java | 26 ++++++ .../dto/response/StoreDetailResponse.java | 30 +++++++ .../dto/response/StoreSummaryResponse.java | 25 ++++++ .../lion/team404/domain/entity/ChatRoom.java | 1 + .../lion/team404/domain/entity/Store.java | 2 +- .../hufs/lion/team404/model/StoreModel.java | 42 ++++++--- .../team404/repository/StoreRepository.java | 21 ++++- .../lion/team404/service/StoreService.java | 31 ++++++- 9 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 src/main/java/hufs/lion/team404/domain/dto/request/StoreFilterRequest.java create mode 100644 src/main/java/hufs/lion/team404/domain/dto/response/StoreDetailResponse.java create mode 100644 src/main/java/hufs/lion/team404/domain/dto/response/StoreSummaryResponse.java diff --git a/src/main/java/hufs/lion/team404/controller/StoreController.java b/src/main/java/hufs/lion/team404/controller/StoreController.java index 8d63284..d970e60 100644 --- a/src/main/java/hufs/lion/team404/controller/StoreController.java +++ b/src/main/java/hufs/lion/team404/controller/StoreController.java @@ -1,29 +1,34 @@ package hufs.lion.team404.controller; - import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -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; -import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.server.ResponseStatusException; + +import hufs.lion.team404.domain.dto.request.StoreFilterRequest; import hufs.lion.team404.domain.dto.request.StoreUpdateRequestDto; import hufs.lion.team404.domain.dto.response.StoreReadResponseDto; import hufs.lion.team404.domain.dto.request.StoreCreateRequestDto; import hufs.lion.team404.domain.dto.response.ApiResponse; +import hufs.lion.team404.domain.dto.response.StoreSummaryResponse; import hufs.lion.team404.model.StoreModel; import hufs.lion.team404.oauth.jwt.UserPrincipal; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; + import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.DeleteMapping; @RestController @RequestMapping("/api/store") @@ -31,10 +36,25 @@ @Slf4j @CrossOrigin(origins = "*") @Tag(name = "업체", description = "업체 관련 API") - public class StoreController { + private final StoreModel storeModel; + private static final Set ALLOWED_SORT_KEYS = Set.of("id", "storeName", "createdAt"); + + private Pageable sanitize(Pageable p) { + var safeOrders = p.getSort().stream() + .map(o -> ALLOWED_SORT_KEYS.contains(o.getProperty()) + ? o + : new Sort.Order(o.getDirection(), "createdAt")) + .collect(Collectors.toList()); + + if (safeOrders.isEmpty()) { + safeOrders = List.of(new Sort.Order(Sort.Direction.DESC, "createdAt")); + } + return PageRequest.of(p.getPageNumber(), p.getPageSize(), Sort.by(safeOrders)); + } + @PostMapping("/create") @Operation( summary = "업체 정보 생성", @@ -43,26 +63,33 @@ public class StoreController { ) public ApiResponse createStore( @AuthenticationPrincipal UserPrincipal authentication, - @Valid @RequestBody StoreCreateRequestDto storeCreateRequestDto) { - - Long userId = authentication.getId(); - storeModel.createStore(storeCreateRequestDto, userId); - + @Valid @RequestBody StoreCreateRequestDto dto + ) { + if (authentication == null) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."); + } + storeModel.createStore(dto, authentication.getId()); return ApiResponse.success("업체 정보가 성공적으로 생성되었습니다."); } + @GetMapping @Operation(summary = "업체 목록 조회", description = "모든 업체 정보를 조회합니다.") public ApiResponse> getAllStores() { return ApiResponse.success(storeModel.getAllStores()); } + @GetMapping("/{storeId}") @Operation(summary = "업체 단건 조회", description = "업체 ID로 업체 정보를 조회합니다.") public ApiResponse getStore(@PathVariable Long storeId) { return ApiResponse.success(storeModel.getStoreById(storeId)); } + @PutMapping("/{storeId}") - @Operation(summary = "업체 정보 수정", description = "업체의 일부 정보를 수정합니다.", - security = @SecurityRequirement(name = "Bearer Authentication")) + @Operation( + summary = "업체 정보 수정", + description = "업체의 일부 정보를 수정합니다.", + security = @SecurityRequirement(name = "Bearer Authentication") + ) public ApiResponse updateStore( @PathVariable Long storeId, @AuthenticationPrincipal UserPrincipal authentication, @@ -71,6 +98,7 @@ public ApiResponse updateStore( Long userId = authentication.getId(); return ApiResponse.success(storeModel.updateStore(storeId, dto, userId)); } + @DeleteMapping("/{storeId}") @Operation( summary = "업체 삭제", @@ -86,4 +114,27 @@ public ApiResponse deleteStore( return ApiResponse.success("삭제되었습니다."); } -} \ No newline at end of file + @GetMapping("/page") + @Operation(summary = "업체 목록 조회(페이지)", description = "페이지네이션으로 업체 목록을 조회합니다.") + public ApiResponse> getStoresPage( + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) + Pageable pageable + ) { + pageable = sanitize(pageable); + return ApiResponse.success(storeModel.getStoresPage(pageable)); + } + + // 필터 조회 (keyword/category/address + 페이지네이션) + @GetMapping("/filter") + @Operation(summary = "업체 필터 조회", description = "키워드, 카테고리, 주소로 필터링하여 조회합니다.") + public ApiResponse> searchStores( + @ModelAttribute StoreFilterRequest filter, + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) + Pageable pageable + ) { + pageable = sanitize(pageable) ; + return ApiResponse.success(storeModel.searchStores(filter, pageable)); + } + + +} diff --git a/src/main/java/hufs/lion/team404/domain/dto/request/StoreFilterRequest.java b/src/main/java/hufs/lion/team404/domain/dto/request/StoreFilterRequest.java new file mode 100644 index 0000000..dc907f7 --- /dev/null +++ b/src/main/java/hufs/lion/team404/domain/dto/request/StoreFilterRequest.java @@ -0,0 +1,26 @@ +package hufs.lion.team404.domain.dto.request; + +/** + * 소상공인 목록/필터 조회용 요청 DTO + * - keyword: storeName / introduction LIKE 검색 + * - category: 정확히 일치 + * - address: 부분 일치 + */ +public record StoreFilterRequest( + String keyword, + String category, + String address +) { + + public StoreFilterRequest { + keyword = normalize(keyword); + category = normalize(category); + address = normalize(address); + } + + private static String normalize(String s) { + if (s == null) return null; + s = s.trim(); + return s.isEmpty() ? null : s; + } +} \ No newline at end of file diff --git a/src/main/java/hufs/lion/team404/domain/dto/response/StoreDetailResponse.java b/src/main/java/hufs/lion/team404/domain/dto/response/StoreDetailResponse.java new file mode 100644 index 0000000..edd7b43 --- /dev/null +++ b/src/main/java/hufs/lion/team404/domain/dto/response/StoreDetailResponse.java @@ -0,0 +1,30 @@ +package hufs.lion.team404.domain.dto.response; + +import hufs.lion.team404.domain.entity.Store; +import java.time.LocalDateTime; + +public record StoreDetailResponse( + Long id, + String storeName, + String category, + String address, + String contact, + String introduction, + String businessNumber, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static StoreDetailResponse from(Store s) { + return new StoreDetailResponse( + s.getId(), + s.getStoreName(), + s.getCategory(), + s.getAddress(), + s.getContact(), + s.getIntroduction(), + s.getBusinessNumber(), + s.getCreatedAt(), + s.getUpdatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/hufs/lion/team404/domain/dto/response/StoreSummaryResponse.java b/src/main/java/hufs/lion/team404/domain/dto/response/StoreSummaryResponse.java new file mode 100644 index 0000000..858d634 --- /dev/null +++ b/src/main/java/hufs/lion/team404/domain/dto/response/StoreSummaryResponse.java @@ -0,0 +1,25 @@ +package hufs.lion.team404.domain.dto.response; + +import hufs.lion.team404.domain.entity.Store; +import java.time.LocalDateTime; + +public record StoreSummaryResponse( + Long id, + String storeName, + String category, + String address, + String contact, + LocalDateTime createdAt +) { + public static StoreSummaryResponse from(Store s) { + return new StoreSummaryResponse( + s.getId(), + s.getStoreName(), + s.getCategory(), + s.getAddress(), + s.getContact(), + s.getCreatedAt() + ); + } +} + diff --git a/src/main/java/hufs/lion/team404/domain/entity/ChatRoom.java b/src/main/java/hufs/lion/team404/domain/entity/ChatRoom.java index b74aaae..decfb86 100644 --- a/src/main/java/hufs/lion/team404/domain/entity/ChatRoom.java +++ b/src/main/java/hufs/lion/team404/domain/entity/ChatRoom.java @@ -60,4 +60,5 @@ public ChatRoom(Student student, Store store, InitiatedBy initiatedBy) { this.student = student; this.initiatedBy = initiatedBy; } + } diff --git a/src/main/java/hufs/lion/team404/domain/entity/Store.java b/src/main/java/hufs/lion/team404/domain/entity/Store.java index cf7e4c6..07ebac9 100644 --- a/src/main/java/hufs/lion/team404/domain/entity/Store.java +++ b/src/main/java/hufs/lion/team404/domain/entity/Store.java @@ -46,7 +46,7 @@ public class Store { private LocalDateTime updatedAt; // 연관관계 - @OneToMany(mappedBy = "store", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "store") private List projectRequests; @Builder diff --git a/src/main/java/hufs/lion/team404/model/StoreModel.java b/src/main/java/hufs/lion/team404/model/StoreModel.java index 9531295..d6ed891 100644 --- a/src/main/java/hufs/lion/team404/model/StoreModel.java +++ b/src/main/java/hufs/lion/team404/model/StoreModel.java @@ -4,10 +4,17 @@ import org.springframework.stereotype.Service; import org.webjars.NotFoundException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.server.ResponseStatusException; +import static org.springframework.http.HttpStatus.CONFLICT; +import org.springframework.transaction.annotation.Transactional; import hufs.lion.team404.domain.dto.request.StoreUpdateRequestDto; import hufs.lion.team404.domain.dto.response.StoreReadResponseDto; import hufs.lion.team404.domain.dto.request.StoreCreateRequestDto; import hufs.lion.team404.domain.dto.response.StoreReadResponseDto; +import hufs.lion.team404.domain.dto.request.StoreFilterRequest; +import hufs.lion.team404.domain.dto.response.StoreSummaryResponse; import hufs.lion.team404.domain.entity.Store; import hufs.lion.team404.domain.entity.User; import hufs.lion.team404.domain.enums.UserRole; @@ -22,21 +29,26 @@ public class StoreModel { private final StoreService storeService; private final UserService userService; - public void createStore(StoreCreateRequestDto storeCreateRequestDto, Long user_id) { - User user = userService.findById(user_id).orElseThrow(() -> new NotFoundException("User not found")); - + @Transactional + public void createStore(StoreCreateRequestDto dto, Long userId) { + User user = userService.findById(userId) + .orElseThrow(() -> new NotFoundException("User not found")); + if (storeService.hasStoreForUser(userId)) { + throw new ResponseStatusException(CONFLICT, "이미 해당 사용자에 등록된 업체가 있습니다."); + } + if (storeService.existsByBusinessNumber(dto.getBusinessNumber())) { + throw new ResponseStatusException(CONFLICT, "이미 등록된 사업자번호입니다."); + } Store store = Store.builder() - .storeName(storeCreateRequestDto.getName()) - .address(storeCreateRequestDto.getAddress()) - .contact(storeCreateRequestDto.getPhone()) - .category(storeCreateRequestDto.getType()) - .introduction(storeCreateRequestDto.getIntroduction()) - .businessNumber(storeCreateRequestDto.getBusinessNumber()) + .storeName(dto.getName()) + .address(dto.getAddress()) + .contact(dto.getPhone()) + .category(dto.getType()) + .introduction(dto.getIntroduction()) + .businessNumber(dto.getBusinessNumber()) .user(user) .build(); - user.setUserRole(UserRole.STORE); - storeService.save(store); } public StoreReadResponseDto getStoreById(Long storeId) { @@ -57,4 +69,12 @@ public StoreReadResponseDto updateStore(Long storeId, StoreUpdateRequestDto dto, public void deleteStore(Long storeId, Long userId) { storeService.deleteStore(storeId, userId); } + public Page getStoresPage(Pageable pageable) { + return storeService.listAll(pageable); + } + public Page searchStores(StoreFilterRequest filter, Pageable pageable) { + return storeService.listWithFilter(filter, pageable); + } + + } \ No newline at end of file diff --git a/src/main/java/hufs/lion/team404/repository/StoreRepository.java b/src/main/java/hufs/lion/team404/repository/StoreRepository.java index 922bf24..74c539d 100644 --- a/src/main/java/hufs/lion/team404/repository/StoreRepository.java +++ b/src/main/java/hufs/lion/team404/repository/StoreRepository.java @@ -3,13 +3,15 @@ import hufs.lion.team404.domain.entity.Store; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; - +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @Repository public interface StoreRepository extends JpaRepository { - Optional findByUserId(Long userId); Optional findByBusinessNumber(String businessNumber); @@ -17,6 +19,19 @@ public interface StoreRepository extends JpaRepository { List findByCategory(String category); List findByStoreNameContaining(String storeName); - + boolean existsByUserId(Long userId); boolean existsByBusinessNumber(String businessNumber); + @Query(""" +select s from Store s + where (:keyword is null + or lower(s.storeName) like lower(concat('%', :keyword, '%')) + or lower(s.introduction) like lower(concat('%', :keyword, '%'))) + and (:category is null or s.category = :category) + and (:address is null or lower(s.address) like lower(concat('%', :address, '%'))) +""") + Page search(@Param("keyword") String keyword, + @Param("category") String category, + @Param("address") String address, + Pageable pageable); + } diff --git a/src/main/java/hufs/lion/team404/service/StoreService.java b/src/main/java/hufs/lion/team404/service/StoreService.java index b0c3a28..4c77f2c 100644 --- a/src/main/java/hufs/lion/team404/service/StoreService.java +++ b/src/main/java/hufs/lion/team404/service/StoreService.java @@ -5,10 +5,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.webjars.NotFoundException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.security.access.AccessDeniedException; import hufs.lion.team404.domain.entity.Store; import hufs.lion.team404.repository.StoreRepository; import hufs.lion.team404.domain.dto.request.StoreUpdateRequestDto; +import hufs.lion.team404.domain.dto.request.StoreFilterRequest; +import hufs.lion.team404.domain.dto.response.StoreSummaryResponse; +import hufs.lion.team404.domain.dto.response.StoreDetailResponse; import lombok.RequiredArgsConstructor; @Service @@ -87,7 +92,31 @@ public void deleteStore(Long storeId, Long userId) { if (!store.getUser().getId().equals(userId)) { throw new AccessDeniedException("해당 업체를 삭제할 권한이 없습니다."); } - storeRepository.delete(store); // 물리 삭제 + storeRepository.delete(store); } + public Page listAll(Pageable pageable) { + return storeRepository.findAll(pageable) + .map(StoreSummaryResponse::from); + } + + + public Page listWithFilter(StoreFilterRequest f, Pageable pageable) { + return storeRepository.search( + f.keyword(), f.category(), f.address(), pageable + ).map(StoreSummaryResponse::from); + } + + + public StoreDetailResponse getDetail(Long id) { + Store s = storeRepository.findById(id) + .orElseThrow(() -> new NotFoundException("해당 가게를 찾을 수 없습니다.: id=" + id)); + return StoreDetailResponse.from(s); + } + public boolean hasStoreForUser(Long userId) { + // 이미 findByUserId가 있으니 그대로 활용 + return storeRepository.findByUserId(userId).isPresent(); + // (원하면 레포지토리에 existsByUserId(Long) 추가 후 그걸 호출해도 됨) + } + }