fix(game): 없는 게임 ID 404 전환 + 프로토타입 dead code 제거

- abstracts/(4파일)·GameCatalog·fragments/header.jspf 삭제(외부참조 0 확인)
- GameController.gameDetail(): GameCatalog fallback(redirect/정적뷰) 제거 →
  DB 미존재 게임 ID = HTTP 404(ResponseStatusException)
- ApiExceptionControllerAdvice: ResponseStatusException 핸들러 추가(404 status
  보존 — 기존 Exception 핸들러가 500으로 가리던 것 수정)
- 회귀 가드 gameDetailThrowsNotFoundWhenGameMissing 추가

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
This commit is contained in:
이정수 2026-06-29 19:10:23 +09:00
parent 5e8816ed4a
commit d143a5cedd
9 changed files with 31 additions and 135 deletions

View File

@ -1,7 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
public class ErrorResult extends Result{
public ErrorResult(int status) {
super(status);
}
}

View File

@ -1,7 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
public abstract class Request {
public boolean IsReceivedAllField() {
return true;
}
}

View File

@ -1,27 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
public abstract class Result {
public int status;
public String message;
public Result() {}
public Result(int status) {
this.status = status;
switch (status)
{
case 200:
this.message = "Success"; return;
case 400:
this.message = "Invalid Request"; return;
case 401:
this.message = "세션 만료"; return;
case 1000:
this.message = "NULL USERS"; return;
default:
System.out.println("잘못된 status 케이스");
this.message = "";
return;
}
}
}

View File

@ -1,18 +0,0 @@
package com.pandoli365.bibimbap.abstracts;
import jakarta.servlet.http.HttpSession;
public abstract class Service<Req extends Request, Res extends Result> {
public boolean is_login = false;
public abstract Res StartService(HttpSession session, Req request);
public Res ChackService(HttpSession session, Req request){
if (is_login && session.getAttribute("id") == null)
return (Res) new ErrorResult(401);
if(request != null && !request.IsReceivedAllField())
return (Res) new ErrorResult(401);
return StartService(session, request);
}
}

View File

@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashMap;
import java.util.Map;
@ -24,6 +25,16 @@ public class ApiExceptionControllerAdvice {
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(body);
}
// ResponseStatusException(: gameDetail 404) 지정 status 보존한다.
// 구체적인 타입이라 아래 Exception 핸들러보다 우선 매칭된다.
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatus(ResponseStatusException exception) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("status", exception.getStatusCode().value());
body.put("message", exception.getReason() != null ? exception.getReason() : "not found");
return ResponseEntity.status(exception.getStatusCode()).body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception exception) {
log.error("API request failed", exception);

View File

@ -2,7 +2,6 @@ package com.pandoli365.bibimbap.controller.api;
import com.pandoli365.bibimbap.data.GameData;
import com.pandoli365.bibimbap.data.GameLikeData;
import com.pandoli365.bibimbap.game.GameCatalog;
import com.pandoli365.bibimbap.mapper.GameCommentsMapper;
import com.pandoli365.bibimbap.mapper.GameLikesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
@ -23,9 +22,9 @@ 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.RequestParam;
import org.springframework.web.server.ResponseStatusException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -147,29 +146,7 @@ public class GameController {
return "game-detail";
}
if (id < Integer.MIN_VALUE || id > Integer.MAX_VALUE || !GameCatalog.isValidId((int) id)) {
return "redirect:/";
}
int intId = (int) id;
int idx = GameCatalog.toIndex(intId);
model.addAttribute("gameId", intId);
model.addAttribute("gameName", GameCatalog.NAMES[idx]);
model.addAttribute("creator", GameCatalog.CREATORS[idx]);
model.addAttribute("likeCount", GameCatalog.LIKE_COUNTS[idx]);
model.addAttribute("likeCountFormatted", String.format("%,d", GameCatalog.LIKE_COUNTS[idx]));
model.addAttribute("creatorNote", GameCatalog.CREATOR_NOTES[idx]);
model.addAttribute("gitUrl", safeExternalUrl(GameCatalog.GIT_URLS[idx]));
model.addAttribute("webglUrl", webglUrlForGame(intId));
model.addAttribute("webglFrameSrc", webglFrameSrc(webglUrlForGame(intId)));
model.addAttribute("webglDeployPath", webglUrlForGame(intId));
model.addAttribute("owner", false);
model.addAttribute("comments", List.of());
model.addAttribute("reviews", List.of());
model.addAttribute("currentUserId", sessionUserId(session));
model.addAttribute("userRole", (String) session.getAttribute("role"));
model.addAttribute("liked", false);
return "game-detail";
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
@GetMapping("/game/{id}/edit")

View File

@ -1,27 +0,0 @@
package com.pandoli365.bibimbap.game;
public final class GameCatalog {
private GameCatalog() {
}
public static final String[] NAMES = {};
public static final String[] CREATORS = {};
public static final int[] LIKE_COUNTS = {};
public static final String[] CREATOR_NOTES = {};
public static final String[] GIT_URLS = {};
public static final int COUNT = NAMES.length;
public static boolean isValidId(int id) {
return id >= 1 && id <= COUNT;
}
public static int toIndex(int id) {
return id - 1;
}
}

View File

@ -1,24 +0,0 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<header class="site-header">
<div class="site-header__inner">
<a class="logo" href="${pageContext.request.contextPath}/">비빔밥</a>
<form class="search" method="get" action="${pageContext.request.contextPath}/">
<input type="search" name="q" value="<c:out value='${q}'/>" placeholder="게임 이름 또는 제작자 검색" aria-label="검색"/>
<button type="submit">검색</button>
</form>
<nav class="auth">
<sec:authorize access="isAuthenticated()">
<a href="${pageContext.request.contextPath}/games/new">게임 올리기</a>
<form action="${pageContext.request.contextPath}/logout" method="post" class="logout-form">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit" class="link-btn">로그아웃</button>
</form>
</sec:authorize>
<sec:authorize access="!isAuthenticated()">
<a href="${pageContext.request.contextPath}/login">로그인</a>
<a class="btn-primary" href="${pageContext.request.contextPath}/register">회원가입</a>
</sec:authorize>
</nav>
</div>
</header>

View File

@ -19,10 +19,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.web.server.ResponseStatusException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowableOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
@ -191,6 +194,21 @@ class GameLikeControllerTest {
verifyNoInteractions(gameLikesMapper);
}
// ---- B2 회귀 가드: 없는 게임 ID 상세 접근 404 (GameCatalog fallback 제거 ) ----
@Test
void gameDetailThrowsNotFoundWhenGameMissing() {
GameController controller = controller();
when(gamesMapper.getGame(99999L)).thenReturn(null);
ResponseStatusException ex = catchThrowableOfType(
() -> controller.gameDetail(99999L, new ExtendedModelMap(), new MockHttpSession()),
ResponseStatusException.class);
assertThat(ex).isNotNull();
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
// ---- helpers ----
private GameController controller() {