Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spring 체스 - 2단계] 연로그(권시연) 미션 제출합니다. #422

Merged
merged 26 commits into from
May 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1626014
refactor: Exception 발생 시 콘솔에 로그 출력
yeon-06 Apr 25, 2022
cfbc8b8
refactor: searchResult 가져오는 경우 post -> get 변경
yeon-06 Apr 25, 2022
e85a315
chore: DB 구조 변경
yeon-06 Apr 25, 2022
df55b8a
docs: 요구사항 정리
yeon-06 Apr 26, 2022
b67bcef
chore: 테스트 DB 구조 변경
yeon-06 Apr 26, 2022
0767b5c
refactor: game 생성 시 title과 password 값 함께 받기
yeon-06 Apr 26, 2022
3798222
feat: 게임 목록 조회 기능 구현
yeon-06 Apr 26, 2022
3739f01
refactor: css 변경
yeon-06 Apr 26, 2022
e15b345
feat: 삭제 기능 구현
yeon-06 Apr 27, 2022
0add4bb
fix: 생성, 이동 안되는 현상 수정
yeon-06 Apr 27, 2022
326213f
test: 수정된 경고 문구 반영
yeon-06 Apr 27, 2022
dabd1cf
refactor: dao 공통 처리 로직 분리
yeon-06 Apr 27, 2022
5b4db87
refactor: path에서 id 받아오는 로직 제거
yeon-06 Apr 27, 2022
cf3a525
fix: 삭제 요청 보내지지 않는 현상 수정
yeon-06 Apr 27, 2022
9b887de
refactor: URL 수정
yeon-06 Apr 28, 2022
d81084c
style: 공백 컨벤션 준수
yeon-06 Apr 29, 2022
5e21666
refactor: printStackTrace() 대신 Logger 사용
yeon-06 Apr 30, 2022
1d6024a
refactor: queryForObject 실행 시 발생하는 예외처리 추가
yeon-06 Apr 30, 2022
64e05f2
refactor: 도메인에서 dto를 생성하는 로직 제거
yeon-06 Apr 30, 2022
981ba51
refactor: id 값은 URI에서 가져오도록 수정
yeon-06 Apr 30, 2022
35e47f3
refactor: dto의 기본 생성자 private으로 변경
yeon-06 Apr 30, 2022
5c71f71
refactor: 조회만 하는 경우 readOnly 속성 추가
yeon-06 Apr 30, 2022
6bfc2fa
refactor: 사용하지 않는 API 삭제
yeon-06 Apr 30, 2022
c601e33
refactor: 테이블과 매핑되는 entity 생성 후 dao에서 이용
yeon-06 Apr 30, 2022
6becd9a
refactor: 다양한 HTTP 상태 코드 이용
yeon-06 Apr 30, 2022
cfc7204
feat: 404 에러 발생 시 커스텀 페이지 출력
yeon-06 Apr 30, 2022
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 웹 체스
> (e): Exception 처리

### 1단계 요구 사항
- 스프링 애플리케이션으로 체스 실행
- Spring MVC를 활용 (Spark 대체)
- JdbcTemplate 활용 (기존의 Connection 직접 생성 로직 대체)
- 요청 받기: @Controller / @RestController 활용

### 2단계 요구 사항
- 체스방 생성
- 제목, 비밀번호 입력받기
- 생성 시 만들어지는 고유값은 url에 활용
- 체스방 목록 조회
- 제목 클릭 시 체스방 참가
- 삭제 버튼 클릭 시 체스방 삭제
- 비밀번호 입력받기
- (e) 진행중인 체스방은 삭제 불가능
- 동일한 비밀번호라면 삭제
8 changes: 5 additions & 3 deletions docker/db/mysql/init/init.sql
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
CREATE TABLE game
(
id BIGINT NOT NULL AUTO_INCREMENT,
id BIGINT NOT NULL AUTO_INCREMENT,
title VARCHAR(30) NOT NULL,
password VARCHAR(30) NOT NULL,
running BOOLEAN NOT NULL DEFAULT true,
PRIMARY KEY (id)
);

