API/실습

46. [ JAVA ] API 문서 문제 : JWT(Json Web Token)(보안) / User (1)

천재단미 2025. 1. 4. 14:27
728x90
반응형

 

 

 

 

 

 

 

 

문제

 

 

지난 시간 알려드린 JWT(Json Web Token)(보안) 추가 하여 문제 풀이를 진행하겠습니다.

 

USER, OWNER, ADMIN으로 나누어 3가지로 진행하겠습니다. 

 

 

테이블명세서 

 

음식점 리뷰 시스템 테이블 명세서 

 

1. user

 
 
사용자 정보를 저장하는 테이블
칼럼명
데이터 타입
NULL
기본값
설명
id
BIGINT
NO
AUTO_INCREMENT
기본키
email
VARCHAR(100)
NO
-
사용자 이메일, UNIQUE
password
VARCHAR(200)
NO
-
암호화된 비밀번호
nickname
VARCHAR(50)
NO
-
사용자 닉네임
role
VARCHAR(10)
NO
'USER'
권한(USER, OWNER, ADMIN)
created_at
TIMESTAMP
NO
CURRENT_TIMESTAMP
생성일시
 

2. restaurant 

 

음식점 정보를 저장하는 테이블
칼럼명
데이터 타입
NULL
기본값
설명
id
BIGINT
NO
AUTO_INCREMENT
기본키
name
VARCHAR(100)
NO
-
음식점 이름
address
VARCHAR(200)
NO
-
음식점 주소
phone
VARCHAR(20)
NO
-
음식점 전화번호
category
VARCHAR(50)
NO
-
음식점 카테고리
description
TEXT
YES
NULL
음식점 설명
created_at
TIMESTAMP
NO
CURRENT_TIMESTAMP
생성일시
 
3. menu

 

메뉴 정보를 저장하는 테이블 
 
 
 
 
 
 
칼럼명
데이터 타입
NULL
기본값
설명
id
BIGINT
NO
AUTO_INCREMENT
기본키
restaurant_id
BIGINT
NO
-
RESTAURANT.id (FK)
name
VARCHAR(100)
NO
-
메뉴 이름
price
INT
NO
-
메뉴 가격
description
TEXT
YES
NULL
메뉴 설명
category
VARCHAR(50)
NO
-
메뉴 카테고리
created_at
TIMESTAMP
NO
CURRENT_TIMESTAMP
생성일시
 
 

4. review

 

리뷰 정보를 저장하는 테이블 

 

API 명세서

1. 음식점 API

1.1. 음식점 목록 조회

다양한 조건으로 음식점 목록을 검색하고 조회할 수 있는 API입니다.

  • URL: /api/v1/restaurants
  • Method: GET
  • 설명:
    • 페이징 처리된 음식점 목록을 반환합니다.
    • 카테고리 필터링과 키워드 검색을 지원합니다.
    • 음식점의 평균 평점과 리뷰 수를 포함합니다.
    • 최신 등록순으로 정렬됩니다.
  • Query Parameters:
page: 페이지 번호 (기본값: 1)
size: 페이지 크기 (기본값: 10)
category: 카테고리 필터 (선택) - 한식, 중식, 일식, 양식 등
keyword: 검색어 - 이름, 주소 검색 (선택, 최소 2글자 이상)

  • Response:
{
    "content": [
        {
            "id": 1,
            "name": "맛있는 식당",
            "category": "한식",
            "address": "서울시 강남구",
            "phone": "02-1234-5678",
            "description": "전통 한식집",
            "avgRating": 4.5,
            "reviewCount": 100,
            "createdAt": "2024-12-27T10:00:00"
        }
    ],
    "pageable": {
        "page": 1,
        "size": 10,
        "totalElements": 100,
        "totalPages": 10
    }
}

1.2. 음식점 상세 조회

특정 음식점의 상세 정보와 메뉴 목록을 조회하는 API입니다.

  • URL: /api/v1/restaurants/{id}
  • Method: GET
  • 설명:
    • 음식점의 기본 정보를 제공합니다.
    • 해당 음식점의 전체 메뉴 목록을 함께 반환합니다.
    • 평균 평점과 총 리뷰 수를 포함합니다.
    • 각 메뉴별 리뷰 수도 함께 제공됩니다.
    • 존재하지 않는 음식점 ID인 경우 404 에러를 반환합니다.
  • Response:
{
		"restaurant": {
		    "id": 1,
		    "name": "맛있는 식당",
		    "category": "한식",
		    "address": "서울시 강남구",
		    "phone": "02-1234-5678",
		    "description": "전통 한식집",
		    "avgRating": 4.5,
		    "reviewCount": 100,
		    "createdAt": "2024-12-27T10:00:00",
	  },
    "menus": [
        {
            "id": 1,
            "name": "김치찌개",
            "price": 8000,
            "description": "돼지고기 김치찌개",
            "category": "찌개",
            "reviewCount": 50
        }
    ]
}

