삭제시 delete가 아닌 기록이 남도록 변경
This commit is contained in:
parent
4e49fd21ff
commit
40c6c88e9d
|
|
@ -50,13 +50,14 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public ModelAndView indexView() {
|
public ModelAndView indexView(@RequestParam(name = "q", required = false) String query) {
|
||||||
return indexModelAndView();
|
return indexModelAndView(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{pageName}")
|
@GetMapping("/{pageName}")
|
||||||
public ModelAndView mainView(@PathVariable("pageName") String pageName,
|
public ModelAndView mainView(@PathVariable("pageName") String pageName,
|
||||||
@RequestParam(name = "id", required = false) String id,
|
@RequestParam(name = "id", required = false) String id,
|
||||||
|
@RequestParam(name = "q", required = false) String query,
|
||||||
HttpSession session,
|
HttpSession session,
|
||||||
HttpServletRequest request) {
|
HttpServletRequest request) {
|
||||||
ModelAndView mv = new ModelAndView();
|
ModelAndView mv = new ModelAndView();
|
||||||
|
|
@ -93,15 +94,19 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
||||||
mv.setViewName("operation-policy");
|
mv.setViewName("operation-policy");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return indexModelAndView();
|
return indexModelAndView(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mv;
|
return mv;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ModelAndView indexModelAndView() {
|
private ModelAndView indexModelAndView(String query) {
|
||||||
|
String normalizedQuery = query == null ? "" : query.trim();
|
||||||
ModelAndView mv = new ModelAndView("index");
|
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;
|
return mv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.ui.Model;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
@ -89,10 +90,10 @@ public class GameController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/game/{id}")
|
@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);
|
GameData game = gamesMapper.getGame(id);
|
||||||
if (game != null) {
|
if (game != null) {
|
||||||
addGameModel(model, game);
|
addGameModel(model, game, sessionUserId(session));
|
||||||
return "game-detail";
|
return "game-detail";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,10 +112,125 @@ public class GameController {
|
||||||
model.addAttribute("gitUrl", GameCatalog.GIT_URLS[idx]);
|
model.addAttribute("gitUrl", GameCatalog.GIT_URLS[idx]);
|
||||||
model.addAttribute("webglUrl", webglUrlForGame(intId));
|
model.addAttribute("webglUrl", webglUrlForGame(intId));
|
||||||
model.addAttribute("webglDeployPath", webglUrlForGame(intId));
|
model.addAttribute("webglDeployPath", webglUrlForGame(intId));
|
||||||
|
model.addAttribute("owner", false);
|
||||||
return "game-detail";
|
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();
|
int likeCount = game.getLikeCount() == null ? 0 : game.getLikeCount();
|
||||||
String webglPath = trimToEmpty(game.getWebglPath());
|
String webglPath = trimToEmpty(game.getWebglPath());
|
||||||
model.addAttribute("gameId", game.getId());
|
model.addAttribute("gameId", game.getId());
|
||||||
|
|
@ -126,6 +242,7 @@ public class GameController {
|
||||||
model.addAttribute("gitUrl", trimToEmpty(game.getGitUrl()));
|
model.addAttribute("gitUrl", trimToEmpty(game.getGitUrl()));
|
||||||
model.addAttribute("webglUrl", webglPath);
|
model.addAttribute("webglUrl", webglPath);
|
||||||
model.addAttribute("webglDeployPath", webglPath);
|
model.addAttribute("webglDeployPath", webglPath);
|
||||||
|
model.addAttribute("owner", currentUserId != null && currentUserId.equals(game.getUserId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long sessionUserId(HttpSession session) {
|
private Long sessionUserId(HttpSession session) {
|
||||||
|
|
@ -167,6 +284,14 @@ public class GameController {
|
||||||
return text == null ? "" : text;
|
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) {
|
private ResponseEntity<Map<String, Object>> response(HttpStatus status, String message) {
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
body.put("status", status.value());
|
body.put("status", status.value());
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ public interface GameCommentsMapper {
|
||||||
deleted_at AS deletedAt
|
deleted_at AS deletedAt
|
||||||
FROM game_comments
|
FROM game_comments
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
GameCommentData getGameComment(long id);
|
GameCommentData getGameComment(long id);
|
||||||
|
|
||||||
|
|
@ -42,8 +43,10 @@ public interface GameCommentsMapper {
|
||||||
SET
|
SET
|
||||||
nickname = #{nickname},
|
nickname = #{nickname},
|
||||||
content = #{content},
|
content = #{content},
|
||||||
deleted_at = #{deletedAt}
|
deleted_at = #{deletedAt},
|
||||||
|
is_delete = CASE WHEN #{deletedAt} IS NULL THEN false ELSE true END
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
int updateGameComment(GameCommentData gameComment);
|
int updateGameComment(GameCommentData gameComment);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.pandoli365.bibimbap.mapper;
|
package com.pandoli365.bibimbap.mapper;
|
||||||
|
|
||||||
import com.pandoli365.bibimbap.data.GameData;
|
import com.pandoli365.bibimbap.data.GameData;
|
||||||
|
import org.apache.ibatis.annotations.Delete;
|
||||||
import org.apache.ibatis.annotations.Insert;
|
import org.apache.ibatis.annotations.Insert;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Options;
|
import org.apache.ibatis.annotations.Options;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
|
@ -30,6 +32,8 @@ public interface GamesMapper {
|
||||||
FROM games g
|
FROM games g
|
||||||
JOIN users u ON u.id = g.user_id
|
JOIN users u ON u.id = g.user_id
|
||||||
WHERE g.id = #{id}
|
WHERE g.id = #{id}
|
||||||
|
AND g.is_delete IS NOT TRUE
|
||||||
|
AND u.is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
GameData getGame(long id);
|
GameData getGame(long id);
|
||||||
|
|
||||||
|
|
@ -51,10 +55,41 @@ public interface GamesMapper {
|
||||||
FROM games g
|
FROM games g
|
||||||
JOIN users u ON u.id = g.user_id
|
JOIN users u ON u.id = g.user_id
|
||||||
WHERE g.is_visible IS NOT FALSE
|
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
|
ORDER BY g.sort_order ASC, g.created_at DESC, g.id DESC
|
||||||
""")
|
""")
|
||||||
List<GameData> getVisibleGames();
|
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("""
|
||||||
INSERT INTO games (
|
INSERT INTO games (
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -82,6 +117,7 @@ public interface GamesMapper {
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT COALESCE(MAX(sort_order), 0) + 1
|
SELECT COALESCE(MAX(sort_order), 0) + 1
|
||||||
FROM games
|
FROM games
|
||||||
|
WHERE is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
int nextSortOrder();
|
int nextSortOrder();
|
||||||
|
|
||||||
|
|
@ -95,8 +131,36 @@ public interface GamesMapper {
|
||||||
webgl_path = #{webglPath},
|
webgl_path = #{webglPath},
|
||||||
thumbnail_url = #{thumbnailUrl},
|
thumbnail_url = #{thumbnailUrl},
|
||||||
is_visible = #{visible},
|
is_visible = #{visible},
|
||||||
sort_order = #{sortOrder}
|
sort_order = #{sortOrder},
|
||||||
|
updated_at = now()
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
int updateGame(GameData game);
|
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
|
updated_at AS updatedAt
|
||||||
FROM user_auth_identities
|
FROM user_auth_identities
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
UserAuthIdentityData getUserAuthIdentity(long id);
|
UserAuthIdentityData getUserAuthIdentity(long id);
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ public interface UserAuthIdentitiesMapper {
|
||||||
FROM user_auth_identities
|
FROM user_auth_identities
|
||||||
WHERE provider = #{provider}
|
WHERE provider = #{provider}
|
||||||
AND provider_user_id = #{providerUserId}
|
AND provider_user_id = #{providerUserId}
|
||||||
AND is_delete = false
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
UserAuthIdentityData getUserAuthIdentityByProvider(
|
UserAuthIdentityData getUserAuthIdentityByProvider(
|
||||||
@Param("provider") String provider,
|
@Param("provider") String provider,
|
||||||
|
|
@ -86,6 +87,7 @@ public interface UserAuthIdentitiesMapper {
|
||||||
last_login_at = #{lastLoginAt},
|
last_login_at = #{lastLoginAt},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
int updateUserAuthIdentity(UserAuthIdentityData userAuthIdentity);
|
int updateUserAuthIdentity(UserAuthIdentityData userAuthIdentity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ public interface UsersMapper {
|
||||||
updated_at AS updatedAt
|
updated_at AS updatedAt
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
UserData getUser(long id);
|
UserData getUser(long id);
|
||||||
|
|
||||||
|
|
@ -57,6 +58,7 @@ public interface UsersMapper {
|
||||||
last_login_at = #{lastLoginAt},
|
last_login_at = #{lastLoginAt},
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
|
AND is_delete IS NOT TRUE
|
||||||
""")
|
""")
|
||||||
int updateUser(UserData user);
|
int updateUser(UserData user);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ server.tomcat.max-http-form-post-size=1GB
|
||||||
server.tomcat.max-swallow-size=-1
|
server.tomcat.max-swallow-size=-1
|
||||||
|
|
||||||
# common
|
# 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
|
# log
|
||||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
logging.level.org.apache.ibatis=TRACE
|
logging.level.org.apache.ibatis=TRACE
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<%
|
||||||
|
boolean owner = Boolean.TRUE.equals(request.getAttribute("owner"));
|
||||||
|
%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -79,6 +82,55 @@
|
||||||
box-shadow: 0 4px 16px rgba(232, 165, 75, 0.12);
|
box-shadow: 0 4px 16px rgba(232, 165, 75, 0.12);
|
||||||
transform: translateX(-2px);
|
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 {
|
.game-info-card {
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.75rem;
|
||||||
|
|
@ -312,24 +364,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
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 {
|
.game-community {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
|
|
@ -634,10 +668,18 @@
|
||||||
<body>
|
<body>
|
||||||
<jsp:include page="/WEB-INF/views/header.jsp"/>
|
<jsp:include page="/WEB-INF/views/header.jsp"/>
|
||||||
<main class="game-page">
|
<main class="game-page">
|
||||||
<a class="game-back" href="${pageContext.request.contextPath}/">
|
<div class="game-topbar">
|
||||||
<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 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>
|
목록으로
|
||||||
|
</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">
|
<section class="game-info-card" aria-labelledby="game-title">
|
||||||
<h1 id="game-title">${gameName}</h1>
|
<h1 id="game-title">${gameName}</h1>
|
||||||
|
|
@ -694,8 +736,6 @@
|
||||||
referrerpolicy="same-origin"></iframe>
|
referrerpolicy="same-origin"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
|
|
||||||
<div class="game-community">
|
<div class="game-community">
|
||||||
|
|
@ -747,6 +787,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
var ctx = '${pageContext.request.contextPath}';
|
||||||
var gameId = ${gameId};
|
var gameId = ${gameId};
|
||||||
var baseLikes = ${likeCount};
|
var baseLikes = ${likeCount};
|
||||||
var LIKE_KEY = 'bibimbap-game-liked';
|
var LIKE_KEY = 'bibimbap-game-liked';
|
||||||
|
|
@ -782,6 +823,54 @@
|
||||||
var likeBtn = document.getElementById('game-like-btn');
|
var likeBtn = document.getElementById('game-like-btn');
|
||||||
var likeCountEl = document.getElementById('game-like-count');
|
var likeCountEl = document.getElementById('game-like-count');
|
||||||
var gid = String(gameId);
|
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() {
|
function syncLike() {
|
||||||
var liked = isLiked(gid);
|
var liked = isLiked(gid);
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,25 @@
|
||||||
String ctx = request.getContextPath();
|
String ctx = request.getContextPath();
|
||||||
String rawDisplayName = session.getAttribute("displayName") == null ? "" : String.valueOf(session.getAttribute("displayName"));
|
String rawDisplayName = session.getAttribute("displayName") == null ? "" : String.valueOf(session.getAttribute("displayName"));
|
||||||
String creatorValue = HtmlUtils.htmlEscape(rawDisplayName);
|
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">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
|
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
|
||||||
<title>신규 게임 개시 | bibimbap</title>
|
<title><%= pageTitle %> | bibimbap</title>
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
|
@ -342,59 +354,59 @@
|
||||||
<main class="game-register-main">
|
<main class="game-register-main">
|
||||||
<section class="game-register-heading" aria-labelledby="game-register-title">
|
<section class="game-register-heading" aria-labelledby="game-register-title">
|
||||||
<p class="game-register-heading__eyebrow">BIBIMBAP GAME</p>
|
<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>
|
</section>
|
||||||
|
|
||||||
<div class="game-register-layout">
|
<div class="game-register-layout">
|
||||||
<section class="game-register-panel" aria-label="게임 등록 양식">
|
<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-form__grid">
|
||||||
<div class="game-field game-field--full">
|
<div class="game-field game-field--full">
|
||||||
<label class="game-field__label" for="game-name">게임 이름</label>
|
<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>
|
||||||
<div class="game-field">
|
<div class="game-field">
|
||||||
<label class="game-field__label" for="game-git-url">소스 코드 URL</label>
|
<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>
|
||||||
<div class="game-field">
|
<div class="game-field">
|
||||||
<label class="game-field__label" for="game-webgl-zip">WebGL zip</label>
|
<label class="game-field__label" for="game-webgl-zip">WebGL zip</label>
|
||||||
<label class="game-file" for="game-webgl-zip">
|
<label class="game-file" for="game-webgl-zip">
|
||||||
<span class="game-file__action">zip 선택</span>
|
<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>
|
</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>
|
||||||
<div class="game-field">
|
<div class="game-field">
|
||||||
<label class="game-field__label" for="game-thumbnail-image">썸네일 이미지</label>
|
<label class="game-field__label" for="game-thumbnail-image">썸네일 이미지</label>
|
||||||
<label class="game-file" for="game-thumbnail-image">
|
<label class="game-file" for="game-thumbnail-image">
|
||||||
<span class="game-file__action">이미지 선택</span>
|
<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>
|
</label>
|
||||||
<input class="game-file__input" type="file" id="game-thumbnail-image" name="thumbnailImage" accept="image/png,image/jpeg,image/webp,image/gif" />
|
<input class="game-file__input" type="file" id="game-thumbnail-image" name="thumbnailImage" accept="image/png,image/jpeg,image/webp,image/gif" />
|
||||||
</div>
|
</div>
|
||||||
<div class="game-field game-field--full">
|
<div class="game-field game-field--full">
|
||||||
<label class="game-field__label" for="game-creator-note">소개</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="game-uploaded-game-uuid" name="gameUuid" />
|
<input type="hidden" id="game-uploaded-game-uuid" name="gameUuid" value="<%= editGameUuid %>" />
|
||||||
<input type="hidden" id="game-uploaded-webgl-path" name="webglPath" />
|
<input type="hidden" id="game-uploaded-webgl-path" name="webglPath" value="<%= editWebglPath %>" />
|
||||||
<input type="hidden" id="game-uploaded-thumbnail-url" name="thumbnailUrl" />
|
<input type="hidden" id="game-uploaded-thumbnail-url" name="thumbnailUrl" value="<%= editThumbnailUrl %>" />
|
||||||
|
|
||||||
<label class="game-check">
|
<label class="game-check">
|
||||||
<input type="checkbox" name="visible" value="true" checked />
|
<input type="checkbox" name="visible" value="true" <%= editVisible ? "checked" : "" %> />
|
||||||
<span>목록에 공개</span>
|
<span>목록에 공개</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="game-form__actions">
|
<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">
|
<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">
|
<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="M12 5v14"/>
|
||||||
<path d="M5 12h14"/>
|
<path d="M5 12h14"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span id="game-register-submit-label">등록</span>
|
<span id="game-register-submit-label"><%= submitText %></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -402,13 +414,17 @@
|
||||||
|
|
||||||
<aside class="game-preview" aria-label="미리보기">
|
<aside class="game-preview" aria-label="미리보기">
|
||||||
<div class="game-preview__media">
|
<div class="game-preview__media">
|
||||||
|
<% if (editThumbnailUrl.isBlank()) { %>
|
||||||
<img id="preview-thumb" alt="" hidden />
|
<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>
|
||||||
<div class="game-preview__body">
|
<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__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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -418,6 +434,7 @@
|
||||||
(function () {
|
(function () {
|
||||||
var ctx = '<%= ctx %>';
|
var ctx = '<%= ctx %>';
|
||||||
var form = document.getElementById('game-register-form');
|
var form = document.getElementById('game-register-form');
|
||||||
|
var editMode = form && form.getAttribute('data-edit-mode') === 'true';
|
||||||
var nameInput = document.getElementById('game-name');
|
var nameInput = document.getElementById('game-name');
|
||||||
var gitUrlInput = document.getElementById('game-git-url');
|
var gitUrlInput = document.getElementById('game-git-url');
|
||||||
var noteInput = document.getElementById('game-creator-note');
|
var noteInput = document.getElementById('game-creator-note');
|
||||||
|
|
@ -489,7 +506,7 @@
|
||||||
if (!submitBtn) return;
|
if (!submitBtn) return;
|
||||||
submitBtn.disabled = submitting;
|
submitBtn.disabled = submitting;
|
||||||
if (submitLabel) {
|
if (submitLabel) {
|
||||||
submitLabel.textContent = submitting ? '등록 중...' : submitText;
|
submitLabel.textContent = submitting ? (editMode ? '수정 중...' : '등록 중...') : submitText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -524,7 +541,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerGame() {
|
function saveGame() {
|
||||||
var body = new URLSearchParams();
|
var body = new URLSearchParams();
|
||||||
body.set('name', valueOr(nameInput, ''));
|
body.set('name', valueOr(nameInput, ''));
|
||||||
body.set('gitUrl', valueOr(gitUrlInput, ''));
|
body.set('gitUrl', valueOr(gitUrlInput, ''));
|
||||||
|
|
@ -612,13 +629,16 @@
|
||||||
|
|
||||||
var webglFile = webglInput && webglInput.files ? webglInput.files[0] : null;
|
var webglFile = webglInput && webglInput.files ? webglInput.files[0] : null;
|
||||||
var thumbnailFile = thumbnailInput && thumbnailInput.files ? thumbnailInput.files[0] : null;
|
var thumbnailFile = thumbnailInput && thumbnailInput.files ? thumbnailInput.files[0] : null;
|
||||||
if (!webglFile) {
|
if (!editMode && !webglFile) {
|
||||||
openModal('업로드 실패', 'WebGL zip 파일을 선택해 주세요.');
|
openModal('업로드 실패', 'WebGL zip 파일을 선택해 주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubmitting(true);
|
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) {
|
if (uploadedGameUuid && webglResult && webglResult.gameUuid) {
|
||||||
uploadedGameUuid.value = webglResult.gameUuid;
|
uploadedGameUuid.value = webglResult.gameUuid;
|
||||||
}
|
}
|
||||||
|
|
@ -644,9 +664,9 @@
|
||||||
if (uploadedThumbnailUrl && thumbnailResult && thumbnailResult.thumbnailUrl) {
|
if (uploadedThumbnailUrl && thumbnailResult && thumbnailResult.thumbnailUrl) {
|
||||||
uploadedThumbnailUrl.value = thumbnailResult.thumbnailUrl;
|
uploadedThumbnailUrl.value = thumbnailResult.thumbnailUrl;
|
||||||
}
|
}
|
||||||
return registerGame();
|
return saveGame();
|
||||||
}).then(function (registerResult) {
|
}).then(function (registerResult) {
|
||||||
openModal('게임 등록 완료', '게임 등록이 완료되었습니다.', '확인', function () {
|
openModal(editMode ? '게임 수정 완료' : '게임 등록 완료', editMode ? '게임이 수정되었습니다.' : '게임 등록이 완료되었습니다.', '확인', function () {
|
||||||
if (registerResult && registerResult.gameId) {
|
if (registerResult && registerResult.gameId) {
|
||||||
window.location.href = ctx + '/game/' + registerResult.gameId;
|
window.location.href = ctx + '/game/' + registerResult.gameId;
|
||||||
} else if (registerResult && registerResult.location) {
|
} else if (registerResult && registerResult.location) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@
|
||||||
if (gamesAttr instanceof List<?>) {
|
if (gamesAttr instanceof List<?>) {
|
||||||
games = (List<GameData>) gamesAttr;
|
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>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
@ -453,10 +460,10 @@
|
||||||
<main class="page-main">
|
<main class="page-main">
|
||||||
<section class="search-section" aria-label="게임·제작자 검색">
|
<section class="search-section" aria-label="게임·제작자 검색">
|
||||||
<div class="home-toolbar">
|
<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">
|
<div class="search-form__field">
|
||||||
<label class="search-form__label" for="q">게임·제작자 검색</label>
|
<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>
|
</div>
|
||||||
<button class="search-form__submit" type="submit">검색</button>
|
<button class="search-form__submit" type="submit">검색</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -483,8 +490,12 @@
|
||||||
|
|
||||||
<section class="card-grid" aria-label="추천 목록">
|
<section class="card-grid" aria-label="추천 목록">
|
||||||
<% if (games.isEmpty()) { %>
|
<% if (games.isEmpty()) { %>
|
||||||
|
<% if (searching) { %>
|
||||||
|
<div class="home-empty">"<%= escapedSearchQuery %>"에 대한 검색 결과가 없습니다.</div>
|
||||||
|
<% } else { %>
|
||||||
<div class="home-empty">아직 공개된 게임이 없습니다.<br>로그인 후 첫 게임을 등록해 주세요.</div>
|
<div class="home-empty">아직 공개된 게임이 없습니다.<br>로그인 후 첫 게임을 등록해 주세요.</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% } %>
|
||||||
<%
|
<%
|
||||||
for (int i = 0; i < games.size(); i++) {
|
for (int i = 0; i < games.size(); i++) {
|
||||||
GameData game = games.get(i);
|
GameData game = games.get(i);
|
||||||
|
|
@ -605,7 +616,7 @@
|
||||||
textBtn.title = term;
|
textBtn.title = term;
|
||||||
textBtn.addEventListener('click', function () {
|
textBtn.addEventListener('click', function () {
|
||||||
input.value = term;
|
input.value = term;
|
||||||
input.focus();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
var removeBtn = document.createElement('button');
|
var removeBtn = document.createElement('button');
|
||||||
|
|
@ -627,17 +638,18 @@
|
||||||
|
|
||||||
if (form && input) {
|
if (form && input) {
|
||||||
form.addEventListener('submit', function (ev) {
|
form.addEventListener('submit', function (ev) {
|
||||||
ev.preventDefault();
|
var q = input.value.trim();
|
||||||
var q = input.value;
|
input.value = q;
|
||||||
addSearchQuery(q);
|
addSearchQuery(q);
|
||||||
renderChips();
|
renderChips();
|
||||||
/* 실제 검색 URL 연동 시: location.href = '...?q=' + encodeURIComponent(q.trim()); */
|
|
||||||
});
|
});
|
||||||
|
|
||||||
clearBtn.addEventListener('click', function () {
|
if (clearBtn) {
|
||||||
clearHistory();
|
clearBtn.addEventListener('click', function () {
|
||||||
renderChips();
|
clearHistory();
|
||||||
});
|
renderChips();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderChips();
|
renderChips();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,24 @@
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
confirmBtn.focus();
|
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) {
|
prompt: function (options) {
|
||||||
var opts = options || {};
|
var opts = options || {};
|
||||||
lastFocused = document.activeElement;
|
lastFocused = document.activeElement;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue