스파르타 Java 단기 심화 과정


코드카타


프로그래머스 172928 공원 산책

https://school.programmers.co.kr/learn/courses/30/lessons/172928

— 문제 설명

지나다니는 길을 'O', 장애물을 'X'로 나타낸 직사각형 격자 모양의 공원에서 로봇 강아지가 산책을 하려합니다. 산책은 로봇 강아지에 미리 입력된 명령에 따라 진행하며, 명령은 다음과 같은 형식으로 주어집니다.

  • ["방향 거리", "방향 거리" … ]

예를 들어 "E 5"는 로봇 강아지가 현재 위치에서 동쪽으로 5칸 이동했다는 의미입니다. 로봇 강아지는 명령을 수행하기 전에 다음 두 가지를 먼저 확인합니다.

  • 주어진 방향으로 이동할 때 공원을 벗어나는지 확인합니다.
  • 주어진 방향으로 이동 중 장애물을 만나는지 확인합니다.

위 두 가지중 어느 하나라도 해당된다면, 로봇 강아지는 해당 명령을 무시하고 다음 명령을 수행합니다.

공원의 가로 길이가 W, 세로 길이가 H라고 할 때, 공원의 좌측 상단의 좌표는 (0, 0), 우측 하단의 좌표는 (H - 1, W - 1) 입니다.

!https://user-images.githubusercontent.com/62426665/217702316-1bd5d3ba-c1d7-4133-bfb5-36bdc85a08ba.png

공원을 나타내는 문자열 배열 park, 로봇 강아지가 수행할 명령이 담긴 문자열 배열 routes가 매개변수로 주어질 때, 로봇 강아지가 모든 명령을 수행 후 놓인 위치를 [세로 방향 좌표, 가로 방향 좌표] 순으로 배열에 담아 return 하도록 solution 함수를 완성해주세요.

— 제한 조건

  • 3 ≤ park의 길이 ≤ 50
    • 3 ≤ park[i]의 길이 ≤ 50
      • park[i]는 다음 문자들로 이루어져 있으며 시작지점은 하나만 주어집니다.
        • S : 시작 지점
        • O : 이동 가능한 통로
        • X : 장애물
    • park는 직사각형 모양입니다.
  • 1 ≤ routes의 길이 ≤ 50
    • routes의 각 원소는 로봇 강아지가 수행할 명령어를 나타냅니다.
    • 로봇 강아지는 routes의 첫 번째 원소부터 순서대로 명령을 수행합니다.
    • routes의 원소는 "op n"과 같은 구조로 이루어져 있으며, op는 이동할 방향, n은 이동할 칸의 수를 의미합니다.
      • op는 다음 네 가지중 하나로 이루어져 있습니다.
        • N : 북쪽으로 주어진 칸만큼 이동합니다.
        • S : 남쪽으로 주어진 칸만큼 이동합니다.
        • W : 서쪽으로 주어진 칸만큼 이동합니다.
        • E : 동쪽으로 주어진 칸만큼 이동합니다.
      • 1 ≤ n ≤ 9

— 입출력 예

parkroutesresult
["SOO","OOO","OOO"]["E 2","S 2","W 1"][2,1]
["SOO","OXX","OOO"]["E 2","S 2","W 1"][0,1]
["OSO","OOO","OXO","OOO"]["E 2","S 3","W 1"][0,0]

입출력 예 #1

입력된 명령대로 동쪽으로 2칸, 남쪽으로 2칸, 서쪽으로 1칸 이동하면 [0,0] -> [0,2] -> [2,2] -> [2,1]이 됩니다.

입출력 예 #2

입력된 명령대로라면 동쪽으로 2칸, 남쪽으로 2칸, 서쪽으로 1칸 이동해야하지만 남쪽으로 2칸 이동할 때 장애물이 있는 칸을 지나기 때문에 해당 명령을 제외한 명령들만 따릅니다. 결과적으로는 [0,0] -> [0,2] -> [0,1]이 됩니다.

입출력 예 #3

