diff --git a/src/main/java/com/pandoli365/bibimbap/controller/WebMvcController.java b/src/main/java/com/pandoli365/bibimbap/controller/WebMvcController.java index 848fcff..0b5c430 100644 --- a/src/main/java/com/pandoli365/bibimbap/controller/WebMvcController.java +++ b/src/main/java/com/pandoli365/bibimbap/controller/WebMvcController.java @@ -82,6 +82,7 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController { if (!isLoggedIn(session)) { return new ModelAndView("redirect:/login"); } + mv.addObject("myGames", gamesMapper.getGamesByUserId(sessionUserId(session))); mv.setViewName("profile"); break; case "signup": @@ -114,6 +115,17 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController { return session != null && session.getAttribute("userId") != null; } + private long sessionUserId(HttpSession session) { + Object userId = session.getAttribute("userId"); + if (userId instanceof Number) { + return ((Number) userId).longValue(); + } + if (userId instanceof String) { + return Long.parseLong((String) userId); + } + throw new IllegalStateException("로그인 세션에 사용자 ID가 없습니다."); + } + /// 접속기기 모바일 확인 함수 private boolean isMobileDevice(HttpServletRequest request) { String userAgent = request.getHeader("User-Agent"); diff --git a/src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java b/src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java index 04a1d00..016fbee 100644 --- a/src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java +++ b/src/main/java/com/pandoli365/bibimbap/mapper/GamesMapper.java @@ -90,6 +90,30 @@ public interface GamesMapper { """) List searchVisibleGames(@Param("query") String query); + @Select(""" + SELECT + g.id, + g.user_id AS userId, + g.name, + u.display_name AS creator, + g.creator_note AS creatorNote, + g.git_url AS gitUrl, + g.webgl_path AS webglPath, + g.thumbnail_url AS thumbnailUrl, + g.like_count AS likeCount, + g.is_visible AS visible, + g.sort_order AS sortOrder, + g.created_at AS createdAt, + g.updated_at AS updatedAt + FROM games g + JOIN users u ON u.id = g.user_id + WHERE g.user_id = #{userId} + AND g.is_delete IS NOT TRUE + AND u.is_delete IS NOT TRUE + ORDER BY g.updated_at DESC, g.created_at DESC, g.id DESC + """) + List getGamesByUserId(@Param("userId") long userId); + @Insert(""" INSERT INTO games ( user_id, diff --git a/src/main/webapp/WEB-INF/jsp/fragments/header.jspf b/src/main/webapp/WEB-INF/jsp/fragments/header.jspf new file mode 100644 index 0000000..c4da0b9 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/header.jspf @@ -0,0 +1,24 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + diff --git a/src/main/webapp/WEB-INF/views/profile.jsp b/src/main/webapp/WEB-INF/views/profile.jsp index 67620f9..0847b24 100644 --- a/src/main/webapp/WEB-INF/views/profile.jsp +++ b/src/main/webapp/WEB-INF/views/profile.jsp @@ -1,5 +1,8 @@ <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" language="java" %> +<%@ page import="com.pandoli365.bibimbap.data.GameData" %> <%@ page import="org.springframework.web.util.HtmlUtils" %> +<%@ page import="java.util.Collections" %> +<%@ page import="java.util.List" %> <%@ page import="java.util.Locale" %> <% @@ -14,6 +17,8 @@ String email = HtmlUtils.htmlEscape(rawEmail.isBlank() ? "이메일 정보 없음" : rawEmail); String avatarUrl = HtmlUtils.htmlEscape(rawAvatarUrl); String avatarInitial = HtmlUtils.htmlEscape(initial); + Object rawMyGames = request.getAttribute("myGames"); + List myGames = rawMyGames instanceof List ? (List) rawMyGames : Collections.emptyList(); %> @@ -224,6 +229,129 @@ .profile-actions form { margin: 0; } + .profile-games { + margin-top: 1rem; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--card-bg); + box-shadow: 0 2px 8px var(--card-shadow); + overflow: hidden; + } + .profile-games__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--border); + } + .profile-games__title { + margin: 0; + font-size: 1.05rem; + line-height: 1.25; + letter-spacing: 0; + } + .profile-games__count { + color: var(--text-muted); + font-size: 0.8125rem; + font-weight: 800; + } + .profile-games__empty { + margin: 0; + padding: 1.25rem 1.5rem; + color: var(--text-muted); + font-size: 0.9375rem; + } + .profile-game-list { + display: grid; + } + .profile-game { + display: grid; + grid-template-columns: 3.75rem minmax(0, 1fr) auto; + gap: 0.875rem; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); + } + .profile-game:last-child { + border-bottom: 0; + } + .profile-game__thumb { + width: 3.75rem; + height: 3.75rem; + border-radius: 10px; + border: 1px solid var(--border); + background: rgba(232, 165, 75, 0.16); + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: center; + } + .profile-game__thumb img { + width: 100%; + height: 100%; + object-fit: cover; + } + .profile-game__fallback { + width: 1.85rem; + height: 1.85rem; + opacity: 0.75; + } + .profile-game__body { + min-width: 0; + } + .profile-game__name { + margin: 0; + color: var(--text); + font-size: 0.975rem; + font-weight: 900; + line-height: 1.3; + text-decoration: none; + word-break: break-word; + } + .profile-game__name:hover { + color: var(--accent); + } + .profile-game__meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.45rem; + color: var(--text-muted); + font-size: 0.8125rem; + font-weight: 700; + } + .profile-game__status { + border-radius: 999px; + padding: 0.15rem 0.5rem; + background: rgba(92, 92, 92, 0.12); + color: var(--text-muted); + } + .profile-game__status--visible { + background: rgba(232, 165, 75, 0.18); + color: var(--text); + } + .profile-game__actions { + display: flex; + gap: 0.5rem; + } + .profile-game__action { + min-height: 2.25rem; + padding: 0 0.75rem; + border: 1px solid var(--border); + border-radius: 9px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text); + font-size: 0.8125rem; + font-weight: 900; + text-decoration: none; + } + .profile-game__action:hover { + border-color: rgba(232, 165, 75, 0.45); + color: var(--accent); + } @media (max-width: 560px) { .profile-main { padding-top: 1.5rem; @@ -253,6 +381,21 @@ .profile-avatar-form .profile-button { flex: 1; } + .profile-games__head { + padding: 1.25rem; + } + .profile-game { + grid-template-columns: 3.25rem minmax(0, 1fr); + padding: 1rem 1.25rem; + } + .profile-game__thumb { + width: 3.25rem; + height: 3.25rem; + } + .profile-game__actions { + grid-column: 1 / -1; + justify-content: flex-end; + } } @@ -289,6 +432,54 @@ + +
+
+

내 게임

+ <%= myGames.size() %>개 +
+ <% if (myGames.isEmpty()) { %> +

등록한 게임이 없습니다.

+ <% } else { %> +
+ <% + for (GameData game : myGames) { + if (game == null || game.getId() == null) { + continue; + } + String gameName = HtmlUtils.htmlEscape(game.getName() == null || game.getName().isBlank() ? "제목 없음" : game.getName()); + String rawThumbUrl = game.getThumbnailUrl(); + boolean hasImage = rawThumbUrl != null && !rawThumbUrl.isBlank(); + String thumbUrl = ""; + if (hasImage) { + thumbUrl = rawThumbUrl.startsWith("/") ? ctx + rawThumbUrl : rawThumbUrl; + thumbUrl = HtmlUtils.htmlEscape(thumbUrl); + } + boolean visible = game.getVisible() == null || game.getVisible(); + %> +
+ +
+ <%= gameName %> +
+ "><%= visible ? "공개" : "비공개" %> + 좋아요 <%= String.format("%,d", game.getLikeCount() == null ? 0 : game.getLikeCount()) %> +
+
+
+ 수정 +
+
+ <% } %> +
+ <% } %> +