2. 메뉴 API

2.1. 음식점별 메뉴 목록 조회

특정 음식점의 메뉴 목록을 조회하는 API입니다.

  • URL: /api/v1/restaurants/{restaurantId}/menus
  • Method: GET
  • 설명:
    • 특정 음식점의 전체 메뉴를 페이징 처리하여 제공합니다.
    • 카테고리별 필터링을 지원합니다.
    • 각 메뉴의 리뷰 수를 포함합니다.
    • 메뉴는 카테고리별, 가격순으로 정렬 가능합니다.
    • 존재하지 않는 음식점 ID인 경우 404 에러를 반환합니다.
  • Query Parameters:
page: 페이지 번호 (기본값: 1)
size: 페이지 크기 (기본값: 10)
category: 메뉴 카테고리 필터 (선택)
sort: 정렬 기준 (price_asc, price_desc, name_asc)

  • Response:
{
    "content": [
        {
            "id": 1,
            "name": "김치찌개",
            "price": 8000,
            "description": "돼지고기 김치찌개",
            "category": "찌개",
            "reviewCount": 50,
            "createdAt": "2024-12-27T10:00:00"
        }
    ],
    "pageable": {
        "page": 1,
        "size": 10,
        "totalElements": 50,
        "totalPages": 5
    }
}

3. 리뷰 API

3.1. 리뷰 작성

새로운 리뷰를 작성하는 API입니다.

  • URL: /api/v1/reviews
  • Method: POST
  • 설명:
    • 인증된 사용자만 리뷰를 작성할 수 있습니다.
    • 동일한 메뉴에 대해 한 사용자는 하나의 리뷰만 작성 가능합니다.
    • 리뷰 작성 시 해당 음식점의 평균 평점이 자동으로 갱신됩니다.
    • 음식점과 메뉴의 리뷰 카운트가 자동으로 증가합니다.
  • Request Header:
Authorization: Bearer {accessToken}

  • Request Body:
{
    "restaurantId": 1,
    "menuId": 1,
    "rating": 5,
    "content": "정말 맛있었습니다!"
}

  • Validation:
    • rating: 1-5 사이의 정수만 가능
    • content: 최소 10자 이상 작성
    • restaurantId: 유효한 음식점 ID
    • menuId: 해당 음식점의 유효한 메뉴 ID
  • Response:
  • 성공 201 문제있을경우 400

3.2. 리뷰 수정

기존 리뷰를 수정하는 API입니다.

  • URL: /api/v1/reviews/{id}
  • Method: PUT
  • 설명:
    • 리뷰 작성자만 수정이 가능합니다.
    • 리뷰 수정 시 음식점의 평균 평점이 자동으로 갱신됩니다.
    • 리뷰 내용과 평점만 수정 가능합니다.
    • 존재하지 않는 리뷰 ID인 경우 404 에러를 반환합니다.
  • Request Header:
Authorization: Bearer {accessToken}

  • Request Body:
{
    "rating": 4,
    "content": "맛있었지만 조금 짰어요"
}

  • Response:
  • 성공 200 문제있을경우 400

4. 사용자 API

4.1. 회원가입

새로운 사용자를 등록하는 API입니다.

  • URL: /api/v1/users/signup
  • Method: POST
  • 설명:
    • 새로운 사용자를 시스템에 등록합니다.
    • 이메일 중복 확인을 수행합니다.
    • 비밀번호는 암호화하여 저장됩니다.
    • 기본 사용자 권한(USER)으로 생성됩니다.
  • Request Body:
{
    "email": "user@example.com",
    "password": "password123",
    "nickname": "사용자닉네임"
}

  • Validation:
    • email: 유효한 이메일 형식 (@포함)
    • password: 최소 8자 이상, 영문/숫자/특수문자 조합
    • nickname: 2-20자 이내, 한글/영문/숫자 허용
  • Response:
    • 201

4.2. 로그인

사용자 인증을 위한 로그인 API입니다.

  • URL: /api/v1/users/login
  • Method: POST
  • 설명:
    • 이메일과 비밀번호로 사용자를 인증합니다.
    • 인증 성공 시 JWT 토큰을 발급합니다.
    • Access Token의 유효기간은 1시간입니다.
  • Request Body:
{
    "email": "user@example.com",
    "password": "password123"
}

  • Response:
{
    "token": "eyJhbGciOiJIUzI1NiJ9..."
}