처음 입력된 명령은 공원을 나가게 되고 두 번째로 입력된 명령 또한 장애물을 지나가게 되므로 두 입력은 제외한 세 번째 명령만 따르므로 결과는 다음과 같습니다. [0,1] -> [0,0]

— 문제 풀이

class Solution {
    public int[][] del = {
        {0, 1},
        {0, -1},
        {1, 0},
        {-1, 0}
    };
    public int[] solution(String[] park, String[] routes) {
        int[] answer = new int[2];
        for(int i=0;i<park.length;i++){
            for(int j=0;j<park[0].length();j++){
                if(park[i].charAt(j)=='S'){
                    answer[0] = i;
                    answer[1] = j;
                }
            }
        }
        
        for(int i=0;i<routes.length;i++){
            String[] tmp = routes[i].split(" ");
            int n = Integer.parseInt(tmp[1]);
            int[] tmpDog = answer.clone();
            boolean flag = false; // 장애물 만나는지 체크하는 flag
            for(int j=0;j<n;j++){
                switch(tmp[0]){
                    case "E":
                        tmpDog[0] += del[0][0];
                        tmpDog[1] += del[0][1];
                        break;
                    case "W":
                        tmpDog[0] += del[1][0];
                        tmpDog[1] += del[1][1];
                        break;
                    case "S":
                        tmpDog[0] += del[2][0];
                        tmpDog[1] += del[2][1];
                        break;
                    case "N":
                        tmpDog[0] += del[3][0];
                        tmpDog[1] += del[3][1];
                        break;
                }
                if(
                    tmpDog[0]<0||tmpDog[0]>=park.length||
                    tmpDog[1]<0||tmpDog[1]>=park[0].length()||
                    park[tmpDog[0]].charAt(tmpDog[1])=='X'
                  ) {
                    flag = true;
                    break;
                }
            }
            if(!flag){
                answer[0] = tmpDog[0];
                answer[1] = tmpDog[1];
            }
        }
        
        return answer;
    }
}

Order 서비스 기본 CRUD

기본적인 CRUD를 우선 작성하기로 했다.
Create
GetOne(단건 조회)
Update
Delete
리스트 조회는 QueryDSL을 사용할 예정이다.

CreateOrder

  • 주문시 필요한 정보는
  • 가게의 ID, 목적지 ID, 주문 타입(온라인/오프라인)
  • 현재는 Store와 Destination이 개발완료되지 않은 상태라 임시로 UUID로만 ID를 사용
  • OrderCreateReqDto
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class OrderCreateReqDto {
        private UUID storeId;
        private UUID destId;
        private OrderType orderType;
    
        public Order toOrder(String userName) {
            return Order.builder()
                    .orderId(UUID.randomUUID())
                    .storeId(this.storeId)
                    .userName(userName)
                    .destId(this.destId)
                    .orderType(this.orderType)
                    .isReviewed(false)
                    .build();
        }
    }
    
  • OrderResDto
    @Getter
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class OrderResDto {
        private UUID orderId;
        private String userName;
        private UUID storeId;
        private UUID destId;
        private OrderType orderType;
        private boolean isReviewed;
    
        public static OrderResDto from(Order order) {
            return OrderResDto.builder()
                    .orderId(order.getOrderId())
                    .userName(order.getUserName())
    //                .storeId(order.getStore().getStoreId())
                    .storeId(order.getStoreId())
                    .destId(order.getDestId())
                    .orderType(order.getOrderType())
                    .isReviewed(order.isReviewed())
                    .build();
        }
    }
    
  • OrderService - createOrder
    @Transactional
        public OrderResDto createOrder(OrderCreateReqDto orderCreateReqDto, String userName) {
            Order order = orderCreateReqDto.toOrder(userName);
    
            return OrderResDto.from(orderRepository.save(order));
        }

