Skip to content
Open
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
89 changes: 70 additions & 19 deletions src/main/java/hufs/lion/team404/controller/StoreController.java
Original file line number Diff line number Diff line change
@@ -1,40 +1,60 @@
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")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin(origins = "*")
@Tag(name = "업체", description = "업체 관련 API")

public class StoreController {

private final StoreModel storeModel;

private static final Set<String> 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 = "업체 정보 생성",
Expand All @@ -43,26 +63,33 @@ public class StoreController {
)
public ApiResponse<Void> 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<List<StoreReadResponseDto>> getAllStores() {
return ApiResponse.success(storeModel.getAllStores());
}

@GetMapping("/{storeId}")
@Operation(summary = "업체 단건 조회", description = "업체 ID로 업체 정보를 조회합니다.")
public ApiResponse<StoreReadResponseDto> 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<StoreReadResponseDto> updateStore(
@PathVariable Long storeId,
@AuthenticationPrincipal UserPrincipal authentication,
Expand All @@ -71,6 +98,7 @@ public ApiResponse<StoreReadResponseDto> updateStore(
Long userId = authentication.getId();
return ApiResponse.success(storeModel.updateStore(storeId, dto, userId));
}

@DeleteMapping("/{storeId}")
@Operation(
summary = "업체 삭제",
Expand All @@ -86,4 +114,27 @@ public ApiResponse<Void> deleteStore(
return ApiResponse.success("삭제되었습니다.");
}

}
@GetMapping("/page")
@Operation(summary = "업체 목록 조회(페이지)", description = "페이지네이션으로 업체 목록을 조회합니다.")
public ApiResponse<Page<StoreSummaryResponse>> 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<Page<StoreSummaryResponse>> 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));
}


}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ public ChatRoom(Student student, Store store, InitiatedBy initiatedBy) {
this.student = student;
this.initiatedBy = initiatedBy;
}

}
2 changes: 1 addition & 1 deletion src/main/java/hufs/lion/team404/domain/entity/Store.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class Store {
private LocalDateTime updatedAt;

// 연관관계
@OneToMany(mappedBy = "store", cascade = CascadeType.ALL)
@OneToMany(mappedBy = "store")
private List<ProjectRequest> projectRequests;

@Builder
Expand Down
42 changes: 31 additions & 11 deletions src/main/java/hufs/lion/team404/model/StoreModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -57,4 +69,12 @@ public StoreReadResponseDto updateStore(Long storeId, StoreUpdateRequestDto dto,
public void deleteStore(Long storeId, Long userId) {
storeService.deleteStore(storeId, userId);
}
public Page<StoreSummaryResponse> getStoresPage(Pageable pageable) {
return storeService.listAll(pageable);
}
public Page<StoreSummaryResponse> searchStores(StoreFilterRequest filter, Pageable pageable) {
return storeService.listWithFilter(filter, pageable);
}


}
21 changes: 18 additions & 3 deletions src/main/java/hufs/lion/team404/repository/StoreRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,35 @@
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<Store, Long> {

Optional<Store> findByUserId(Long userId);

Optional<Store> findByBusinessNumber(String businessNumber);

List<Store> findByCategory(String category);

List<Store> 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<Store> search(@Param("keyword") String keyword,
@Param("category") String category,
@Param("address") String address,
Pageable pageable);

}
Loading