삭제시 delete가 아닌 기록이 남도록 변경
This commit is contained in:
parent
4e49fd21ff
commit
40c6c88e9d
|
|
@ -50,13 +50,14 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public ModelAndView indexView() {
|
||||
return indexModelAndView();
|
||||
public ModelAndView indexView(@RequestParam(name = "q", required = false) String query) {
|
||||
return indexModelAndView(query);
|
||||
}
|
||||
|
||||
@GetMapping("/{pageName}")
|
||||
public ModelAndView mainView(@PathVariable("pageName") String pageName,
|
||||
@RequestParam(name = "id", required = false) String id,
|
||||
@RequestParam(name = "q", required = false) String query,
|
||||
HttpSession session,
|
||||
HttpServletRequest request) {
|
||||
ModelAndView mv = new ModelAndView();
|
||||
|
|
@ -93,15 +94,19 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
mv.setViewName("operation-policy");
|
||||
break;
|
||||
default:
|
||||
return indexModelAndView();
|
||||
return indexModelAndView(query);
|
||||
}
|
||||
|
||||
return mv;
|
||||
}
|
||||
|
||||
private ModelAndView indexModelAndView() {
|
||||
private ModelAndView indexModelAndView(String query) {
|
||||
String normalizedQuery = query == null ? "" : query.trim();
|
||||
ModelAndView mv = new ModelAndView("index");
|
||||
mv.addObject("games", gamesMapper.getVisibleGames());
|
||||
mv.addObject("games", normalizedQuery.isBlank()
|
||||
? gamesMapper.getVisibleGames()
|
||||
: gamesMapper.searchVisibleGames(normalizedQuery));
|
||||
mv.addObject("searchQuery", normalizedQuery);
|
||||
return mv;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity;
|
|||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
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;
|
||||
|
|
@ -89,10 +90,10 @@ public class GameController {
|
|||
}
|
||||
|
||||
@GetMapping("/game/{id}")
|
||||
public String gameDetail(@PathVariable("id") long id, Model model) {
|
||||
public String gameDetail(@PathVariable("id") long id, Model model, HttpSession session) {
|
||||
GameData game = gamesMapper.getGame(id);
|
||||
if (game != null) {
|
||||
addGameModel(model, game);
|
||||
addGameModel(model, game, sessionUserId(session));
|
||||
return "game-detail";
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +112,125 @@ public class GameController {
|
|||
model.addAttribute("gitUrl", GameCatalog.GIT_URLS[idx]);
|
||||
model.addAttribute("webglUrl", webglUrlForGame(intId));
|
||||
model.addAttribute("webglDeployPath", webglUrlForGame(intId));
|
||||
model.addAttribute("owner", false);
|
||||
return "game-detail";
|
||||
}
|
||||
|
||||
private void addGameModel(Model model, GameData game) {
|
||||
@GetMapping("/game/{id}/edit")
|
||||
public String gameEditView(@PathVariable("id") long id, Model model, HttpSession session) {
|
||||
Long userId = sessionUserId(session);
|
||||
if (userId == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
GameData game = gamesMapper.getGame(id);
|
||||
if (game == null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
if (!userId.equals(game.getUserId())) {
|
||||
return "redirect:/game/" + id;
|
||||
}
|
||||
|
||||
model.addAttribute("editMode", true);
|
||||
model.addAttribute("editGameId", game.getId());
|
||||
model.addAttribute("editGameName", trimToEmpty(game.getName()));
|
||||
model.addAttribute("editCreatorNote", trimToEmpty(game.getCreatorNote()));
|
||||
model.addAttribute("editGitUrl", trimToEmpty(game.getGitUrl()));
|
||||
model.addAttribute("editWebglPath", trimToEmpty(game.getWebglPath()));
|
||||
model.addAttribute("editThumbnailUrl", trimToEmpty(game.getThumbnailUrl()));
|
||||
model.addAttribute("editVisible", game.getVisible() == null || game.getVisible());
|
||||
model.addAttribute("editGameUuid", gameUuidFromPath(trimToEmpty(game.getWebglPath())));
|
||||
return "game-register";
|
||||
}
|
||||
|
||||
@PostMapping("/game/{id}/edit")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> updateGame(
|
||||
@PathVariable("id") long id,
|
||||
@RequestParam(name = "name", required = false) String name,
|
||||
@RequestParam(name = "gitUrl", required = false) String gitUrl,
|
||||
@RequestParam(name = "creatorNote", required = false) String creatorNote,
|
||||
@RequestParam(name = "webglPath", required = false) String webglPath,
|
||||
@RequestParam(name = "thumbnailUrl", required = false) String thumbnailUrl,
|
||||
@RequestParam(name = "visible", required = false) String visible,
|
||||
HttpSession session
|
||||
) {
|
||||
Long userId = sessionUserId(session);
|
||||
if (userId == null) {
|
||||
return response(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
|
||||
}
|
||||
|
||||
GameData existing = gamesMapper.getGame(id);
|
||||
if (existing == null) {
|
||||
return response(HttpStatus.NOT_FOUND, "게임을 찾을 수 없습니다.");
|
||||
}
|
||||
if (!userId.equals(existing.getUserId())) {
|
||||
return response(HttpStatus.FORBIDDEN, "작성자만 수정할 수 있습니다.");
|
||||
}
|
||||
|
||||
String normalizedName = trimToNull(name);
|
||||
String normalizedWebglPath = trimToNull(webglPath);
|
||||
if (normalizedName == null || normalizedName.length() > 80) {
|
||||
return response(HttpStatus.BAD_REQUEST, "게임 이름을 확인해 주세요.");
|
||||
}
|
||||
if (!isGameAssetPath(normalizedWebglPath) || !normalizedWebglPath.endsWith("/index.html")) {
|
||||
return response(HttpStatus.BAD_REQUEST, "WebGL zip 업로드를 먼저 완료해 주세요.");
|
||||
}
|
||||
|
||||
String normalizedThumbnailUrl = trimToNull(thumbnailUrl);
|
||||
if (normalizedThumbnailUrl != null && !isGameAssetPath(normalizedThumbnailUrl)) {
|
||||
return response(HttpStatus.BAD_REQUEST, "썸네일 경로가 올바르지 않습니다.");
|
||||
}
|
||||
|
||||
String normalizedCreatorNote = trimToEmpty(creatorNote);
|
||||
if (normalizedCreatorNote.length() > 600) {
|
||||
return response(HttpStatus.BAD_REQUEST, "소개는 600자 이하로 입력해 주세요.");
|
||||
}
|
||||
|
||||
existing.setName(normalizedName);
|
||||
existing.setCreatorNote(normalizedCreatorNote);
|
||||
existing.setGitUrl(trimToEmpty(gitUrl));
|
||||
existing.setWebglPath(normalizedWebglPath);
|
||||
existing.setThumbnailUrl(normalizedThumbnailUrl == null ? "" : normalizedThumbnailUrl);
|
||||
existing.setVisible(isChecked(visible));
|
||||
gamesMapper.updateGame(existing);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("status", 200);
|
||||
body.put("message", "게임이 수정되었습니다.");
|
||||
body.put("gameId", existing.getId());
|
||||
body.put("location", "/game/" + existing.getId());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@DeleteMapping("/game/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Object>> deleteGame(@PathVariable("id") long id, HttpSession session) {
|
||||
Long userId = sessionUserId(session);
|
||||
if (userId == null) {
|
||||
return response(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
|
||||
}
|
||||
|
||||
GameData existing = gamesMapper.getGame(id);
|
||||
if (existing == null) {
|
||||
return response(HttpStatus.NOT_FOUND, "게임을 찾을 수 없습니다.");
|
||||
}
|
||||
if (!userId.equals(existing.getUserId())) {
|
||||
return response(HttpStatus.FORBIDDEN, "작성자만 삭제할 수 있습니다.");
|
||||
}
|
||||
|
||||
gamesMapper.softDeleteGameComments(id);
|
||||
gamesMapper.deleteGameLikes(id);
|
||||
gamesMapper.softDeleteGame(id);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("status", 200);
|
||||
body.put("message", "게임이 삭제되었습니다.");
|
||||
body.put("location", "/");
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
private void addGameModel(Model model, GameData game, Long currentUserId) {
|
||||
int likeCount = game.getLikeCount() == null ? 0 : game.getLikeCount();
|
||||
String webglPath = trimToEmpty(game.getWebglPath());
|
||||
model.addAttribute("gameId", game.getId());
|
||||
|
|
@ -126,6 +242,7 @@ public class GameController {
|
|||
model.addAttribute("gitUrl", trimToEmpty(game.getGitUrl()));
|
||||
model.addAttribute("webglUrl", webglPath);
|
||||
model.addAttribute("webglDeployPath", webglPath);
|
||||
model.addAttribute("owner", currentUserId != null && currentUserId.equals(game.getUserId()));
|
||||
}
|
||||
|
||||
private Long sessionUserId(HttpSession session) {
|
||||
|
|
@ -167,6 +284,14 @@ public class GameController {
|
|||
return text == null ? "" : text;
|
||||
}
|
||||
|
||||
private String gameUuidFromPath(String webglPath) {
|
||||
if (!isGameAssetPath(webglPath)) {
|
||||
return "";
|
||||
}
|
||||
String[] parts = webglPath.split("/");
|
||||
return parts.length > 2 ? parts[2] : "";
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> response(HttpStatus status, String message) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("status", status.value());
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public interface GameCommentsMapper {
|
|||
deleted_at AS deletedAt
|
||||
FROM game_comments
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
GameCommentData getGameComment(long id);
|
||||
|
||||
|
|
@ -42,8 +43,10 @@ public interface GameCommentsMapper {
|
|||
SET
|
||||
nickname = #{nickname},
|
||||
content = #{content},
|
||||
deleted_at = #{deletedAt}
|
||||
deleted_at = #{deletedAt},
|
||||
is_delete = CASE WHEN #{deletedAt} IS NULL THEN false ELSE true END
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int updateGameComment(GameCommentData gameComment);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.pandoli365.bibimbap.mapper;
|
||||
|
||||
import com.pandoli365.bibimbap.data.GameData;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
|
|
@ -30,6 +32,8 @@ public interface GamesMapper {
|
|||
FROM games g
|
||||
JOIN users u ON u.id = g.user_id
|
||||
WHERE g.id = #{id}
|
||||
AND g.is_delete IS NOT TRUE
|
||||
AND u.is_delete IS NOT TRUE
|
||||
""")
|
||||
GameData getGame(long id);
|
||||
|
||||
|
|
@ -51,10 +55,41 @@ public interface GamesMapper {
|
|||
FROM games g
|
||||
JOIN users u ON u.id = g.user_id
|
||||
WHERE g.is_visible IS NOT FALSE
|
||||
AND g.is_delete IS NOT TRUE
|
||||
AND u.is_delete IS NOT TRUE
|
||||
ORDER BY g.sort_order ASC, g.created_at DESC, g.id DESC
|
||||
""")
|
||||
List<GameData> getVisibleGames();
|
||||
|
||||
@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.is_visible IS NOT FALSE
|
||||
AND g.is_delete IS NOT TRUE
|
||||
AND u.is_delete IS NOT TRUE
|
||||
AND (
|
||||
g.name ILIKE CONCAT('%', #{query}, '%')
|
||||
OR u.display_name ILIKE CONCAT('%', #{query}, '%')
|
||||
OR g.creator_note ILIKE CONCAT('%', #{query}, '%')
|
||||
)
|
||||
ORDER BY g.sort_order ASC, g.created_at DESC, g.id DESC
|
||||
""")
|
||||
List<GameData> searchVisibleGames(@Param("query") String query);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO games (
|
||||
user_id,
|
||||
|
|
@ -82,6 +117,7 @@ public interface GamesMapper {
|
|||
@Select("""
|
||||
SELECT COALESCE(MAX(sort_order), 0) + 1
|
||||
FROM games
|
||||
WHERE is_delete IS NOT TRUE
|
||||
""")
|
||||
int nextSortOrder();
|
||||
|
||||
|
|
@ -95,8 +131,36 @@ public interface GamesMapper {
|
|||
webgl_path = #{webglPath},
|
||||
thumbnail_url = #{thumbnailUrl},
|
||||
is_visible = #{visible},
|
||||
sort_order = #{sortOrder}
|
||||
sort_order = #{sortOrder},
|
||||
updated_at = now()
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int updateGame(GameData game);
|
||||
|
||||
@Update("""
|
||||
UPDATE game_comments
|
||||
SET
|
||||
is_delete = true,
|
||||
deleted_at = COALESCE(deleted_at, now())
|
||||
WHERE game_id = #{gameId}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int softDeleteGameComments(@Param("gameId") long gameId);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM game_likes
|
||||
WHERE game_id = #{gameId}
|
||||
""")
|
||||
int deleteGameLikes(@Param("gameId") long gameId);
|
||||
|
||||
@Update("""
|
||||
UPDATE games
|
||||
SET
|
||||
is_delete = true,
|
||||
updated_at = now()
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int softDeleteGame(@Param("id") long id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ public interface UserAuthIdentitiesMapper {
|
|||
updated_at AS updatedAt
|
||||
FROM user_auth_identities
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
UserAuthIdentityData getUserAuthIdentity(long id);
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ public interface UserAuthIdentitiesMapper {
|
|||
FROM user_auth_identities
|
||||
WHERE provider = #{provider}
|
||||
AND provider_user_id = #{providerUserId}
|
||||
AND is_delete = false
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
UserAuthIdentityData getUserAuthIdentityByProvider(
|
||||
@Param("provider") String provider,
|
||||
|
|
@ -86,6 +87,7 @@ public interface UserAuthIdentitiesMapper {
|
|||
last_login_at = #{lastLoginAt},
|
||||
updated_at = now()
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int updateUserAuthIdentity(UserAuthIdentityData userAuthIdentity);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public interface UsersMapper {
|
|||
updated_at AS updatedAt
|
||||
FROM users
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
UserData getUser(long id);
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ public interface UsersMapper {
|
|||
last_login_at = #{lastLoginAt},
|
||||
updated_at = now()
|
||||
WHERE id = #{id}
|
||||
AND is_delete IS NOT TRUE
|
||||
""")
|
||||
int updateUser(UserData user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ server.tomcat.max-http-form-post-size=1GB
|
|||
server.tomcat.max-swallow-size=-1
|
||||
|
||||
# common
|
||||
spring.config.import=classpath:${spring.profiles.active}/db.properties
|
||||
spring.config.import=optional:classpath:${spring.profiles.active}/db.properties,optional:file:src/main/resources/${spring.profiles.active}/db.properties
|
||||
|
||||
# log
|
||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%
|
||||
boolean owner = Boolean.TRUE.equals(request.getAttribute("owner"));
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
|
|
@ -79,6 +82,55 @@
|
|||
box-shadow: 0 4px 16px rgba(232, 165, 75, 0.12);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
.game-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.game-topbar .game-back {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.game-owner-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.game-owner-button {
|
||||
min-height: 2.25rem;
|
||||
padding: 0 0.8rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--card-bg);
|
||||
color: var(--text);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.game-owner-button:hover {
|
||||
border-color: rgba(232, 165, 75, 0.45);
|
||||
color: var(--accent);
|
||||
}
|
||||
.game-owner-button--danger:hover {
|
||||
border-color: rgba(180, 50, 50, 0.32);
|
||||
color: #b33;
|
||||
background: rgba(180, 50, 50, 0.06);
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.game-topbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
.game-owner-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.game-info-card {
|
||||
margin-bottom: 1.75rem;
|
||||
|
|
@ -312,24 +364,6 @@
|
|||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
.game-webgl__hint {
|
||||
margin: 0.85rem 0 0;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-muted);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.game-webgl__hint code {
|
||||
font-size: 0.68rem;
|
||||
word-break: break-all;
|
||||
color: var(--accent);
|
||||
padding: 0.1em 0.35em;
|
||||
background: var(--surface);
|
||||
border-radius: 4px;
|
||||
}
|
||||
/* 제작자 한마디 ~ 덧글: 공통 패널 */
|
||||
.game-community {
|
||||
margin-top: 2rem;
|
||||
|
|
@ -634,10 +668,18 @@
|
|||
<body>
|
||||
<jsp:include page="/WEB-INF/views/header.jsp"/>
|
||||
<main class="game-page">
|
||||
<div class="game-topbar">
|
||||
<a class="game-back" href="${pageContext.request.contextPath}/">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||
목록으로
|
||||
</a>
|
||||
<% if (owner) { %>
|
||||
<div class="game-owner-actions" aria-label="게시물 관리">
|
||||
<a class="game-owner-button" href="${pageContext.request.contextPath}/game/${gameId}/edit">수정</a>
|
||||
<button type="button" class="game-owner-button game-owner-button--danger" id="game-delete-button">삭제</button>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<section class="game-info-card" aria-labelledby="game-title">
|
||||
<h1 id="game-title">${gameName}</h1>
|
||||
|
|
@ -694,8 +736,6 @@
|
|||
referrerpolicy="same-origin"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<p class="game-webgl__hint">실제 Unity WebGL 빌드는 리소스 폴더에 두세요. 예: <code>static${webglDeployPath}</code><br>
|
||||
준비되면 <code>GameController</code>에서 <code>webglUrl</code>을 해당 경로(<code>webglUrlForGame(id)</code>)로 바꾸면 됩니다.</p>
|
||||
</section>
|
||||
|
||||
<div class="game-community">
|
||||
|
|
@ -747,6 +787,7 @@
|
|||
|
||||
<script>
|
||||
(function () {
|
||||
var ctx = '${pageContext.request.contextPath}';
|
||||
var gameId = ${gameId};
|
||||
var baseLikes = ${likeCount};
|
||||
var LIKE_KEY = 'bibimbap-game-liked';
|
||||
|
|
@ -782,6 +823,54 @@
|
|||
var likeBtn = document.getElementById('game-like-btn');
|
||||
var likeCountEl = document.getElementById('game-like-count');
|
||||
var gid = String(gameId);
|
||||
var deleteBtn = document.getElementById('game-delete-button');
|
||||
|
||||
function deleteGame() {
|
||||
fetch(ctx + '/game/' + encodeURIComponent(gid), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(function (res) {
|
||||
return res.json().catch(function () {
|
||||
return { message: '게시물을 삭제하지 못했습니다.' };
|
||||
}).then(function (data) {
|
||||
if (!res.ok) {
|
||||
throw new Error(data && data.message ? data.message : '게시물을 삭제하지 못했습니다.');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}).then(function () {
|
||||
window.location.href = ctx + '/';
|
||||
}).catch(function (err) {
|
||||
if (window.BibimbapModal && typeof window.BibimbapModal.alert === 'function') {
|
||||
window.BibimbapModal.alert({
|
||||
title: '삭제 실패',
|
||||
message: err.message || '게시물을 삭제하지 못했습니다.',
|
||||
confirmText: '확인'
|
||||
});
|
||||
} else {
|
||||
alert(err.message || '게시물을 삭제하지 못했습니다.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', function () {
|
||||
if (window.BibimbapModal && typeof window.BibimbapModal.confirm === 'function') {
|
||||
window.BibimbapModal.confirm({
|
||||
title: '게시물 삭제',
|
||||
message: '삭제하시겠습니까? 삭제 후 복구가 불가능합니다.',
|
||||
confirmText: '삭제',
|
||||
cancelText: '취소',
|
||||
onConfirm: deleteGame
|
||||
});
|
||||
} else if (confirm('게시물을 삭제할까요?')) {
|
||||
deleteGame();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function syncLike() {
|
||||
var liked = isLiked(gid);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,25 @@
|
|||
String ctx = request.getContextPath();
|
||||
String rawDisplayName = session.getAttribute("displayName") == null ? "" : String.valueOf(session.getAttribute("displayName"));
|
||||
String creatorValue = HtmlUtils.htmlEscape(rawDisplayName);
|
||||
boolean editMode = Boolean.TRUE.equals(request.getAttribute("editMode"));
|
||||
String editGameId = request.getAttribute("editGameId") == null ? "" : String.valueOf(request.getAttribute("editGameId"));
|
||||
String formAction = ctx + (editMode ? "/game/" + editGameId + "/edit" : "/game/new");
|
||||
String pageTitle = editMode ? "게임 수정" : "신규 게임 게시";
|
||||
String submitText = editMode ? "수정" : "등록";
|
||||
String editGameName = HtmlUtils.htmlEscape(request.getAttribute("editGameName") == null ? "" : String.valueOf(request.getAttribute("editGameName")));
|
||||
String editCreatorNote = HtmlUtils.htmlEscape(request.getAttribute("editCreatorNote") == null ? "" : String.valueOf(request.getAttribute("editCreatorNote")));
|
||||
String editGitUrl = HtmlUtils.htmlEscape(request.getAttribute("editGitUrl") == null ? "" : String.valueOf(request.getAttribute("editGitUrl")));
|
||||
String editWebglPath = HtmlUtils.htmlEscape(request.getAttribute("editWebglPath") == null ? "" : String.valueOf(request.getAttribute("editWebglPath")));
|
||||
String editThumbnailUrl = HtmlUtils.htmlEscape(request.getAttribute("editThumbnailUrl") == null ? "" : String.valueOf(request.getAttribute("editThumbnailUrl")));
|
||||
String editGameUuid = HtmlUtils.htmlEscape(request.getAttribute("editGameUuid") == null ? "" : String.valueOf(request.getAttribute("editGameUuid")));
|
||||
boolean editVisible = !editMode || Boolean.TRUE.equals(request.getAttribute("editVisible"));
|
||||
%>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
|
||||
<title>신규 게임 개시 | bibimbap</title>
|
||||
<title><%= pageTitle %> | bibimbap</title>
|
||||
<style>
|
||||
html {
|
||||
color-scheme: light;
|
||||
|
|
@ -342,59 +354,59 @@
|
|||
<main class="game-register-main">
|
||||
<section class="game-register-heading" aria-labelledby="game-register-title">
|
||||
<p class="game-register-heading__eyebrow">BIBIMBAP GAME</p>
|
||||
<h1 class="game-register-heading__title" id="game-register-title">신규 게임 개시</h1>
|
||||
<h1 class="game-register-heading__title" id="game-register-title"><%= pageTitle %></h1>
|
||||
</section>
|
||||
|
||||
<div class="game-register-layout">
|
||||
<section class="game-register-panel" aria-label="게임 등록 양식">
|
||||
<form class="game-form" action="<%= ctx %>/game/new" method="post" id="game-register-form">
|
||||
<form class="game-form" action="<%= formAction %>" method="post" id="game-register-form" data-edit-mode="<%= editMode %>">
|
||||
<div class="game-form__grid">
|
||||
<div class="game-field game-field--full">
|
||||
<label class="game-field__label" for="game-name">게임 이름</label>
|
||||
<input class="game-field__input" type="text" id="game-name" name="name" maxlength="80" autocomplete="off" required />
|
||||
<input class="game-field__input" type="text" id="game-name" name="name" maxlength="80" autocomplete="off" value="<%= editGameName %>" required />
|
||||
</div>
|
||||
<div class="game-field">
|
||||
<label class="game-field__label" for="game-git-url">소스 코드 URL</label>
|
||||
<input class="game-field__input" type="url" id="game-git-url" name="gitUrl" placeholder="https://example.com/repository" autocomplete="url" />
|
||||
<input class="game-field__input" type="url" id="game-git-url" name="gitUrl" placeholder="https://example.com/repository" autocomplete="url" value="<%= editGitUrl %>" />
|
||||
</div>
|
||||
<div class="game-field">
|
||||
<label class="game-field__label" for="game-webgl-zip">WebGL zip</label>
|
||||
<label class="game-file" for="game-webgl-zip">
|
||||
<span class="game-file__action">zip 선택</span>
|
||||
<span class="game-file__name" id="game-webgl-file-name">선택된 파일 없음</span>
|
||||
<span class="game-file__name" id="game-webgl-file-name"><%= editMode && !editWebglPath.isBlank() ? "기존 WebGL 유지" : "선택된 파일 없음" %></span>
|
||||
</label>
|
||||
<input class="game-file__input" type="file" id="game-webgl-zip" name="webglZip" accept=".zip,application/zip,application/x-zip-compressed" required />
|
||||
<input class="game-file__input" type="file" id="game-webgl-zip" name="webglZip" accept=".zip,application/zip,application/x-zip-compressed" <%= editMode ? "" : "required" %> />
|
||||
</div>
|
||||
<div class="game-field">
|
||||
<label class="game-field__label" for="game-thumbnail-image">썸네일 이미지</label>
|
||||
<label class="game-file" for="game-thumbnail-image">
|
||||
<span class="game-file__action">이미지 선택</span>
|
||||
<span class="game-file__name" id="game-thumbnail-file-name">선택된 파일 없음</span>
|
||||
<span class="game-file__name" id="game-thumbnail-file-name"><%= editMode && !editThumbnailUrl.isBlank() ? "기존 썸네일 유지" : "선택된 파일 없음" %></span>
|
||||
</label>
|
||||
<input class="game-file__input" type="file" id="game-thumbnail-image" name="thumbnailImage" accept="image/png,image/jpeg,image/webp,image/gif" />
|
||||
</div>
|
||||
<div class="game-field game-field--full">
|
||||
<label class="game-field__label" for="game-creator-note">소개</label>
|
||||
<textarea class="game-field__textarea" id="game-creator-note" name="creatorNote" maxlength="600"></textarea>
|
||||
<textarea class="game-field__textarea" id="game-creator-note" name="creatorNote" maxlength="600"><%= editCreatorNote %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="game-uploaded-game-uuid" name="gameUuid" />
|
||||
<input type="hidden" id="game-uploaded-webgl-path" name="webglPath" />
|
||||
<input type="hidden" id="game-uploaded-thumbnail-url" name="thumbnailUrl" />
|
||||
<input type="hidden" id="game-uploaded-game-uuid" name="gameUuid" value="<%= editGameUuid %>" />
|
||||
<input type="hidden" id="game-uploaded-webgl-path" name="webglPath" value="<%= editWebglPath %>" />
|
||||
<input type="hidden" id="game-uploaded-thumbnail-url" name="thumbnailUrl" value="<%= editThumbnailUrl %>" />
|
||||
|
||||
<label class="game-check">
|
||||
<input type="checkbox" name="visible" value="true" checked />
|
||||
<input type="checkbox" name="visible" value="true" <%= editVisible ? "checked" : "" %> />
|
||||
<span>목록에 공개</span>
|
||||
</label>
|
||||
|
||||
<div class="game-form__actions">
|
||||
<a class="game-button" href="<%= ctx %>/">취소</a>
|
||||
<a class="game-button" href="<%= editMode ? ctx + "/game/" + editGameId : ctx + "/" %>">취소</a>
|
||||
<button class="game-button game-button--primary" type="submit" id="game-register-submit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 5v14"/>
|
||||
<path d="M5 12h14"/>
|
||||
</svg>
|
||||
<span id="game-register-submit-label">등록</span>
|
||||
<span id="game-register-submit-label"><%= submitText %></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -402,13 +414,17 @@
|
|||
|
||||
<aside class="game-preview" aria-label="미리보기">
|
||||
<div class="game-preview__media">
|
||||
<% if (editThumbnailUrl.isBlank()) { %>
|
||||
<img id="preview-thumb" alt="" hidden />
|
||||
<img class="game-preview__fallback" id="preview-fallback" src="<%= ctx %>/images/logo.png" alt="" width="120" height="120" />
|
||||
<% } else { %>
|
||||
<img id="preview-thumb" src="<%= ctx + editThumbnailUrl %>" alt="" />
|
||||
<% } %>
|
||||
<img class="game-preview__fallback" id="preview-fallback" src="<%= ctx %>/images/logo.png" alt="" width="120" height="120" <%= editThumbnailUrl.isBlank() ? "" : "hidden" %> />
|
||||
</div>
|
||||
<div class="game-preview__body">
|
||||
<h2 class="game-preview__name" id="preview-name">게임 이름</h2>
|
||||
<h2 class="game-preview__name" id="preview-name"><%= editGameName.isBlank() ? "게임 이름" : editGameName %></h2>
|
||||
<p class="game-preview__creator" id="preview-creator"><%= creatorValue.isBlank() ? "제작자" : creatorValue %></p>
|
||||
<p class="game-preview__note" id="preview-note">소개</p>
|
||||
<p class="game-preview__note" id="preview-note"><%= editCreatorNote.isBlank() ? "소개" : editCreatorNote %></p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
|
@ -418,6 +434,7 @@
|
|||
(function () {
|
||||
var ctx = '<%= ctx %>';
|
||||
var form = document.getElementById('game-register-form');
|
||||
var editMode = form && form.getAttribute('data-edit-mode') === 'true';
|
||||
var nameInput = document.getElementById('game-name');
|
||||
var gitUrlInput = document.getElementById('game-git-url');
|
||||
var noteInput = document.getElementById('game-creator-note');
|
||||
|
|
@ -489,7 +506,7 @@
|
|||
if (!submitBtn) return;
|
||||
submitBtn.disabled = submitting;
|
||||
if (submitLabel) {
|
||||
submitLabel.textContent = submitting ? '등록 중...' : submitText;
|
||||
submitLabel.textContent = submitting ? (editMode ? '수정 중...' : '등록 중...') : submitText;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +541,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
function registerGame() {
|
||||
function saveGame() {
|
||||
var body = new URLSearchParams();
|
||||
body.set('name', valueOr(nameInput, ''));
|
||||
body.set('gitUrl', valueOr(gitUrlInput, ''));
|
||||
|
|
@ -612,13 +629,16 @@
|
|||
|
||||
var webglFile = webglInput && webglInput.files ? webglInput.files[0] : null;
|
||||
var thumbnailFile = thumbnailInput && thumbnailInput.files ? thumbnailInput.files[0] : null;
|
||||
if (!webglFile) {
|
||||
if (!editMode && !webglFile) {
|
||||
openModal('업로드 실패', 'WebGL zip 파일을 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
uploadFile(ctx + '/api/game-files/webgl-zip', webglFile).then(function (webglResult) {
|
||||
(webglFile ? uploadFile(ctx + '/api/game-files/webgl-zip', webglFile) : Promise.resolve({
|
||||
gameUuid: uploadedGameUuid ? uploadedGameUuid.value : '',
|
||||
webglPath: uploadedWebglPath ? uploadedWebglPath.value : ''
|
||||
})).then(function (webglResult) {
|
||||
if (uploadedGameUuid && webglResult && webglResult.gameUuid) {
|
||||
uploadedGameUuid.value = webglResult.gameUuid;
|
||||
}
|
||||
|
|
@ -644,9 +664,9 @@
|
|||
if (uploadedThumbnailUrl && thumbnailResult && thumbnailResult.thumbnailUrl) {
|
||||
uploadedThumbnailUrl.value = thumbnailResult.thumbnailUrl;
|
||||
}
|
||||
return registerGame();
|
||||
return saveGame();
|
||||
}).then(function (registerResult) {
|
||||
openModal('게임 등록 완료', '게임 등록이 완료되었습니다.', '확인', function () {
|
||||
openModal(editMode ? '게임 수정 완료' : '게임 등록 완료', editMode ? '게임이 수정되었습니다.' : '게임 등록이 완료되었습니다.', '확인', function () {
|
||||
if (registerResult && registerResult.gameId) {
|
||||
window.location.href = ctx + '/game/' + registerResult.gameId;
|
||||
} else if (registerResult && registerResult.location) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@
|
|||
if (gamesAttr instanceof List<?>) {
|
||||
games = (List<GameData>) gamesAttr;
|
||||
}
|
||||
String searchQuery = "";
|
||||
Object searchQueryAttr = request.getAttribute("searchQuery");
|
||||
if (searchQueryAttr instanceof String) {
|
||||
searchQuery = ((String) searchQueryAttr).trim();
|
||||
}
|
||||
boolean searching = !searchQuery.isBlank();
|
||||
String escapedSearchQuery = HtmlUtils.htmlEscape(searchQuery);
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
|
@ -453,10 +460,10 @@
|
|||
<main class="page-main">
|
||||
<section class="search-section" aria-label="게임·제작자 검색">
|
||||
<div class="home-toolbar">
|
||||
<form class="search-form" role="search" action="#" method="get">
|
||||
<form class="search-form" role="search" action="<%= ctx %>/" method="get">
|
||||
<div class="search-form__field">
|
||||
<label class="search-form__label" for="q">게임·제작자 검색</label>
|
||||
<input class="search-form__input" type="search" id="q" name="q" placeholder="게임·제작자 검색" autocomplete="off" enterkeyhint="search" />
|
||||
<input class="search-form__input" type="search" id="q" name="q" value="<%= escapedSearchQuery %>" placeholder="게임·제작자 검색" autocomplete="off" enterkeyhint="search" />
|
||||
</div>
|
||||
<button class="search-form__submit" type="submit">검색</button>
|
||||
</form>
|
||||
|
|
@ -483,8 +490,12 @@
|
|||
|
||||
<section class="card-grid" aria-label="추천 목록">
|
||||
<% if (games.isEmpty()) { %>
|
||||
<% if (searching) { %>
|
||||
<div class="home-empty">"<%= escapedSearchQuery %>"에 대한 검색 결과가 없습니다.</div>
|
||||
<% } else { %>
|
||||
<div class="home-empty">아직 공개된 게임이 없습니다.<br>로그인 후 첫 게임을 등록해 주세요.</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<%
|
||||
for (int i = 0; i < games.size(); i++) {
|
||||
GameData game = games.get(i);
|
||||
|
|
@ -605,7 +616,7 @@
|
|||
textBtn.title = term;
|
||||
textBtn.addEventListener('click', function () {
|
||||
input.value = term;
|
||||
input.focus();
|
||||
form.submit();
|
||||
});
|
||||
|
||||
var removeBtn = document.createElement('button');
|
||||
|
|
@ -627,17 +638,18 @@
|
|||
|
||||
if (form && input) {
|
||||
form.addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
var q = input.value;
|
||||
var q = input.value.trim();
|
||||
input.value = q;
|
||||
addSearchQuery(q);
|
||||
renderChips();
|
||||
/* 실제 검색 URL 연동 시: location.href = '...?q=' + encodeURIComponent(q.trim()); */
|
||||
});
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', function () {
|
||||
clearHistory();
|
||||
renderChips();
|
||||
});
|
||||
}
|
||||
|
||||
renderChips();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,6 +189,24 @@
|
|||
document.addEventListener('keydown', onKeyDown);
|
||||
confirmBtn.focus();
|
||||
},
|
||||
confirm: function (options) {
|
||||
var opts = options || {};
|
||||
lastFocused = document.activeElement;
|
||||
titleEl.textContent = opts.title || '확인';
|
||||
messageEl.textContent = opts.message || '';
|
||||
fieldEl.hidden = true;
|
||||
inputEl.value = '';
|
||||
inputEl.removeAttribute('maxlength');
|
||||
inputEl.removeAttribute('placeholder');
|
||||
cancelBtn.hidden = false;
|
||||
cancelBtn.textContent = opts.cancelText || '취소';
|
||||
confirmBtn.textContent = opts.confirmText || '확인';
|
||||
onConfirm = opts.onConfirm || null;
|
||||
onCancel = opts.onCancel || null;
|
||||
root.hidden = false;
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
confirmBtn.focus();
|
||||
},
|
||||
prompt: function (options) {
|
||||
var opts = options || {};
|
||||
lastFocused = document.activeElement;
|
||||
|
|
|
|||
Loading…
Reference in New Issue