GetOrder ( 단건 조회 )

  • orderId를 PathVariable로 받아와서 단건 조회를 실시함 ( 삭제되지 않아야 함 )
  • OrderService - getOrder
    public OrderResDto getOrder(UUID orderId, String userName) {
            Order order = orderRepository.findOneByOrderIdAndDeletedAtIsNull(orderId).orElseThrow(()->{
                log.error("주문 정보를 찾을 수 없음");
                return new ResponseStatusException(HttpStatus.NOT_FOUND, "주문 정보를 찾을 수 없음");
            }
            );
            if(!order.getUserName().equals(userName)){
                log.error("접근할 수 없음");
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "접근할 수 없음");
            }
            return OrderResDto.from(order);
        }

UpdateOrder

  • orderId와 Review 작성 여부를 받아와 Order정보를 수정 함
  • 수정할 정보가 더 늘어날 수 있어서 OrderUpdateReqDto로 따로 Class 생성해둔 상태
  • Review상태가 바뀌지 않아 확인한 결과 boolean 타입+ isXXX 네이밍 + lombok을 사용하면 값이 정상적으로 매핑되지 않는 이슈가 발생해 Boolean 타입으로 ReqDto를 변경
  • OrderService - updateOrder
    @Transactional
        public OrderResDto updateOrder(OrderUpdateReqDto orderUpdateReqDto, String userName){
            // 본인의 주문인지 확인
            Order order = orderRepository.findOneByOrderIdAndDeletedAtIsNull(orderUpdateReqDto.getOrderId()).orElseThrow(()->{
                        log.error("주문 정보를 찾을 수 없음");
                        return new ResponseStatusException(HttpStatus.NOT_FOUND, "주문 정보를 찾을 수 없음");
                    }
            );
            if(!order.getUserName().equals(userName)){
                log.error("접근할 수 없음");
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "접근할 수 없음");
            }
            // 주문 정보 수정 ( 현재는 review 상태 변경 )
            order.updateOrder(orderUpdateReqDto.getIsReviewed());
            return OrderResDto.from(order);
        }

DeleteOrder

  • 삭제는 SoftDelete로 deletedAt, deletedBy 부분에 시간과 삭제한 사용자명을 입력함으로 처리
  • OrderService - deleteOrder
    @Transactional
        public void deleteOrder(UUID orderId, String userName){
            // 본인의 주문인지 확인
            Order order = orderRepository.findOneByOrderIdAndDeletedAtIsNull(orderId).orElseThrow(()->{
                        log.error("주문 정보를 찾을 수 없음");
                        return new ResponseStatusException(HttpStatus.NOT_FOUND, "주문 정보를 찾을 수 없음");
                    }
            );
            if(!order.getUserName().equals(userName)){
                log.error("접근할 수 없음");
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "접근할 수 없음");
            }
            // 주문 삭제
            order.setDeleted(LocalDateTime.now(), userName);
        }

QueryDSL 설정 ( Spring Boot 3.X )

  • Spring Boot 3.3.3 에서 QueryDSL 설정을 추가

Build.gradle

  • dependencies에 아래 내용 추가
    // queryDSL - Spring Boot 3.X 설정
    	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    	annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
  • clean 추가
    clean {
    	delete file('src/main/generated')
    }

QueryDslConfig

@Configuration
@RequiredArgsConstructor
public class QueryDslConfig {
    private final EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}

Order Search ( QueryDsl 활용한 검색 )

  • Parameter로 page, limit(페이지 사이즈), isAsc(정렬순서), orderBy(createdAt or modifiedAt), keyword를 받아서 검색

