게임 등록 버그 수정
This commit is contained in:
parent
b61973f96d
commit
4e49fd21ff
|
|
@ -28,7 +28,7 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
|
||||
@RequestMapping("/error")
|
||||
public ModelAndView errorView(HttpServletRequest request) {
|
||||
ModelAndView mv = new ModelAndView("/errer");
|
||||
ModelAndView mv = new ModelAndView("errer");
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
if (status != null) {
|
||||
mv.addObject("statusCode", status);
|
||||
|
|
@ -46,7 +46,7 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
if (!isLoggedIn(session)) {
|
||||
return new ModelAndView("redirect:/login");
|
||||
}
|
||||
return new ModelAndView("/game-register");
|
||||
return new ModelAndView("game-register");
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
|
|
@ -65,7 +65,7 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
}
|
||||
switch (pageName) {
|
||||
case "error":
|
||||
mv.setViewName("/errer");
|
||||
mv.setViewName("errer");
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
if (status != null) {
|
||||
mv.addObject("statusCode", status);
|
||||
|
|
@ -75,22 +75,22 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
if (isLoggedIn(session)) {
|
||||
return new ModelAndView("redirect:/profile");
|
||||
}
|
||||
mv.setViewName("/login");
|
||||
mv.setViewName("login");
|
||||
break;
|
||||
case "profile":
|
||||
if (!isLoggedIn(session)) {
|
||||
return new ModelAndView("redirect:/login");
|
||||
}
|
||||
mv.setViewName("/profile");
|
||||
mv.setViewName("profile");
|
||||
break;
|
||||
case "signup":
|
||||
mv.setViewName("/signup");
|
||||
mv.setViewName("signup");
|
||||
break;
|
||||
case "terms":
|
||||
mv.setViewName("/terms");
|
||||
mv.setViewName("terms");
|
||||
break;
|
||||
case "operation-policy":
|
||||
mv.setViewName("/operation-policy");
|
||||
mv.setViewName("operation-policy");
|
||||
break;
|
||||
default:
|
||||
return indexModelAndView();
|
||||
|
|
@ -100,7 +100,7 @@ public class WebMvcController implements WebMvcConfigurer, ErrorController {
|
|||
}
|
||||
|
||||
private ModelAndView indexModelAndView() {
|
||||
ModelAndView mv = new ModelAndView("/index");
|
||||
ModelAndView mv = new ModelAndView("index");
|
||||
mv.addObject("games", gamesMapper.getVisibleGames());
|
||||
return mv;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package com.pandoli365.bibimbap.controller.api;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class ApiExceptionControllerAdvice {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiExceptionControllerAdvice.class);
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleMaxUploadSizeExceeded() {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("status", HttpStatus.PAYLOAD_TOO_LARGE.value());
|
||||
body.put("message", "upload file is too large. WebGL zip must be 1GB or smaller.");
|
||||
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(body);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleException(Exception exception) {
|
||||
log.error("API request failed", exception);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
body.put("message", "server error");
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,8 @@ public class GameController {
|
|||
@RequestParam(name = "visible", required = false) String visible,
|
||||
HttpSession session
|
||||
) {
|
||||
if (sessionUserId(session) == null) {
|
||||
Long userId = sessionUserId(session);
|
||||
if (userId == null) {
|
||||
return response(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
|
||||
}
|
||||
|
||||
|
|
@ -65,8 +66,8 @@ public class GameController {
|
|||
}
|
||||
|
||||
GameData game = new GameData();
|
||||
game.setUserId(userId);
|
||||
game.setName(normalizedName);
|
||||
game.setCreator(sessionDisplayName(session));
|
||||
game.setCreatorNote(normalizedCreatorNote);
|
||||
game.setGitUrl(trimToEmpty(gitUrl));
|
||||
game.setWebglPath(normalizedWebglPath);
|
||||
|
|
@ -145,27 +146,6 @@ public class GameController {
|
|||
return null;
|
||||
}
|
||||
|
||||
private String sessionDisplayName(HttpSession session) {
|
||||
String displayName = sessionValue(session, "displayName");
|
||||
if (displayName != null) {
|
||||
return displayName;
|
||||
}
|
||||
String email = sessionValue(session, "email");
|
||||
return email == null ? "bibimbap 사용자" : email;
|
||||
}
|
||||
|
||||
private String sessionValue(HttpSession session, String name) {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = session.getAttribute(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isBlank() ? null : text;
|
||||
}
|
||||
|
||||
private boolean isChecked(String value) {
|
||||
return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import jakarta.servlet.http.HttpSession;
|
|||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
|
@ -31,6 +34,7 @@ import java.util.zip.ZipInputStream;
|
|||
@RequestMapping("/api/game-files")
|
||||
public class GameUploadController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GameUploadController.class);
|
||||
private static final long WEBGL_EXTRACTED_MAX_BYTES = 512L * 1024 * 1024;
|
||||
private static final int WEBGL_MAX_ENTRIES = 8_000;
|
||||
private static final long THUMBNAIL_MAX_BYTES = 10L * 1024 * 1024;
|
||||
|
|
@ -38,6 +42,11 @@ public class GameUploadController {
|
|||
@Value("${app.upload.game-storage-path:src/main/resources/static}")
|
||||
private String uploadStoragePath;
|
||||
|
||||
@GetMapping("/ping")
|
||||
public ResponseEntity<Map<String, Object>> ping() {
|
||||
return ResponseEntity.ok(Map.of("status", "ok"));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> uploadGameFiles(
|
||||
@RequestParam("files") MultipartFile[] files,
|
||||
|
|
@ -87,6 +96,10 @@ public class GameUploadController {
|
|||
@RequestParam(name = "file", required = false) MultipartFile file,
|
||||
HttpSession session
|
||||
) throws IOException {
|
||||
log.info("WebGL zip upload request received. fileName={}, size={}, contentType={}",
|
||||
file == null ? null : file.getOriginalFilename(),
|
||||
file == null ? null : file.getSize(),
|
||||
file == null ? null : file.getContentType());
|
||||
Long userId = sessionUserId(session);
|
||||
if (userId == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
|
|
@ -125,6 +138,8 @@ public class GameUploadController {
|
|||
response.put("deployPath", deployPath);
|
||||
response.put("entryCount", extractResult.entryCount());
|
||||
response.put("extractedBytes", extractResult.extractedBytes());
|
||||
log.info("WebGL zip upload completed. gameUuid={}, webglPath={}, entryCount={}, extractedBytes={}",
|
||||
gameUuid, webglPath, extractResult.entryCount(), extractResult.extractedBytes());
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
deleteRecursively(targetDir);
|
||||
|
|
|
|||
|
|
@ -164,6 +164,39 @@ public class UserController {
|
|||
return "redirect:/";
|
||||
}
|
||||
|
||||
@PostMapping("/profile/nickname")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateProfileNickname(
|
||||
@RequestParam(name = "displayName", required = false) String displayName,
|
||||
HttpServletRequest request
|
||||
) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Long userId = sessionUserId(session);
|
||||
if (userId == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(new AuthResult(HttpStatus.UNAUTHORIZED.value(), "로그인이 필요합니다."));
|
||||
}
|
||||
|
||||
String normalizedDisplayName = normalizeDisplayName(displayName, null);
|
||||
if (normalizedDisplayName == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "변경할 닉네임을 입력해 주세요."));
|
||||
}
|
||||
|
||||
UserData user = usersMapper.getUser(userId);
|
||||
if (user == null || !STATUS_ACTIVE.equals(user.getStatus())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(new AuthResult(HttpStatus.FORBIDDEN.value(), "사용할 수 없는 계정입니다."));
|
||||
}
|
||||
|
||||
user.setDisplayName(normalizedDisplayName);
|
||||
usersMapper.updateUser(user);
|
||||
updateAuthIdentityDisplayName(session, normalizedDisplayName);
|
||||
updateDisplayNameSession(session, normalizedDisplayName);
|
||||
|
||||
return ResponseEntity.ok(new ProfileNicknameResult(200, "닉네임이 변경되었습니다.", normalizedDisplayName));
|
||||
}
|
||||
|
||||
@PostMapping("/profile/avatar")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateProfileAvatar(
|
||||
|
|
@ -390,6 +423,30 @@ public class UserController {
|
|||
userAuthIdentitiesMapper.updateUserAuthIdentity(identity);
|
||||
}
|
||||
|
||||
private void updateAuthIdentityDisplayName(HttpSession session, String displayName) {
|
||||
Object authIdentityId = session.getAttribute("authIdentityId");
|
||||
Long identityId = null;
|
||||
if (authIdentityId instanceof Number number) {
|
||||
identityId = number.longValue();
|
||||
} else if (authIdentityId instanceof String text) {
|
||||
try {
|
||||
identityId = Long.parseLong(text);
|
||||
} catch (NumberFormatException e) {
|
||||
identityId = null;
|
||||
}
|
||||
}
|
||||
if (identityId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserAuthIdentityData identity = userAuthIdentitiesMapper.getUserAuthIdentity(identityId);
|
||||
if (identity == null) {
|
||||
return;
|
||||
}
|
||||
identity.setDisplayName(displayName);
|
||||
userAuthIdentitiesMapper.updateUserAuthIdentity(identity);
|
||||
}
|
||||
|
||||
private void updateAvatarSession(HttpSession session, String avatarUrl) {
|
||||
session.setAttribute("avatarUrl", avatarUrl);
|
||||
|
||||
|
|
@ -406,6 +463,22 @@ public class UserController {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateDisplayNameSession(HttpSession session, String displayName) {
|
||||
session.setAttribute("displayName", displayName);
|
||||
|
||||
Object account = session.getAttribute("account");
|
||||
if (account instanceof Map<?, ?> existingAccount) {
|
||||
Map<String, Object> updatedAccount = new LinkedHashMap<>();
|
||||
existingAccount.forEach((key, value) -> {
|
||||
if (key != null) {
|
||||
updatedAccount.put(String.valueOf(key), value);
|
||||
}
|
||||
});
|
||||
updatedAccount.put("displayName", displayName);
|
||||
session.setAttribute("account", updatedAccount);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveLoginSession(HttpSession session, UserData user, UserAuthIdentityData identity) {
|
||||
session.setAttribute("id", user.getId());
|
||||
session.setAttribute("userId", user.getId());
|
||||
|
|
@ -537,4 +610,7 @@ public class UserController {
|
|||
|
||||
public record ProfileAvatarResult(int status, String message, String avatarUrl) {
|
||||
}
|
||||
|
||||
public record ProfileNicknameResult(int status, String message, String displayName) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.time.OffsetDateTime;
|
|||
public class GameData {
|
||||
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String name;
|
||||
private String creator;
|
||||
private String creatorNote;
|
||||
|
|
@ -25,6 +26,14 @@ public class GameData {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,47 +14,51 @@ public interface GamesMapper {
|
|||
|
||||
@Select("""
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
creator,
|
||||
creator_note AS creatorNote,
|
||||
git_url AS gitUrl,
|
||||
webgl_path AS webglPath,
|
||||
thumbnail_url AS thumbnailUrl,
|
||||
like_count AS likeCount,
|
||||
is_visible AS visible,
|
||||
sort_order AS sortOrder,
|
||||
created_at AS createdAt,
|
||||
updated_at AS updatedAt
|
||||
FROM games
|
||||
WHERE id = #{id}
|
||||
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.id = #{id}
|
||||
""")
|
||||
GameData getGame(long id);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
creator,
|
||||
creator_note AS creatorNote,
|
||||
git_url AS gitUrl,
|
||||
webgl_path AS webglPath,
|
||||
thumbnail_url AS thumbnailUrl,
|
||||
like_count AS likeCount,
|
||||
is_visible AS visible,
|
||||
sort_order AS sortOrder,
|
||||
created_at AS createdAt,
|
||||
updated_at AS updatedAt
|
||||
FROM games
|
||||
WHERE is_visible IS NOT FALSE
|
||||
ORDER BY sort_order ASC, created_at DESC, id DESC
|
||||
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
|
||||
ORDER BY g.sort_order ASC, g.created_at DESC, g.id DESC
|
||||
""")
|
||||
List<GameData> getVisibleGames();
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO games (
|
||||
user_id,
|
||||
name,
|
||||
creator,
|
||||
creator_note,
|
||||
git_url,
|
||||
webgl_path,
|
||||
|
|
@ -62,8 +66,8 @@ public interface GamesMapper {
|
|||
is_visible,
|
||||
sort_order
|
||||
) VALUES (
|
||||
#{userId},
|
||||
#{name},
|
||||
#{creator},
|
||||
#{creatorNote},
|
||||
#{gitUrl},
|
||||
#{webglPath},
|
||||
|
|
@ -84,8 +88,8 @@ public interface GamesMapper {
|
|||
@Update("""
|
||||
UPDATE games
|
||||
SET
|
||||
user_id = #{userId},
|
||||
name = #{name},
|
||||
creator = #{creator},
|
||||
creator_note = #{creatorNote},
|
||||
git_url = #{gitUrl},
|
||||
webgl_path = #{webglPath},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
spring.profiles.active=@app.profile@
|
||||
spring.application.name=bibimbap
|
||||
|
||||
# ViewResolver
|
||||
spring.mvc.view.prefix=/WEB-INF/views/
|
||||
spring.mvc.view.suffix=.jsp
|
||||
|
||||
# IP
|
||||
server.address=0.0.0.0
|
||||
|
||||
# encoding
|
||||
server.servlet.encoding.force-response=false
|
||||
|
||||
# file upload Max Size
|
||||
spring.servlet.multipart.max-file-size=1GB
|
||||
spring.servlet.multipart.max-request-size=1GB
|
||||
server.tomcat.max-http-form-post-size=1GB
|
||||
server.tomcat.max-swallow-size=-1
|
||||
|
||||
# common
|
||||
spring.config.import=classpath:${spring.profiles.active}/db.properties
|
||||
|
||||
# log
|
||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
logging.level.org.apache.ibatis=TRACE
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
spring.profiles.active=dev
|
||||
spring.application.name=bibimbap
|
||||
|
||||
# ViewResolver
|
||||
spring.mvc.view.prefix=/WEB-INF/views/
|
||||
spring.mvc.view.suffix=.jsp
|
||||
|
||||
# common
|
||||
spring.config.import=classpath:dev/db.properties
|
||||
|
||||
# IP
|
||||
server.address=0.0.0.0
|
||||
|
||||
# encoding
|
||||
server.servlet.encoding.force-response=false
|
||||
|
||||
# file upload Max Size
|
||||
spring.servlet.multipart.max-file-size=100MB
|
||||
spring.servlet.multipart.max-request-size=100MB
|
||||
|
||||
# log
|
||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
logging.level.org.apache.ibatis=TRACE
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
spring.profiles.active=live
|
||||
spring.application.name=bibimbap
|
||||
|
||||
# ViewResolver
|
||||
spring.mvc.view.prefix=/WEB-INF/views/
|
||||
spring.mvc.view.suffix=.jsp
|
||||
|
||||
# common
|
||||
spring.config.import=classpath:live/db.properties
|
||||
|
||||
# IP
|
||||
server.address=0.0.0.0
|
||||
|
||||
# encoding
|
||||
server.servlet.encoding.force-response=false
|
||||
|
||||
# file upload Max Size
|
||||
spring.servlet.multipart.max-file-size=100MB
|
||||
spring.servlet.multipart.max-request-size=100MB
|
||||
|
||||
# log
|
||||
mybatis.configuration.log-impl=org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
logging.level.org.apache.ibatis=TRACE
|
||||
|
|
@ -508,6 +508,8 @@
|
|||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: body
|
||||
}).catch(function () {
|
||||
throw new Error('Upload request failed before receiving a server response. Please check the server console and browser Network tab.');
|
||||
}).then(function (res) {
|
||||
return res.json().catch(function () {
|
||||
return { message: '파일을 업로드하지 못했습니다.' };
|
||||
|
|
|
|||
|
|
@ -46,6 +46,35 @@
|
|||
line-height: 1.6;
|
||||
color: var(--modal-muted, #5c5c5c);
|
||||
}
|
||||
.site-modal__field {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.site-modal__field[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.site-modal__field-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 800;
|
||||
color: var(--modal-text, #1a1a1a);
|
||||
}
|
||||
.site-modal__input {
|
||||
width: 100%;
|
||||
height: 2.875rem;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--modal-border, rgba(0, 0, 0, 0.08));
|
||||
border-radius: 10px;
|
||||
padding: 0 0.875rem;
|
||||
background: var(--modal-bg, #fff);
|
||||
color: var(--modal-text, #1a1a1a);
|
||||
font: inherit;
|
||||
}
|
||||
.site-modal__input:focus {
|
||||
outline: none;
|
||||
border-color: #e8a54b;
|
||||
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.22);
|
||||
}
|
||||
.site-modal__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
@ -65,6 +94,14 @@
|
|||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.site-modal__button--secondary {
|
||||
border-color: var(--modal-border, rgba(0, 0, 0, 0.08));
|
||||
background: transparent;
|
||||
color: var(--modal-text, #1a1a1a);
|
||||
}
|
||||
.site-modal__button[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.site-modal__button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
|
@ -73,7 +110,12 @@
|
|||
<div class="site-modal__panel" role="document">
|
||||
<h2 class="site-modal__title" id="site-modal-title"></h2>
|
||||
<p class="site-modal__message" id="site-modal-message"></p>
|
||||
<label class="site-modal__field" id="site-modal-field" hidden>
|
||||
<span class="site-modal__field-label" id="site-modal-field-label"></span>
|
||||
<input class="site-modal__input" type="text" id="site-modal-input" autocomplete="off" />
|
||||
</label>
|
||||
<div class="site-modal__actions">
|
||||
<button class="site-modal__button site-modal__button--secondary" type="button" id="site-modal-cancel" hidden>취소</button>
|
||||
<button class="site-modal__button" type="button" id="site-modal-confirm">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -83,33 +125,51 @@
|
|||
var root = document.getElementById('site-modal');
|
||||
var titleEl = document.getElementById('site-modal-title');
|
||||
var messageEl = document.getElementById('site-modal-message');
|
||||
var fieldEl = document.getElementById('site-modal-field');
|
||||
var fieldLabelEl = document.getElementById('site-modal-field-label');
|
||||
var inputEl = document.getElementById('site-modal-input');
|
||||
var cancelBtn = document.getElementById('site-modal-cancel');
|
||||
var confirmBtn = document.getElementById('site-modal-confirm');
|
||||
var onConfirm = null;
|
||||
var onCancel = null;
|
||||
var lastFocused = null;
|
||||
|
||||
if (!root || !titleEl || !messageEl || !confirmBtn) return;
|
||||
if (!root || !titleEl || !messageEl || !fieldEl || !fieldLabelEl || !inputEl || !cancelBtn || !confirmBtn) return;
|
||||
|
||||
function close() {
|
||||
function close(confirmed) {
|
||||
root.hidden = true;
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
var callback = onConfirm;
|
||||
var callback = confirmed ? onConfirm : onCancel;
|
||||
var inputValue = inputEl.value;
|
||||
onConfirm = null;
|
||||
onCancel = null;
|
||||
if (lastFocused && typeof lastFocused.focus === 'function') {
|
||||
lastFocused.focus();
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
callback(inputValue);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(ev) {
|
||||
if (ev.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
close();
|
||||
close(true);
|
||||
} else if (ev.key === 'Escape' && !cancelBtn.hidden) {
|
||||
ev.preventDefault();
|
||||
close(false);
|
||||
}
|
||||
}
|
||||
|
||||
confirmBtn.addEventListener('click', close);
|
||||
confirmBtn.addEventListener('click', function () {
|
||||
close(true);
|
||||
});
|
||||
cancelBtn.addEventListener('click', function () {
|
||||
close(false);
|
||||
});
|
||||
|
||||
window.BibimbapModal = {
|
||||
alert: function (options) {
|
||||
|
|
@ -117,11 +177,45 @@
|
|||
lastFocused = document.activeElement;
|
||||
titleEl.textContent = opts.title || '알림';
|
||||
messageEl.textContent = opts.message || '';
|
||||
fieldEl.hidden = true;
|
||||
inputEl.value = '';
|
||||
inputEl.removeAttribute('maxlength');
|
||||
inputEl.removeAttribute('placeholder');
|
||||
cancelBtn.hidden = true;
|
||||
confirmBtn.textContent = opts.confirmText || '확인';
|
||||
onConfirm = opts.onConfirm || null;
|
||||
onCancel = null;
|
||||
root.hidden = false;
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
confirmBtn.focus();
|
||||
},
|
||||
prompt: function (options) {
|
||||
var opts = options || {};
|
||||
lastFocused = document.activeElement;
|
||||
titleEl.textContent = opts.title || '입력';
|
||||
messageEl.textContent = opts.message || '';
|
||||
fieldLabelEl.textContent = opts.label || '입력값';
|
||||
inputEl.value = opts.value || '';
|
||||
if (opts.placeholder) {
|
||||
inputEl.setAttribute('placeholder', opts.placeholder);
|
||||
} else {
|
||||
inputEl.removeAttribute('placeholder');
|
||||
}
|
||||
if (opts.maxLength) {
|
||||
inputEl.setAttribute('maxlength', opts.maxLength);
|
||||
} else {
|
||||
inputEl.removeAttribute('maxlength');
|
||||
}
|
||||
fieldEl.hidden = false;
|
||||
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);
|
||||
inputEl.focus();
|
||||
inputEl.select();
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -7,20 +7,12 @@
|
|||
String rawDisplayName = session.getAttribute("displayName") == null ? "" : String.valueOf(session.getAttribute("displayName"));
|
||||
String rawEmail = session.getAttribute("email") == null ? "" : String.valueOf(session.getAttribute("email"));
|
||||
String rawAvatarUrl = session.getAttribute("avatarUrl") == null ? "" : String.valueOf(session.getAttribute("avatarUrl"));
|
||||
String rawRole = session.getAttribute("role") == null ? "" : String.valueOf(session.getAttribute("role"));
|
||||
String rawStatus = session.getAttribute("status") == null ? "" : String.valueOf(session.getAttribute("status"));
|
||||
String rawAuthProvider = session.getAttribute("authProvider") == null ? "" : String.valueOf(session.getAttribute("authProvider"));
|
||||
String rawLastLoginAt = session.getAttribute("lastLoginAt") == null ? "" : String.valueOf(session.getAttribute("lastLoginAt"));
|
||||
String initialSource = rawDisplayName.isBlank() ? rawEmail : rawDisplayName;
|
||||
String initial = initialSource.isBlank() ? "B" : initialSource.substring(0, 1).toUpperCase(Locale.ROOT);
|
||||
|
||||
String displayName = HtmlUtils.htmlEscape(rawDisplayName.isBlank() ? "bibimbap 사용자" : rawDisplayName);
|
||||
String email = HtmlUtils.htmlEscape(rawEmail.isBlank() ? "이메일 정보 없음" : rawEmail);
|
||||
String avatarUrl = HtmlUtils.htmlEscape(rawAvatarUrl);
|
||||
String role = HtmlUtils.htmlEscape(rawRole.isBlank() ? "USER" : rawRole);
|
||||
String status = HtmlUtils.htmlEscape(rawStatus.isBlank() ? "ACTIVE" : rawStatus);
|
||||
String authProvider = HtmlUtils.htmlEscape(rawAuthProvider.isBlank() ? "email" : rawAuthProvider);
|
||||
String lastLoginAt = HtmlUtils.htmlEscape(rawLastLoginAt.isBlank() ? "이번 세션" : rawLastLoginAt);
|
||||
String avatarInitial = HtmlUtils.htmlEscape(initial);
|
||||
%>
|
||||
<html lang="ko">
|
||||
|
|
@ -280,34 +272,16 @@
|
|||
<span id="profile-avatar-initial" <%= avatarUrl.isBlank() ? "" : "hidden" %>><%= avatarInitial %></span>
|
||||
</div>
|
||||
<div class="profile-summary__body">
|
||||
<h2 class="profile-summary__name"><%= displayName %></h2>
|
||||
<h2 class="profile-summary__name" id="profile-display-name"><%= displayName %></h2>
|
||||
<p class="profile-summary__email"><%= email %></p>
|
||||
<form class="profile-avatar-form" action="<%= ctx %>/profile/avatar" method="post" enctype="multipart/form-data" id="profile-avatar-form">
|
||||
<label class="profile-button profile-button--small" for="profile-avatar-input" id="profile-avatar-label">프로필 이미지 변경</label>
|
||||
<input class="profile-avatar-form__input" type="file" id="profile-avatar-input" name="avatar" accept="image/png,image/jpeg,image/webp,image/gif" />
|
||||
<button class="profile-button profile-button--small" type="button" id="profile-nickname-button" data-current-name="<%= displayName %>">닉네임 변경</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-details" aria-label="계정 정보">
|
||||
<div class="profile-detail">
|
||||
<p class="profile-detail__label">권한</p>
|
||||
<p class="profile-detail__value"><%= role %></p>
|
||||
</div>
|
||||
<div class="profile-detail">
|
||||
<p class="profile-detail__label">상태</p>
|
||||
<p class="profile-detail__value"><%= status %></p>
|
||||
</div>
|
||||
<div class="profile-detail">
|
||||
<p class="profile-detail__label">로그인 방식</p>
|
||||
<p class="profile-detail__value"><%= authProvider %></p>
|
||||
</div>
|
||||
<div class="profile-detail">
|
||||
<p class="profile-detail__label">최근 로그인</p>
|
||||
<p class="profile-detail__value"><%= lastLoginAt %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<a class="profile-button profile-button--primary" href="<%= ctx %>/">홈으로 이동</a>
|
||||
<form action="<%= ctx %>/logout" method="post">
|
||||
|
|
@ -326,9 +300,12 @@
|
|||
var avatarLabel = document.getElementById('profile-avatar-label');
|
||||
var avatarImg = document.getElementById('profile-avatar-img');
|
||||
var avatarInitial = document.getElementById('profile-avatar-initial');
|
||||
var nicknameButton = document.getElementById('profile-nickname-button');
|
||||
var displayNameEl = document.getElementById('profile-display-name');
|
||||
var previewUrl = null;
|
||||
var savedAvatarSrc = avatarImg && !avatarImg.hidden ? avatarImg.getAttribute('src') : '';
|
||||
var defaultLabelText = avatarLabel ? avatarLabel.textContent : '';
|
||||
var defaultNicknameText = nicknameButton ? nicknameButton.textContent : '';
|
||||
|
||||
function openModal(title, message, confirmText, onConfirm) {
|
||||
if (window.BibimbapModal && typeof window.BibimbapModal.alert === 'function') {
|
||||
|
|
@ -388,6 +365,98 @@
|
|||
avatarLabel.style.pointerEvents = uploading ? 'none' : '';
|
||||
}
|
||||
|
||||
function setNicknameSaving(saving) {
|
||||
if (!nicknameButton) return;
|
||||
nicknameButton.disabled = saving;
|
||||
nicknameButton.textContent = saving ? '변경 중...' : defaultNicknameText;
|
||||
}
|
||||
|
||||
function currentDisplayName() {
|
||||
if (!nicknameButton) return '';
|
||||
return nicknameButton.getAttribute('data-current-name') || '';
|
||||
}
|
||||
|
||||
function updateDisplayName(displayName) {
|
||||
if (displayNameEl) {
|
||||
displayNameEl.textContent = displayName;
|
||||
}
|
||||
if (nicknameButton) {
|
||||
nicknameButton.setAttribute('data-current-name', displayName);
|
||||
}
|
||||
if (avatarInitial) {
|
||||
avatarInitial.textContent = displayName ? displayName.charAt(0).toUpperCase() : 'B';
|
||||
}
|
||||
}
|
||||
|
||||
function submitNickname(nextName) {
|
||||
var value = (nextName || '').trim();
|
||||
if (!value) {
|
||||
openModal('닉네임 변경 실패', '변경할 닉네임을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (value.length > 32) {
|
||||
openModal('닉네임 변경 실패', '닉네임은 32자 이하로 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
var body = new URLSearchParams();
|
||||
body.set('displayName', value);
|
||||
setNicknameSaving(true);
|
||||
|
||||
fetch(ctx + '/profile/nickname', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: body
|
||||
}).then(function (res) {
|
||||
return res.json().catch(function () {
|
||||
return { message: '닉네임을 변경하지 못했습니다.' };
|
||||
}).then(function (data) {
|
||||
if (!res.ok) {
|
||||
var error = new Error(data && data.message ? data.message : '닉네임을 변경하지 못했습니다.');
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}).then(function (data) {
|
||||
updateDisplayName(data && data.displayName ? data.displayName : value);
|
||||
openModal('닉네임 변경 완료', '닉네임이 변경되었습니다.');
|
||||
}).catch(function (err) {
|
||||
openModal('닉네임 변경 실패', err.message || '닉네임을 변경하지 못했습니다.', '확인', function () {
|
||||
if (err.status === 401) {
|
||||
window.location.href = ctx + '/login';
|
||||
}
|
||||
});
|
||||
}).finally(function () {
|
||||
setNicknameSaving(false);
|
||||
});
|
||||
}
|
||||
|
||||
function openNicknamePrompt() {
|
||||
var currentName = currentDisplayName();
|
||||
if (window.BibimbapModal && typeof window.BibimbapModal.prompt === 'function') {
|
||||
window.BibimbapModal.prompt({
|
||||
title: '닉네임 변경',
|
||||
message: '새로운 닉네임을 입력해 주세요.',
|
||||
label: '닉네임',
|
||||
value: currentName,
|
||||
maxLength: 32,
|
||||
confirmText: '변경',
|
||||
cancelText: '취소',
|
||||
onConfirm: submitNickname
|
||||
});
|
||||
return;
|
||||
}
|
||||
var nextName = prompt('새로운 닉네임을 입력해 주세요.', currentName);
|
||||
if (nextName !== null) {
|
||||
submitNickname(nextName);
|
||||
}
|
||||
}
|
||||
|
||||
function uploadAvatar(file) {
|
||||
if (!form || !file) return;
|
||||
|
||||
|
|
@ -454,6 +523,10 @@
|
|||
});
|
||||
}
|
||||
|
||||
if (nicknameButton) {
|
||||
nicknameButton.addEventListener('click', openNicknamePrompt);
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ CREATE SEQUENCE IF NOT EXISTS "games_id_seq";
|
|||
CREATE TABLE "games" (
|
||||
"id" bigint DEFAULT nextval('games_id_seq'::regclass) NOT NULL,
|
||||
"name" character varying(200) NOT NULL,
|
||||
"creator" character varying(100) NOT NULL,
|
||||
"creator_note" text,
|
||||
"git_url" character varying(500),
|
||||
"webgl_path" character varying(500),
|
||||
|
|
@ -47,6 +46,7 @@ CREATE TABLE "games" (
|
|||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"is_delete" boolean DEFAULT false NOT NULL,
|
||||
"user_id" bigint NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue