diff --git a/src/main/java/game/controller/GameStartController.java b/src/main/java/game/controller/GameStartController.java index 9712cc2..41f17de 100644 --- a/src/main/java/game/controller/GameStartController.java +++ b/src/main/java/game/controller/GameStartController.java @@ -59,6 +59,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.sendError(HttpServletResponse.SC_FORBIDDEN, "게임 시작 권한이 없습니다. (host만 가능)"); return; } + int updated = roomPlayerDao.updatePlayersToInGame(roomId); System.out.println("[GameStart] roomId=" + roomId + " updatedPlayers=" + updated); diff --git a/src/main/java/lobby/controller/CreateRoomController.java b/src/main/java/lobby/controller/CreateRoomController.java index 047e43c..015ee61 100644 --- a/src/main/java/lobby/controller/CreateRoomController.java +++ b/src/main/java/lobby/controller/CreateRoomController.java @@ -60,6 +60,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) String roomId = roomResult.getId(); + session.setAttribute("hostUserId", hostUserId); + roomPlayerDAO.enterIfAbsent(roomResult.getId(), hostUserId); LobbyWebSocket.broadcastRoomList(); diff --git a/src/main/java/room/controller/RoomController.java b/src/main/java/room/controller/RoomController.java new file mode 100644 index 0000000..86bee1b --- /dev/null +++ b/src/main/java/room/controller/RoomController.java @@ -0,0 +1,144 @@ +package room.controller; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.google.gson.Gson; + +import room.dao.RoomPlayerDAO; +import room.dao.RoomPlayerDAOImpl; +import room.dto.RoomActiveCountDTO; + +@WebServlet("/room/*") +public class RoomController extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final Gson gson = new Gson(); + private final RoomPlayerDAO roomPlayerDAO = new RoomPlayerDAOImpl(); + + @FunctionalInterface + private interface Handler { + void handle(HttpServletRequest req, HttpServletResponse res, String userId) throws Exception; + } + + private final Map getHandlers = new HashMap<>(); + + @Override + public void init() { + getHandlers.put("count", this::handleGetCount); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + + String action = getAction(req); + if (action == null) { + sendJson(res, 400, ApiError.of("잘못된 요청입니다.")); + return; + } + + String userId = getLoginUserId(req); + if (userId == null || userId.isBlank()) { + sendJson(res, 401, ApiError.of("로그인이 필요합니다.")); + return; + } + + Handler handler = getHandlers.get(action); + if (handler == null) { + sendJson(res, 404, ApiError.of("존재하지 않는 API입니다.")); + return; + } + + try { + handler.handle(req, res, userId); + } catch (Exception e) { + e.printStackTrace(); + sendJson(res, 500, ApiError.of("서버 오류가 발생했습니다.")); + } + } + + /** + * GET /room/count?roomId= + * 응답: { ok:true, data:{ roomId, activeCount } } + */ + private void handleGetCount(HttpServletRequest req, HttpServletResponse res, String userId) throws Exception { + String roomId = req.getParameter("roomId"); + if (roomId == null || roomId.isBlank()) { + sendJson(res, 400, ApiError.of("roomId가 필요합니다.")); + return; + } + int activeCount = roomPlayerDAO.countActivePlayers(roomId); + RoomActiveCountDTO dto = new RoomActiveCountDTO(roomId, activeCount); + sendJson(res, 200, ApiSuccess.of(dto)); + } + + private String getAction(HttpServletRequest req) { + String pathInfo = req.getPathInfo(); + if (pathInfo == null || "/".equals(pathInfo)) + return null; + return pathInfo.substring(1); + } + + private String getLoginUserId(HttpServletRequest req) { + HttpSession session = req.getSession(false); + return (session == null) ? null : (String)session.getAttribute("loginUserId"); + } + + private void sendJson(HttpServletResponse res, int status, Object body) throws IOException { + res.setStatus(status); + res.setContentType("application/json; charset=UTF-8"); + res.getWriter().write(gson.toJson(body)); + } + + private static class ApiSuccess { + private final boolean ok = true; + private final T data; + + private ApiSuccess(T data) { + this.data = data; + } + + public static ApiSuccess of(T data) { + return new ApiSuccess<>(data); + } + + public boolean isOk() { + return ok; + } + + public T getData() { + return data; + } + } + + private static class ApiError { + private final boolean ok = false; + private final String message; + + private ApiError(String message) { + this.message = message; + } + + public static ApiError of(String message) { + return new ApiError(message); + } + + public boolean isOk() { + return ok; + } + + public String getMessage() { + return message; + } + } +} diff --git a/src/main/java/room/controller/ViewRoomController.java b/src/main/java/room/controller/ViewRoomController.java index ce03157..bbfb4a6 100644 --- a/src/main/java/room/controller/ViewRoomController.java +++ b/src/main/java/room/controller/ViewRoomController.java @@ -7,19 +7,26 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import room.dao.RoomDAO; +import room.dao.RoomDAOImpl; @WebServlet("/room") public class ViewRoomController extends HttpServlet { private static final long serialVersionUID = 1L; + private final RoomDAO roomDAO = new RoomDAOImpl(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { String roomId = request.getParameter("roomId"); String playType = request.getParameter("playType"); + HttpSession session = request.getSession(false); - if (roomId == null) { + if (playType == null || roomId == null || session == null) { response.sendRedirect("/lobby"); return; } @@ -27,6 +34,17 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) request.setAttribute("roomId", roomId); request.setAttribute("playType", playType); + String userId = (String)session.getAttribute("loginUserId"); + String hostUserId = roomDAO.findHostUserIdByRoomId(roomId); + + if (hostUserId == null) { + response.sendRedirect("/lobby?error=host_not_found"); + return; + } + + session.setAttribute("hostUserId", hostUserId); + session.setAttribute("userId", userId); + request.getRequestDispatcher("/WEB-INF/views/room.jsp").forward(request, response); return; } catch (Exception e) { diff --git a/src/main/java/room/dao/RoomDAO.java b/src/main/java/room/dao/RoomDAO.java index 6351c4b..13495fe 100644 --- a/src/main/java/room/dao/RoomDAO.java +++ b/src/main/java/room/dao/RoomDAO.java @@ -22,4 +22,11 @@ RoomDTO createRoom( */ String getHostUserId(String roomId) throws Exception; + /** + * roomId로 방장 userId 조회 + * @param roomId 방 ID + * @return hostUserId (없으면 null) + */ + String findHostUserIdByRoomId(String roomId) throws Exception; + } \ No newline at end of file diff --git a/src/main/java/room/dao/RoomDAOImpl.java b/src/main/java/room/dao/RoomDAOImpl.java index 77ec757..15cc30a 100644 --- a/src/main/java/room/dao/RoomDAOImpl.java +++ b/src/main/java/room/dao/RoomDAOImpl.java @@ -169,6 +169,29 @@ public String getHostUserId(String roomId) throws Exception { } } + @Override + public String findHostUserIdByRoomId(String roomId) throws Exception { + + String sql = """ + SELECT host_user_id + FROM room + WHERE id = ? + """; + + try ( + Connection conn = DB.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, roomId); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getString("host_user_id"); + } + return null; + } + } + } + private RoomDTO mapToRoom(ResultSet rs) throws SQLException { return RoomDTO.builder() .id(rs.getString("id")) diff --git a/src/main/java/room/dto/RoomActiveCountDTO.java b/src/main/java/room/dto/RoomActiveCountDTO.java new file mode 100644 index 0000000..f0d50b3 --- /dev/null +++ b/src/main/java/room/dto/RoomActiveCountDTO.java @@ -0,0 +1,11 @@ +package room.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RoomActiveCountDTO { + private String roomId; + private int activeCount; +} diff --git a/src/main/java/room/ws/RoomWebSocket.java b/src/main/java/room/ws/RoomWebSocket.java index df7b4f2..8ae79e9 100644 --- a/src/main/java/room/ws/RoomWebSocket.java +++ b/src/main/java/room/ws/RoomWebSocket.java @@ -148,7 +148,7 @@ public void onMessage(Session session, String text) { case "ROOM_EXIT": { String roomId = sessionContext.getRoomId(session); - service.onExit(session, roomId); + service.onExit(session, roomId, "ROOM_EXIT"); sessionContext.leaveRoom(session); service.sendIfOpen(session, "ROOM_EXIT", Map.of()); @@ -215,7 +215,7 @@ public void onClose(Session session, CloseReason reason) { String result = roomService.exitAndHandleHost(roomId, userId); System.out.println("[RoomWS][EXIT] roomId=" + roomId + " userId=" + userId + " result=" + result); - service.onExit(session, roomId); + service.onExit(session, roomId, result); LobbyWebSocket.broadcastRoomList(); } } catch (Exception e) { diff --git a/src/main/java/room/ws/RoomWebSocketService.java b/src/main/java/room/ws/RoomWebSocketService.java index 894a2f6..c5f1422 100644 --- a/src/main/java/room/ws/RoomWebSocketService.java +++ b/src/main/java/room/ws/RoomWebSocketService.java @@ -9,6 +9,8 @@ import com.google.gson.Gson; +import room.dao.RoomDAO; +import room.dao.RoomDAOImpl; import room.dao.RoomPlayerDAO; import room.dao.RoomPlayerDAOImpl; import room.dto.RoomPlayerDTO; @@ -25,6 +27,7 @@ public class RoomWebSocketService { private static final SessionContext sessionContext = SessionContext.getInstance(); private static final RoomSessionRegistry roomRegistry = RoomSessionRegistry.getInstance(); private final RoomPlayerDAO roomPlayerDao = new RoomPlayerDAOImpl(); + private final RoomDAO roomDao = new RoomDAOImpl(); public void sendIfOpen(Session s, String type, Map payload) { if (s == null || !s.isOpen()) @@ -74,22 +77,35 @@ public void onChat(Session session, String roomId, String text) { "text", text)); } - public void onExit(Session session, String roomId) { + public void onExit(Session session, String roomId, String result) { if (roomId == null || roomId.isBlank()) return; roomRegistry.leave(roomId, session); + if ("HOST_CHANGE".equals(result)) { + String hostUserId; + try { + hostUserId = roomDao.getHostUserId(roomId); + if (hostUserId != null) { + broadcastHostChanged(roomId, hostUserId); + } + } catch (Exception e) { + e.printStackTrace(); + } + + } broadcast(roomId, "USER_EXIT", Map.of( "userId", sessionContext.getUserId(session), "nickname", sessionContext.getNickname(session))); + } /** @OnClose/@OnError 최종 정리 */ public void cleanup(Session session) { String roomId = sessionContext.getRoomId(session); if (roomId != null && !roomId.isBlank()) { - onExit(session, roomId); + onExit(session, roomId, null); sessionContext.leaveRoom(session); } else { roomRegistry.removeFromAnyRoom(session); @@ -111,6 +127,17 @@ public void broadcastGameStart(String roomId, String gameId, String playType) { broadcast(roomId, "GAME_START", payload); } + public void broadcastHostChanged(String roomId, String hostUserId) { + if (roomId == null || roomId.isBlank()) + return; + + Map payload = new HashMap<>(); + payload.put("roomId", roomId); + payload.put("hostUserId", hostUserId); + + broadcast(roomId, "HOST_CHANGE", payload); + } + private void broadcast(String roomId, String type, Map payload) { Set sessions = roomRegistry.getSessions(roomId); for (Session s : sessions) { diff --git a/src/main/webapp/WEB-INF/views/lobby.jsp b/src/main/webapp/WEB-INF/views/lobby.jsp index a659d20..ce9fa24 100644 --- a/src/main/webapp/WEB-INF/views/lobby.jsp +++ b/src/main/webapp/WEB-INF/views/lobby.jsp @@ -44,10 +44,8 @@ -
-

게임 방 목록

참여할 게임을 선택하세요

diff --git a/src/main/webapp/WEB-INF/views/room.jsp b/src/main/webapp/WEB-INF/views/room.jsp index e8bbada..1ecad5c 100644 --- a/src/main/webapp/WEB-INF/views/room.jsp +++ b/src/main/webapp/WEB-INF/views/room.jsp @@ -16,6 +16,7 @@ data-room-name="" data-play-type="" data-host-user-id="" + data-user-id="" >
@@ -49,16 +50,14 @@
-

👥 참가자

+

👥 참가자

    -
    - -
    +

    💬 채팅

    diff --git a/src/main/webapp/static/room/room.js b/src/main/webapp/static/room/room.js index 433d1d6..7a5fd8b 100644 --- a/src/main/webapp/static/room/room.js +++ b/src/main/webapp/static/room/room.js @@ -2,6 +2,9 @@ const pageEl = document.querySelector("#room-page"); const roomId = pageEl?.dataset?.roomId || ""; const roomName = pageEl?.dataset?.roomName || ""; + const userId = pageEl?.dataset?.userId || ""; + + const wsStatus = document.querySelector("#ws-status"); const chatInput = document.querySelector("#chat-input"); // input @@ -180,6 +183,27 @@ appendSystemLog("방에서 나갔습니다."); break; } + + case "HOST_CHANGE": { + appendSystemLog("방장이 변경되었습니다."); + const p = msg.payload; + + if (p.hostUserId !== userId) break; + + if (document.querySelector("#btn-start")) break; + + const btn = document.createElement("button"); + btn.type = "button"; + btn.id = "btn-start"; + btn.className = "btn-start"; + btn.textContent = "🎯 시작하기"; + + btn.addEventListener("click", startButtonClick); + + document.querySelector(".side-nav")?.appendChild(btn); + break; + + } case "ERROR": { handleError(msg); @@ -225,32 +249,60 @@ location.href = "/lobby"; }); - const startForm = document.querySelector("#start-form"); - startForm?.addEventListener("submit", async (e) => { - e.preventDefault(); - - const formData = new FormData(startForm); - const params = new URLSearchParams(formData); - - try { - const response = await fetch(startForm.action, { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, - body: params, - }); - - if (!response.ok) { - const text = await response.text(); - alert(`게임 시작 실패: ${text}`); - } - // 성공 시: 서버가 WS로 GAME_START를 보내면 - // room.js의 GAME_START 핸들러가 페이지 이동 처리함 :contentReference[oaicite:2]{index=2} - } catch (err) { - console.error("게임 시작 요청 실패:", err); - alert("게임 시작 요청 중 오류가 발생했습니다."); - } - }); +const btnStart = document.querySelector("#btn-start"); + btnStart?.addEventListener("click", async () => { + startButtonClick() + }); + + async function startButtonClick() { + const page = document.querySelector("#room-page"); + const roomId = page.dataset.roomId; + const playType = page.dataset.playType; + const contextPath = page.dataset.contextPath || ""; + + try { + const countRes = await fetch( + `${contextPath}/room/count?roomId=${encodeURIComponent(roomId)}`, + { method: "GET", credentials: "same-origin" } + ); + const countResult = await countRes.json(); + + if (!countResult.ok) { + alert(countResult.message || "인원 수 조회 실패"); + return; + } + + const { activeCount } = countResult.data; + const minPlayers = playType === "1" ? 4 : 2; + + if (activeCount < minPlayers) { + alert(`아직 인원이 부족합니다. (최소 ${minPlayers}명 필요)`); + return; + } + + const body = new URLSearchParams(); + body.set("roomId", roomId); + body.set("playType", playType); + + const startRes = await fetch(`${contextPath}/game/start`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, + body, + credentials: "same-origin", + }); + + if (!startRes.ok) { + const text = await startRes.text(); + alert(`게임 시작 실패: ${text}`); + return; + } + + } catch (err) { + console.error(err); + alert("게임 시작 요청 중 오류가 발생했습니다."); + } + } /* IME 조합 상태 추적 (한글 뒷글자 중복 방지) */ let isComposing = false; chatInput?.addEventListener("compositionstart", () => {