공통 사항

1. 에러 처리

주요 에러 상황:

  • 400 Bad Request: 잘못된 요청 파라미터
  • 401 Unauthorized: 인증 실패 또는 토큰 만료
  • 403 Forbidden: 권한 없음
  • 404 Not Found: 리소스를 찾을 수 없음
  • 409 Conflict: 데이터 충돌 (예: 이메일 중복)
  • 500 Internal Server Error: 서버 오류

2. 페이징 처리

페이징이 적용된 모든 API는 다음 정보를 포함합니다:

  • page: 현재 페이지 번호 (1부터 시작)
  • size: 페이지당 항목 수
  • totalElements: 전체 데이터 수
  • totalPages: 전체 페이지 수

3. 인증

  • Bearer 토큰 방식의 JWT 인증을 사용합니다.
  • 인증이 필요한 API의 경우 반드시 Authorization 헤더에 토큰을 포함해야 합니다.

4. API 버전 관리

  • URI의 /api/v1 prefix로 API 버전을 관리합니다.
  • 향후 하위 호환성이 깨지는 변경사항이 있을 경우 버전이 변경될 수 있습니다.

파일 처리하기 (나중에 함)

리뷰 작성 시, 사진과 별점과 내용을 보내는 방법

3.1. 리뷰 작성

새로운 리뷰를 작성하는 API입니다.

  • URL: /api/v1/reviews/restaurant/{restaurant_id}/menu/{menu_id}
  • Method: POST
  • 설명:
    • 인증된 사용자만 리뷰를 작성할 수 있습니다.
    • 동일한 메뉴에 대해 한 사용자는 하나의 리뷰만 작성 가능합니다.
    • 리뷰 작성 시 해당 음식점의 평균 평점이 자동으로 갱신됩니다.
    • 음식점과 메뉴의 리뷰 카운트가 자동으로 증가합니다.
  • Request Header:
Authorization: Bearer {accessToken}

  • Request Body: Form-Data
- rating  (text) : 5
- content (text) : 이 음식 정말 맛있게 잘 먹었습니다~ 굿!
- image   (file) : 사진파일
  • Validation:
    • rating: 1-5 사이의 정수만 가능
    • content: 최소 10자 이상 작성
    • restaurantId: 유효한 음식점 ID
    • menuId: 해당 음식점의 유효한 메뉴 ID
  • Response:
  • 성공 201 문제있을경우 400

 

 

풀이

 

 

 
DBeaver와 intelliJ 를 데이터 베이스의 내용에 맞게 설계 해줍니다. 
 
 

 

DBeaver

 

 

- 각 서버는 아래의 초기 세팅 참조로 하여 진행 

- DB 테이블 명세서를 기초로 DB를 세팅

 

40. [ JAVA ] API 문제 풀이 : 초기 세팅

문제 풀이를 하기전에 초기 세팅 방법에 대하여 알아보겠습니다.문제풀이에 필요한 프로그램 및 사이트는 DBeaver,Spring Boot,Gibhubdp, intelliJ ,putty입니다. DBeaver DBeaver 실행후 새로운 Databases생성 -> Tab

danmi1109.tistory.com

 

- 토큰 관련 초기 세팅 

 

45. [ JAVA ] JWT(Json Web Token)(보안) 기초 세팅

JWT(Json Web Token)에 대하여 알아보았았습니다.   intelliJ 위와 같이 Postman 에서 로그인을 진행하면  token이 발행될 수 있도록  intelliJ에서 초기 작업을 진행하도록 하겠습니다.  초기 세팅을

danmi1109.tistory.com

 

  • 순서 설명

 

- 회원가입을 제일 먼저 진행하여 줍니다.

- 회원가입으로 인하여 정보 입력및 로그인후 토큰을 전달하여 userId를 확인 할수 있게 됩니다. 

 

4.1. 회원가입

새로운 사용자를 등록하는 API입니다.

  • URI의 /api/v1 prefix로 API 버전을 관리합니다.

 - http://localhost:8080/api/v1 으로 진행 

 

Postman

 

 

intelliJ

 

- Controller 패키지 생성- UserController 클래스 생성

 

 

  • URL: /api/v1/users/signup
  • Method: POST
  • 설명:
    • 새로운 사용자를 시스템에 등록합니다.
    • 이메일 중복 확인을 수행합니다.
    • 비밀번호는 암호화하여 저장됩니다.
    • 기본 사용자 권한(USER)으로 생성됩니다.
  • Response:
    • 201

 

 

 