CREATE TABLE event
(
game_id BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
game_id BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
description VARCHAR(20)
);
14 changes: 14 additions & 0 deletions src/main/java/chess/controller/CustomErrorController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package chess.controller;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class CustomErrorController implements ErrorController {

@RequestMapping("/error")
public String handleError() {
return "error";
}
}
14 changes: 14 additions & 0 deletions src/main/java/chess/controller/ExceptionResolver.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package chess.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
Expand All @@ -9,14 +12,25 @@
public class ExceptionResolver {

private static final String DEFAULT_SERVER_EXCEPTION_MESSAGE = "예상치 못한 문제가 발생하였습니다.";
private static final String NO_DATA_EXCEPTION_MESSAGE = "해당 데이터를 찾을 수 없습니다.";
private static final String EMPTY_STRING = "";
private static final Logger logger = LoggerFactory.getLogger(ExceptionResolver.class);

@ExceptionHandler
public ResponseEntity<String> handleException(Exception e) {
logger.error(EMPTY_STRING, e);
return new ResponseEntity<>(DEFAULT_SERVER_EXCEPTION_MESSAGE, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler
public ResponseEntity<String> handleException(EmptyResultDataAccessException e) {
logger.error(EMPTY_STRING, e);
return new ResponseEntity<>(NO_DATA_EXCEPTION_MESSAGE, HttpStatus.NOT_FOUND);
Comment on lines 22 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인스턴스 생성 or Builder 둘 중 하나로 통일하는 건 어떨까요?

}

@ExceptionHandler
public ResponseEntity<String> handleException(IllegalArgumentException e) {
logger.error(EMPTY_STRING, e);
return ResponseEntity.badRequest().body(e.getMessage());
}
}
28 changes: 20 additions & 8 deletions src/main/java/chess/controller/GameController.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package chess.controller;

import chess.domain.event.MoveEvent;
import chess.domain.event.MoveRoute;
import chess.dto.CreateGameDto;
import chess.dto.CreateGameRequest;
import chess.dto.CreateGameResponse;
import chess.dto.DeleteGameRequest;
import chess.dto.GameDto;
import chess.dto.MoveRouteDto;
import chess.service.ChessService;
import chess.util.ResponseUtil;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

Expand All @@ -27,8 +31,9 @@ public GameController(ChessService chessService) {
}

@PostMapping("/new")
public CreateGameDto createGame() {
return chessService.initGame();
@ResponseStatus(HttpStatus.CREATED)
public CreateGameResponse createGame(@RequestBody CreateGameRequest createGameRequest) {
return chessService.initGame(createGameRequest);
}

@GetMapping("/{id}")
Expand All @@ -38,10 +43,17 @@ public ModelAndView findGame(@PathVariable int id) {
}

@PostMapping("/{id}")
public ModelAndView playGame(@PathVariable int id,
@RequestBody MoveRoute moveRoute) {
chessService.playGame(id, new MoveEvent(moveRoute));
public ModelAndView playGame(@PathVariable int id
, @RequestBody MoveRouteDto moveRoute) {
chessService.playGame(id, moveRoute);
GameDto gameDto = chessService.findGame(id);
return ResponseUtil.createModelAndView(HTML_TEMPLATE_PATH, gameDto);
}

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION)
public void delete(@PathVariable int id
,@RequestBody DeleteGameRequest deleteGameRequest) {
Comment on lines +46 to +56
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 내용이 컨벤션처럼 보이는데 컨벤션이 바뀌었을까요?

Copy link
Author

@yeon-06 yeon-06 May 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 아니요ㅠㅠ 값을 수정하다 미처 확인을 못한 것 같습니다😂
기존 내용이 맞습니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하..! 그럴 수 있져 ㅎ_ㅎ..

chessService.deleteGame(id, deleteGameRequest);
}
}
3 changes: 3 additions & 0 deletions src/main/java/chess/controller/HomeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import chess.dto.GameCountDto;
import chess.service.ChessService;
import chess.util.ResponseUtil;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

Expand All @@ -19,6 +21,7 @@ public HomeController(ChessService chessService) {
}

@GetMapping
@ResponseStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION)
public ModelAndView home() {
GameCountDto gameCountDto = chessService.countGames();
return ResponseUtil.createModelAndView(HTML_TEMPLATE_PATH, gameCountDto);
Expand Down
19 changes: 10 additions & 9 deletions src/main/java/chess/controller/SearchController.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package chess.controller;

import chess.dto.GameCountDto;
import chess.dto.SearchResultDto;
import chess.entity.GameEntity;
import chess.dto.GamesResponse;
import chess.service.ChessService;
import chess.util.ResponseUtil;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

Expand All @@ -24,13 +26,12 @@ public SearchController(ChessService chessService) {
}

@GetMapping
public ModelAndView render() {
@ResponseStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION)
public ModelAndView renderSearchPage() {
GameCountDto gameCountDto = chessService.countGames();
return ResponseUtil.createModelAndView(HTML_TEMPLATE_PATH, gameCountDto);
}
List<GameEntity> gameEntities = chessService.selectAllGames();
GamesResponse gamesResponse = new GamesResponse(gameCountDto, gameEntities);

@PostMapping
public SearchResultDto searchResult(@RequestParam(name = "game_id") int gameId) {
return chessService.searchGame(gameId);
return ResponseUtil.createModelAndView(HTML_TEMPLATE_PATH, gamesResponse);
}
}
10 changes: 5 additions & 5 deletions src/main/java/chess/dao/EventDao.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package chess.dao;

import chess.domain.event.Event;
import chess.entity.EventEntity;
import java.util.List;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
Expand All @@ -17,17 +17,17 @@ public EventDao(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}

private final RowMapper<Event> eventRowMapper = (resultSet, rowNum) ->
Event.of(resultSet.getString("type"),
private final RowMapper<EventEntity> eventRowMapper = (resultSet, rowNum) ->
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테이블과 일대일 매핑되는 entity라는 클래스를 별도로 분리했습니다!
나중에 event의 type, description 반환 -> event의 type만 반환 식으로 반환해야하는 값이 달라지면 매번 쿼리문이나 dao의 메서드 등을 수정해야하는 상황을 방지하기 위함입니다!

new EventEntity(resultSet.getString("type"),
resultSet.getString("description"));

public List<Event> findAllByGameId(int gameId) {
public List<EventEntity> findAllByGameId(int gameId) {
final String sql = "SELECT type, description FROM event WHERE game_id = :game_id";
SqlParameterSource paramSource = new MapSqlParameterSource("game_id", gameId);
return namedParameterJdbcTemplate.query(sql, paramSource, eventRowMapper);
}

public void save(int gameId, Event event) {
public void save(int gameId, EventEntity event) {
final String sql = "INSERT INTO event (game_id, type, description)"
+ "VALUES (:game_id, :type, :description)";

Expand Down
58 changes: 41 additions & 17 deletions src/main/java/chess/dao/GameDao.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package chess.dao;

import chess.dto.CreateGameRequest;
import chess.entity.GameEntity;
import chess.util.DaoUtil;
import java.util.List;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

@Repository
Expand All @@ -16,11 +19,34 @@ public GameDao(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}

public int saveAndGetGeneratedId() {
final String sql = "INSERT INTO game VALUES ()";
KeyHolder keyHolder = new GeneratedKeyHolder();
namedParameterJdbcTemplate.update(sql, new EmptySqlParameterSource(), keyHolder);
return keyHolder.getKey().intValue();
private final RowMapper<GameEntity> eventRowMapper = (resultSet, rowNum) ->
new GameEntity(resultSet.getInt("id"),
resultSet.getString("title"),
resultSet.getString("password"),
resultSet.getBoolean("running")
);

public List<GameEntity> selectAll() {
final String sql = "SELECT id, title, password, running FROM game";
return DaoUtil.queryNoParameter(namedParameterJdbcTemplate, sql, eventRowMapper);
}

public GameEntity findById(int id) {
final String sql = "SELECT id, title, password, running FROM game WHERE id = :id";
MapSqlParameterSource paramSource = new MapSqlParameterSource();
paramSource.addValue("id", id);

return namedParameterJdbcTemplate.queryForObject(sql, paramSource, eventRowMapper);
}

public int saveAndGetGeneratedId(CreateGameRequest request) {
final String sql = "INSERT INTO game(title, password) VALUES (:title, :password)";

MapSqlParameterSource paramSource = new MapSqlParameterSource();
paramSource.addValue("title", request.getTitle());
paramSource.addValue("password", request.getPassword());

return DaoUtil.queryForKeyHolder(namedParameterJdbcTemplate, sql, paramSource);
}

public void finishGame(int gameId) {
Expand All @@ -31,21 +57,19 @@ public void finishGame(int gameId) {
namedParameterJdbcTemplate.update(sql, paramSource);
}

public boolean checkById(int gameId) {
final String sql = "SELECT COUNT(*) FROM game WHERE id = :game_id";

MapSqlParameterSource paramSource = new MapSqlParameterSource("game_id", gameId);
int existingGameCount = namedParameterJdbcTemplate.queryForObject(sql, paramSource, Integer.class);
return existingGameCount > 0;
}

public int countAll() {
final String sql = "SELECT COUNT(*) FROM game";
return namedParameterJdbcTemplate.queryForObject(sql, new EmptySqlParameterSource(), Integer.class);
return DaoUtil.queryForInt(namedParameterJdbcTemplate, sql, new EmptySqlParameterSource());
}

public int countRunningGames() {
final String sql = "SELECT COUNT(*) FROM game WHERE running = true";
return namedParameterJdbcTemplate.queryForObject(sql, new EmptySqlParameterSource(), Integer.class);
return DaoUtil.queryForInt(namedParameterJdbcTemplate, sql, new EmptySqlParameterSource());
}

public int delete(int id) {
final String sql = "delete from game where id = :id";
MapSqlParameterSource paramSource = new MapSqlParameterSource("id", id);
return namedParameterJdbcTemplate.update(sql, paramSource);
}
}
8 changes: 5 additions & 3 deletions src/main/java/chess/domain/event/Event.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package chess.domain.event;

import chess.entity.EventEntity;

public abstract class Event {

public static Event of(String type, String description) {
EventType eventType = EventType.valueOf(type);
public static Event of(EventEntity eventEntity) {
EventType eventType = EventType.valueOf(eventEntity.getType());
if (eventType == EventType.MOVE) {
return new MoveEvent(description);
return new MoveEvent(eventEntity.getDescription());
}
return new InitEvent();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/chess/domain/game/BlackTurn.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protected Game continueGame() {
}

@Override
protected GameState getState() {
public GameState getState() {
return GameState.BLACK_TURN;
}

Expand Down
7 changes: 5 additions & 2 deletions src/main/java/chess/domain/game/Game.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package chess.domain.game;

import chess.domain.board.Board;
import chess.domain.event.Event;
import chess.domain.game.statistics.GameResult;
import chess.dto.GameDto;
import chess.domain.game.statistics.GameState;

public interface Game {

Expand All @@ -12,5 +13,7 @@ public interface Game {

GameResult getResult();

GameDto toDtoOf(int gameId);
Copy link
Author

@yeon-06 yeon-06 Apr 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인에서 dto를 생성한다 = dto 가 수정되면 도메인을 수정해야한다

라고 생각해서 약간의 로직을 바꿨습니다!
dto.of(도메인) 의 방식으로 dto를 생성할 수 있게 수정했습니다

Board getBoard();

GameState getState();
}
2 changes: 1 addition & 1 deletion src/main/java/chess/domain/game/GameOver.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public GameResult getResult() {
}

@Override
protected GameState getState() {
public GameState getState() {
return GameState.OVER;
}

Expand Down
9 changes: 7 additions & 2 deletions src/main/java/chess/domain/game/NewGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import chess.domain.board.Board;
import chess.domain.event.Event;
import chess.domain.game.statistics.GameResult;
import chess.dto.GameDto;
import chess.domain.game.statistics.GameState;
import chess.util.BoardMapGeneratorUtil;

public final class NewGame implements Game {
Expand All @@ -30,7 +30,12 @@ public GameResult getResult() {
}

@Override
public GameDto toDtoOf(int gameId) {
public Board getBoard() {
throw new UnsupportedOperationException(GAME_NOT_STARTED_EXCEPTION_MESSAGE);
}

@Override
public GameState getState() {
throw new UnsupportedOperationException(GAME_NOT_STARTED_EXCEPTION_MESSAGE);
}
}
Loading