OrderCustomRepository

  • OrderCustomRepository
    public interface OrderCustomRepository {
        Page<Order> searchByUserName(String userName, String keyword, Pageable pageable);
    }
  • OrderCustomRepositoryImpl
    import static com.blue.service.config.QueryDslUtil.getAllOrderSpecifiers;
    import static com.blue.service.domain.order.QOrder.order;
    
    @RequiredArgsConstructor
    public class OrderCustomRepositoryImpl implements OrderCustomRepository{
    
        private final JPAQueryFactory jpaQueryFactory;
    
        @Override
        public Page<Order> searchByUserName(String userName,
                                            String keyword,
                                            Pageable pageable) {
            List<OrderSpecifier> orders = getAllOrderSpecifiers(pageable, "order");
    
            List<Order> query = jpaQueryFactory
                    .selectFrom(order)
                    .distinct()
                    .where(
                            order.userName.eq(userName)
                            //가게명 조건 추가 필요 가게명 contains keyword
                    )
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .orderBy(orders.stream().toArray(OrderSpecifier[]::new))
                    .fetch();
    
            JPAQuery<Long> countQuery = jpaQueryFactory
                    .select(order.count())
                    .from(order)
                    .where(order.userName.eq(userName));
    
            return PageableExecutionUtils.getPage(query, pageable, () -> countQuery.fetchOne());
        }
    }
  • 동적으로 createdAt 혹은 modifiedAt을 통해 정렬을 하기 위해 OrderSpecifier를 따로 만들어줘서 orderBy에 적용함
  • PageImpl보다 두가지 조건()에서 count 쿼리 실행을 제외하여 성능을 개선한 PageableExecutionUtils.getPage를 사용
  • OrderRepository에 OrderCustomRepository 상속 추가
    public interface OrderRepository extends JpaRepository<Order, UUID>, OrderCustomRepository

QueryDslUtil

  • 각각의 커스텀 Repo마다 OrderSpecifier를 만들면 중복이 많아지므로 QueryDslUtil에서 한번에 관리
    public class QueryDslUtil {
    
        public static OrderSpecifier<?> getSortedColumn(Order order, Path<?> parent, String fieldName){
            Path<Object> fieldPath = Expressions.path(Object.class, parent, fieldName);
    
            return new OrderSpecifier(order, fieldPath);
        }
    
        public static List<OrderSpecifier> getAllOrderSpecifiers(Pageable pageable, String entityType){
            List<OrderSpecifier> orders = new ArrayList<>();
    
            if (!pageable.getSort().isEmpty()){
                for(Sort.Order order : pageable.getSort()){
                    if("createdAt".equals(order.getProperty())){
                        Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
                        Path path;
                        switch (entityType){
                            case "order":
                                path = QOrder.order;
                                break;
                            default:
                                throw new IllegalArgumentException("Entity의 타입이 잘못되었음");
                        }
                        OrderSpecifier<?> orderSpecifier = QueryDslUtil.getSortedColumn(direction, path, "createdAt");
                        orders.add(orderSpecifier);
                    } else if ("modifiedAt".equals(order.getProperty())) {
                        Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
                        Path path;
                        switch (entityType){
                            case "order":
                                path = QOrder.order;
                                break;
                            default:
                                throw new IllegalArgumentException("Entity의 타입이 잘못되었음");
                        }
                        OrderSpecifier<?> orderSpecifier = QueryDslUtil.getSortedColumn(direction, path, "modifiedAt");
                        orders.add(orderSpecifier);
                    }
    
                }
            }
    
            return orders;
        }
    }

OrderService

public List<OrderResDto> searchOrder(String userName, int page, int limit,
                                         Boolean isAsc, String orderBy, String keyword) {
        Sort.Direction direction;
        if(isAsc){
            direction = Sort.Direction.ASC;
        }else {
            direction = Sort.Direction.DESC;
        }
        Pageable pageable = PageRequest.of(page-1, limit, Sort.by(direction, orderBy));

        Page<OrderResDto> orderPage = orderRepository.searchByUserName(userName, keyword, pageable).map(OrderResDto::from);

        return orderPage.toList();
    }

OrderController

@GetMapping
    @Operation(summary = "주문 검색")
    public ResponseEntity<List<OrderResDto>> searchOrder(
            @RequestHeader(name = "X-User-Name") String userName,
            @RequestParam(name = "page") int page,
            @RequestParam(name = "limit") int limit,
            @RequestParam(name = "isAsc") Boolean isAsc,
            @RequestParam(name = "orderBy") String orderBy,
            @RequestParam(name = "keyword", required = false) String keyword
    ){
        return ResponseEntity.ok(orderService.searchOrder(userName, page, limit, isAsc, orderBy, keyword));
    }
profile
기록을 남겨보자

0개의 댓글