새로운 사용자를 등록하는 API입니다.

  • URL: /api/v1/users/signup
  • Method: POST
    @PostMapping("/api/v1/users/signup")
    signUp(@RequestBody UserRequest userRequest) {
    
    
    
    userService.signUp(userRequest);

 

- UserRequest/ userSevice 를 요하기때문에

- dto 패키지 생성

- UserRequest 클래스 생성 

 

  • Validation:
    • email: 유효한 이메일 형식 (@포함)
    • password: 최소 8자 이상, 영문/숫자/특수문자 조합
    • nickname: 2-20자 이내, 한글/영문/숫자 허용
  • Request Body:
{
    "email": "user@example.com",
    "password": "password123",
    "nickname": "사용자닉네임"
}

 

package com.example.jwt3.dto;

public class UserRequest {
  public String email;
  public String password;
  public String nickname;

  public UserRequest() {
  }


  public UserRequest(String email, String password, String nickname) {
    this.email = email;
    this.password = password;
    this.nickname = nickname;
  }
}

 

- sevice 패키지 생성

- UserSevice 클래스 생성 

 

  • Validation:
    • email: 유효한 이메일 형식 (@포함)
    • password: 최소 8자 이상, 영문/숫자/특수문자 조합
    • nickname: 2-20자 이내, 한글/영문/숫자 허용

 @Service
public class UserService {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    UserDAO userDAO;

    @Autowired
    JwtConfig jwtConfig;

    public int signUp(UserRequest userRequest){

        if( EmailValidator.isValidEmail( userRequest.email ) == false ){
            return 1;
        }
        if( PasswordValidator.isValidPassword( userRequest.password ) == false ){
            return 2;
        }
        if(NicknameValidator.isValidInput(userRequest.nickname) == false){
            return 3;
        }
        

        // 비밀번호 암호화
        userRequest.password = passwordEncoder.encode(userRequest.password);

        // DB에 저장한다.
        try {
            userDAO.signUp(userRequest);
            System.out.println("UserService  2");
        } catch (Exception e) {
            // 이메일이 중복된경우.
            System.out.println("UserService  3");
            return 4;
        }
        return 0;
    }

 

userDAO.signUp(userRequest);

- dao 패키지 생성

- UserDAO 클래스 생성 

 

DBeaver

 

 

- 확인후

sql 문  확인 

- sql 문 작성하여 실행 유무 확인 

- 실행 확인후 intelliJ 에 복사하여 진행합니다. ( 문자열의 경우 '? ' 로 대체 하여 진행합니다. )

 

예) DBeaver sql 문 

INSERT INTO user(email,password,nickname,role)

values("admin4@gmail.com","pass1234567","맛있는집헌터","USER");

 

String sql = "insert INTO user \n" +
        "(email, password, nickname, role)\n" +
        "  values (? , ? , ? , 'USER');";

 

위와 같이 변환 하여 진행하여 줍니다. 

 

 

@Repository
public class UserDAO {

    @Autowired
    JdbcTemplate jdbcTemplate;

    public int signUp(UserRequest userRequest){
        // DB에 유저 정보를 저장하는 메소드
        String sql = "insert INTO user \n" +
                "(email, password, nickname, role)\n" +
                "  values (? , ? , ? , 'USER');";
        return jdbcTemplate.update(sql, userRequest.email, 
                userRequest.password, userRequest.nickname);
    }

 

 

  • 설명:
    • 새로운 사용자를 시스템에 등록합니다.
    • 이메일 중복 확인을 수행합니다.
    • 비밀번호는 암호화하여 저장됩니다.
    • 기본 사용자 권한(USER)으로 생성됩니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;

@Repository
public class UserDAO {

    @Autowired
    JdbcTemplate jdbcTemplate;

    public int signUp(UserRequest userRequest){
        // DB에 유저 정보를 저장하는 메소드
        String sql = "insert INTO user \n" +
                "(email, password, nickname, role)\n" +
                "  values (? , ? , ? , 'USER');";
        return jdbcTemplate.update(sql, userRequest.email, 
                userRequest.password, userRequest.nickname);
    }

@RestController
public class UserController {

    @Autowired
    UserService userService;

    //- 새로운 사용자를 시스템에 등록합니다.
    //- 이메일 중복 확인을 수행합니다.
    //- 비밀번호는 암호화하여 저장됩니다.
    //- 기본 사용자 권한(USER)으로 생성됩니다.

    @PostMapping("/api/v1/users/signup")
    public ResponseEntity<Object> signUp(@RequestBody UserRequest userRequest) {

        System.out.println(userRequest.email);

        int result = userService.signUp(userRequest);

        if(result != 0){
            return ResponseEntity.status(400).build();
        }
        return ResponseEntity.status(201).build();
    }

 

 

 - 회원가입 진행이 잘되었는지 확인 진행

    • Response:
      • 201

 

Postman

 

 

- 회원가입 완료 진행 (201) 

 

4.2. 로그인

사용자 인증을 위한 로그인 API입니다.

      • URL: /api/v1/users/login
      • Method: POST
      • 설명:
        • 이메일과 비밀번호로 사용자를 인증합니다.
        • 인증 성공 시 JWT 토큰을 발급합니다.
        • Access Token의 유효기간은 1시간입니다.
      • Request Body:
{
    "email": "user@example.com",
    "password": "password123"
}

      • Response:
{
    "token": "eyJhbGciOiJIUzI1NiJ9..."
}

 

Postman

 

 

 

intelliJ

 

      • URL: /api/v1/users/login
      • Method: POST
      • 설명:
        • 이메일과 비밀번호로 사용자를 인증합니다.
        • 인증 성공 시 JWT 토큰을 발급합니다.
        • Access Token의 유효기간은 1시간입니다.

공통사항- 인증 

      • Bearer 토큰 방식의 JWT 인증을 사용합니다.
      • 인증이 필요한 API의 경우 반드시 Authorization 헤더에 토큰을 포함해야 합니다.

 

JwtConfig

 

@Configuration
public class JwtConfig {

    Key key;
 
    long tokenValidMillisecond = 1*60*60*1000;

    public JwtConfig(@Value("${jwt.secret}") String secretKey) {

        this.key = Keys.hmacShaKeyFor(secretKey.getBytes());
    }

 

 

UserController

 

 

    @PostMapping("/api/v1/users/login")
    public ResponseEntity<UserLoginResponse> userLogin(@RequestBody UserRequest userRequest) {
        // 로그인 로직
        Object result = userService.userLogin(userRequest);

        if(result instanceof Integer){
            return ResponseEntity.status(400).build();
        }

        return ResponseEntity.status(200).body(new UserLoginResponse((String)result));
    }
}

 

 

UserService

 

 

    public Object userLogin(UserRequest userRequest) {

        // 이메일 형식이 올바른지 체크
        if( EmailValidator.isValidEmail( userRequest.email ) == false ){
            return 1;
        }

        try {
            User user = userDAO.userLogin(userRequest);

            // 비밀번호가 일치하는지 체크
            if( passwordEncoder.matches(userRequest.password, user.password) == false ){
                return 3;
            }

            // jwt 토큰 생성
            String token = jwtConfig.createToken(user.id);
            return token;

        } catch (Exception e) {
            return 2;
        }


    }

 

 

DBeaver

 

 

intelliJ

 

 

    public User userLogin(UserRequest userRequest) {
        // DB 에서 유저 정보를 조회하는 메소드
        String sql = "SELECT *\n" +
                " from user\n" +
                " where email = ? ;";
        return jdbcTemplate.queryForObject(sql, new UserRowMapper(), userRequest.email);
    }

 

 

- entity 패키지 생성

- User 클래스 생성

 

package com.example.jwt3.entity;

public class User {
    public long id;
    public String email;
    public String password;
    public String nickname;
    public String role;
    public String createdAt;

    public User() {
    }

    public User(long id, String email, String password, 
                String nickname, String role, String createdAt) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.role = role;
        this.createdAt = createdAt;
    }
}

 

- dto 패키지

- UserLoginResponse 클래스 생성 

package com.example.jwt3.dto;

public class UserLoginResponse {
    public String token;

    public UserLoginResponse() {
    }

    public UserLoginResponse(String token) {
        this.token = token;
    }
}

 

Postman

 

 

1.1. 음식점 목록 조회

다양한 조건으로 음식점 목록을 검색하고 조회할 수 있는 API입니다.

      • URL: /api/v1/restaurants
      • Method: GET
      • 설명:
        • 페이징 처리된 음식점 목록을 반환합니다.
        • 카테고리 필터링과 키워드 검색을 지원합니다.
        • 음식점의 평균 평점과 리뷰 수를 포함합니다.
        • 최신 등록순으로 정렬됩니다.

 

Postman

 

 

intelliJ

 

 

@RestController
public class RestaurantController {

    @Autowired
    RestaurantService restaurantService;

     //- 페이징 처리된 음식점 목록을 반환합니다.
    // - 카테고리 필터링과 키워드 검색을 지원합니다.
    //- 음식점의 평균 평점과 리뷰 수를 포함합니다.
   //- 최신 등록순으로 정렬됩니다.
    @GetMapping("/api/v1/restaurants")
    public ResponseEntity<RestaurantListResponse> getRestaurants(@RequestParam int page,
                                                                 @RequestParam int size,
                                                                 @RequestParam(required = false) String category,
                                                                 @RequestParam(required = false) String keyword) {
        // 카테코리는 있고 키워드가 없는 경우
        if (category == null && keyword == null) {
            RestaurantListResponse restaurantListResponse =
                    restaurantService.getRestaurants(page, size, category, null);
            return ResponseEntity.status(200).body(restaurantListResponse);

        } else if (category == null && keyword != null) {
            // 카테고리가 없고 키워드가 있는경우
            RestaurantListResponse restaurantListResponse =
                    restaurantService.getRestaurants(page, size, null, keyword);
            return ResponseEntity.status(200).body(restaurantListResponse);


        } else {
            // 카테고리와 키워드가 모두 있는 경우
            RestaurantListResponse restaurantListResponse =
                    restaurantService.getRestaurants(page, size, category, keyword);
            return ResponseEntity.status(200).body(restaurantListResponse);


        }
    }

 

 

        • Query Parameters:
page: 페이지 번호 (기본값: 1)
size: 페이지 크기 (기본값: 10)
category: 카테고리 필터 (선택) - 한식, 중식, 일식, 양식 등
keyword: 검색어 - 이름, 주소 검색 (선택, 최소 2글자 이상)

 

@Service
public class RestaurantService {
    @Autowired
    private RestaurantDAO restaurantDAO;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    JwtConfig jwtConfig;
    
    
    public RestaurantListResponse getRestaurants(int page,
                                                 int size,
                                                 String category,
                                                 String keyword){
        // 카테고리가 있고 키워드는 없는 경우
        if(category != null && keyword == null){
            int offset = (page - 1) * size;
            List<RestaurantResponse> restaurantList =
                    restaurantDAO.getRestaurants(offset, size, category, null);
            int totalElements = restaurantDAO.getTotalElements(category, null);
            int totalPages = (int) Math.ceil( (double) totalElements / size );

            PageableResponse pageableResponse =
                    new PageableResponse(page, size, totalElements,totalPages);
            RestaurantListResponse restaurantListResponse =
                    new RestaurantListResponse(restaurantList, pageableResponse);
            return restaurantListResponse;
        } else if(category == null && keyword != null){
            // 카테고리가 없고 키워드가 있는 경우
            int offset = (page - 1) * size;
            List<RestaurantResponse> restaurantList = 
                    restaurantDAO.getRestaurants(offset, size, null, keyword);
            int totalElements = restaurantDAO.getTotalElements(null, keyword);
            int totalPages = (int) Math.ceil( (double) totalElements / size );

            PageableResponse pageableResponse =
                    new PageableResponse(page, size, totalElements,totalPages);
            RestaurantListResponse restaurantListResponse =
                    new RestaurantListResponse(restaurantList, pageableResponse);
            return restaurantListResponse;
        } else {
            // 카테고리와 키워드가 모두 있는 경우
            int offset = (page - 1) * size;
            List<RestaurantResponse> restaurantList = 
                    restaurantDAO.getRestaurants(offset, size, category, keyword);
            int totalElements = restaurantDAO.getTotalElements(category, keyword);
            int totalPages = (int) Math.ceil( (double) totalElements / size );

            PageableResponse pageableResponse =
                    new PageableResponse(page, size, totalElements,totalPages);
            RestaurantListResponse restaurantListResponse =
                    new RestaurantListResponse(restaurantList, pageableResponse);
            return restaurantListResponse;
        }

        
    }

 

        • Response:
{
    "content": [
        {
            "id": 1,
            "name": "맛있는 식당",
            "category": "한식",
            "address": "서울시 강남구",
            "phone": "02-1234-5678",
            "description": "전통 한식집",
            "avgRating": 4.5,
            "reviewCount": 100,
            "createdAt": "2024-12-27T10:00:00"
        }
    ],
    "pageable": {
        "page": 1,
        "size": 10,
        "totalElements": 100,
        "totalPages": 10
    }
}

 

 

DBeaver

 

 

intelliJ

 

    public List<RestaurantResponse> getRestaurants(int offset,
                                                   int size,
                                                   String category,
                                                   String keyword) {
        // 카테고리가 있고, 키워드가 없는 경우.
        if(category != null && keyword == null){
            String sql = "SELECT r.id, r.name, r.category, r.address, r.phone, r.description, \n" +
                    "\t\t\tIFNULL(  avg(rv.rating) , 0)  avg_rating   ,\n" +
                    "\t\t\tcount(rv.id)  review_count,\n" +
                    "\t\t\tr.created_at\n" +
                    "from restaurant r\n" +
                    "left join review  rv\n" +
                    "on r.id = rv.restaurant_id\n" +
                    "where r.category = ? \n" +
                    "group by r.id \n" +
                    "order by r.created_at desc\n" +
                    "limit ? , ? ;";
            return jdbcTemplate.query(sql, new RestaurantRowMapper(), category, offset, size);
        } else if(category == null && keyword != null){
            // 카테고리가 없고, 키워드가 있는 경우.
            String sql = "SELECT r.id, r.name, r.category, r.address, r.phone, r.description, \n" +
                    "\t\t\tIFNULL(  avg(rv.rating) , 0)  avg_rating   ,\n" +
                    "\t\t\tcount(rv.id)  review_count,\n" +
                    "\t\t\tr.created_at\n" +
                    "from restaurant r\n" +
                    "left join review  rv\n" +
                    "on r.id = rv.restaurant_id\n" +
                    "where r.name like ? or r.address like ? \n" +
                    "group by r.id \n" +
                    "order by r.created_at desc\n" +
                    "limit ? , ? ;";
            return jdbcTemplate.query(sql, new RestaurantRowMapper(), 
                    "%"+keyword+"%", "%"+keyword+"%" , offset, size);
        } else {
            // 카테고리와 키워드 둘다 있는 경우
            String sql = "SELECT r.id, r.name, r.category, r.address, r.phone, r.description, \n" +
                    "\t\t\tIFNULL(  avg(rv.rating) , 0)  avg_rating   ,\n" +
                    "\t\t\tcount(rv.id)  review_count,\n" +
                    "\t\t\tr.created_at\n" +
                    "from restaurant r\n" +
                    "left join review  rv\n" +
                    "on r.id = rv.restaurant_id\n" +
                    "where (r.name like ? or r.address like ? ) and category = ? \n" +
                    "group by r.id \n" +
                    "order by r.created_at desc\n" +
                    "limit ? ,  ? ;";
            return jdbcTemplate.query(sql, new RestaurantRowMapper(), 
                    "%"+keyword+"%", "%"+keyword+"%", category, offset, size);
        }



 

package com.example.jwt3.dto;

public class RestaurantResponse {

    public Long id;
    public String  name;
    public String category;
    public String address;
    public String phone;
    public String description;
    public Double avgRating;
    public Integer reviewCount;
    public String createdAt;

    public RestaurantResponse() {
    }

    public RestaurantResponse(Long id, String name, String category, 
                              String address, String phone, String description, 
                              Double avgRating, Integer reviewCount, String createdAt) {
        this.id = id;
        this.name = name;
        this.category = category;
        this.address = address;
        this.phone = phone;
        this.description = description;
        this.avgRating = avgRating;
        this.reviewCount = reviewCount;
        this.createdAt = createdAt;
    }
}

 

 

 

package com.example.jwt3.dto;

public class PageableResponse {
    public Integer page;
    public Integer size;
    public Integer totalElements;
    public Integer totalPages;


    public PageableResponse() {
    }

    public PageableResponse(Integer page, Integer size, 
                            Integer totalElements, Integer totalPages) {
        this.page = page;
        this.size = size;
        this.totalElements = totalElements;
        this.totalPages = totalPages;
    }
}

 

 

Postman

 

 

1.2. 음식점 상세 조회

특정 음식점의 상세 정보와 메뉴 목록을 조회하는 API입니다.

 

        • URL: /api/v1/restaurants/{id}
        • Method: GET
        • 설명:
          • 음식점의 기본 정보를 제공합니다.
          • 해당 음식점의 전체 메뉴 목록을 함께 반환합니다.
          • 평균 평점과 총 리뷰 수를 포함합니다.
          • 각 메뉴별 리뷰 수도 함께 제공됩니다.
          • 존재하지 않는 음식점 ID인 경우 404 에러를 반환합니다.

 

intelliJ

 

    @GetMapping("/api/v1/restaurants/{id}")
    public ResponseEntity<RestaurantDetailResponse> getRestaurantDetail(
         @PathVariable long id) {
        RestaurantDetailResponse restaurantDetailResponse =
                restaurantService.getRestaurantDetail(id);
        return ResponseEntity.status(200).body(restaurantDetailResponse);
    }

 

 

package com.example.jwt3.dto;

import java.util.List;

public class RestaurantDetailResponse {
    public Long id;
    public String name;
    public String category;
    public String address;
    public String phone;
    public String description;
    public Double avgRating;
    public Integer reviewCount;
    public String createdAt;
    public List<MenuListResponse> menus;

    public RestaurantDetailResponse() {
    }

    public RestaurantDetailResponse(Long id, String name, 
                                    String category, String address, String phone, 
                                    String description, Double avgRating, Integer reviewCount, 
                                    String createdAt, List<MenuListResponse> menus) {
        this.id = id;
        this.name = name;
        this.category = category;
        this.address = address;
        this.phone = phone;
        this.description = description;
        this.avgRating = avgRating;
        this.reviewCount = reviewCount;
        this.createdAt = createdAt;
        this.menus = menus;
    }
}

      public RestaurantDetailResponse getRestaurantDetail(long id){
        RestaurantResponse restaurantResponse =
                restaurantDAO.getRestaurantDetail(id);

        List<MenuListResponse> menuList = restaurantDAO.getMenuList(id);

        RestaurantDetailResponse restaurantDetailResponse =
                new RestaurantDetailResponse();
        restaurantDetailResponse.id = restaurantResponse.id;
        restaurantDetailResponse.menus = menuList;
        restaurantDetailResponse.address = restaurantResponse.address;
        restaurantDetailResponse.createdAt = restaurantResponse.createdAt;
        restaurantDetailResponse.avgRating = restaurantResponse.avgRating;
        restaurantDetailResponse.category = restaurantResponse.category;
        restaurantDetailResponse.description = restaurantResponse.description;
        restaurantDetailResponse.name = restaurantResponse.name;
        restaurantDetailResponse.phone = restaurantResponse.phone;
        restaurantDetailResponse.reviewCount = restaurantResponse.reviewCount;

        return restaurantDetailResponse;

    }

 

          • Response:
{
		"restaurant": {
		    "id": 1,
		    "name": "맛있는 식당",
		    "category": "한식",
		    "address": "서울시 강남구",
		    "phone": "02-1234-5678",
		    "description": "전통 한식집",
		    "avgRating": 4.5,
		    "reviewCount": 100,
		    "createdAt": "2024-12-27T10:00:00",
	  },
    "menus": [
        {
            "id": 1,
            "name": "김치찌개",
            "price": 8000,
            "description": "돼지고기 김치찌개",
            "category": "찌개",
            "reviewCount": 50
        }
    ]
}

 

package com.example.jwt3.dto;

public class MenuListResponse {

    public Long id;
    public String name;
    public Integer price;
    public String description;
    public String category;
    public Integer reviewCount;

    public MenuListResponse() {
    }

    public MenuListResponse(Long id, String name, Integer price, 
                            String description, String category, Integer reviewCount) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.description = description;
        this.category = category;
        this.reviewCount = reviewCount;
    }
}

 

DBeaver

 

 

 

intelliJ

 

    public RestaurantResponse getRestaurantDetail(long id){
        String sql = "SELECT r.id, r.name, r.category, r.address,\n" 
                "r.phone, r.description, \n" +
                "IFNULL(  avg(rv.rating) , 0)  avg_rating   ,\n" +
                "count(rv.id)  review_count,\n" +
                "r.created_at\n" +
                "from restaurant r\n" +
                "left join review  rv\n" +
                "on r.id = rv.restaurant_id\n" +
                "where r.id = ? \n" +
                "group by r.id;";
        return jdbcTemplate.queryForObject(sql, new RestaurantRowMapper(), id );
    }

    public static class MenuListRowMapper implements RowMapper<MenuListResponse>{

        @Override
        public MenuListResponse mapRow(ResultSet rs, int rowNum) throws SQLException {
            MenuListResponse menuListResponse = new MenuListResponse();
            menuListResponse.id = rs.getLong("id");
            menuListResponse.name = rs.getString("name");
            menuListResponse.price = rs.getInt("price");
            menuListResponse.description = rs.getString("description");
            menuListResponse.category = rs.getString("category");
            menuListResponse.reviewCount = rs.getInt("reviewCount");
            return menuListResponse;
        }
    }

 

 

Postman

 

 

728x90
반응형
home top bottom
}