2726 lines
113 KiB
Plaintext
2726 lines
113 KiB
Plaintext
TAP version 13
|
||
# Subtest: SSR player ships the stored ratio and responsive stage without the old hard limit
|
||
not ok 1 - SSR player ships the stored ratio and responsive stage without the old hard limit
|
||
---
|
||
duration_ms: 2.454666
|
||
type: 'test'
|
||
location: '/private/var/folders/g5/hclggccx15qdjf89b6vn1fw80000gn/T/bibimbap-prechange-oo0st4cd/src/test/js/webgl-player.test.cjs:29:1'
|
||
failureType: 'testCodeFailure'
|
||
error: |-
|
||
The input did not match the regular expression /data-display-width="<%= displayWidth %>"/. Input:
|
||
|
||
'<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>\n' +
|
||
'<%@ page import="org.springframework.web.util.HtmlUtils" %>\n' +
|
||
'<%\n' +
|
||
' boolean owner = Boolean.TRUE.equals(request.getAttribute("owner"));\n' +
|
||
' String ctx = request.getContextPath();\n' +
|
||
' String gameIdValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("gameId")));\n' +
|
||
' String gameNameValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("gameName")));\n' +
|
||
' String creatorValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("creator")));\n' +
|
||
' String likeCountFormattedValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("likeCountFormatted")));\n' +
|
||
' String creatorNoteValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("creatorNote")));\n' +
|
||
' String gitUrlValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("gitUrl")));\n' +
|
||
' String webglFrameSrc = String.valueOf(request.getAttribute("webglFrameSrc"));\n' +
|
||
' if (webglFrameSrc == null || webglFrameSrc.isBlank() || "null".equals(webglFrameSrc)) {\n' +
|
||
' webglFrameSrc = String.valueOf(request.getAttribute("webglUrl"));\n' +
|
||
' }\n' +
|
||
' if (webglFrameSrc == null || webglFrameSrc.isBlank() || "null".equals(webglFrameSrc)) {\n' +
|
||
' webglFrameSrc = "";\n' +
|
||
' } else if (webglFrameSrc.startsWith("/")) {\n' +
|
||
' webglFrameSrc = ctx + webglFrameSrc;\n' +
|
||
' }\n' +
|
||
' String webglFrameSrcValue = HtmlUtils.htmlEscape(webglFrameSrc);\n' +
|
||
'%>\n' +
|
||
'<!DOCTYPE html>\n' +
|
||
'<html lang="ko">\n' +
|
||
'<head>\n' +
|
||
' <meta charset="UTF-8">\n' +
|
||
' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
|
||
' <jsp:include page="/WEB-INF/views/theme-init.jsp"/>\n' +
|
||
' <title><%= gameNameValue %> — bibimbap</title>\n' +
|
||
' <style>\n' +
|
||
' html {\n' +
|
||
' color-scheme: light;\n' +
|
||
' --surface: #faf8f5;\n' +
|
||
' --card-bg: #fff;\n' +
|
||
' --text: #1a1a1a;\n' +
|
||
' --text-muted: #5c5c5c;\n' +
|
||
' --accent: #e8a54b;\n' +
|
||
' --accent-soft: rgba(232, 165, 75, 0.14);\n' +
|
||
' --border: rgba(0, 0, 0, 0.08);\n' +
|
||
' --webgl-bg: #0d0d0d;\n' +
|
||
' --panel-shadow: 0 4px 24px rgba(26, 26, 26, 0.06);\n' +
|
||
' --radius: 16px;\n' +
|
||
' }\n' +
|
||
' html[data-theme="dark"] {\n' +
|
||
' color-scheme: dark;\n' +
|
||
' --surface: #121212;\n' +
|
||
' --card-bg: #1e1e1e;\n' +
|
||
' --text: #ece8e1;\n' +
|
||
' --text-muted: #a39e96;\n' +
|
||
' --border: rgba(255, 255, 255, 0.1);\n' +
|
||
' --accent-soft: rgba(232, 165, 75, 0.12);\n' +
|
||
' --webgl-bg: #080808;\n' +
|
||
' --panel-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);\n' +
|
||
' }\n' +
|
||
' body {\n' +
|
||
' margin: 0;\n' +
|
||
' min-height: 100vh;\n' +
|
||
' font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;\n' +
|
||
' background: var(--surface);\n' +
|
||
' color: var(--text);\n' +
|
||
' }\n' +
|
||
' html[data-theme="dark"] body {\n' +
|
||
' background: linear-gradient(165deg, #1c1a18 0%, var(--surface) 38%, #0e0e0e 100%);\n' +
|
||
' background-attachment: fixed;\n' +
|
||
' }\n' +
|
||
' html:not([data-theme="dark"]) body {\n' +
|
||
' background: linear-gradient(165deg, #fff9f0 0%, var(--surface) 45%, #f3ede6 100%);\n' +
|
||
' background-attachment: fixed;\n' +
|
||
' }\n' +
|
||
' .game-page {\n' +
|
||
' max-width: 56rem;\n' +
|
||
' margin: 0 auto;\n' +
|
||
' padding: 1.25rem max(1rem, env(safe-area-inset-left)) 2.75rem max(1rem, env(safe-area-inset-right));\n' +
|
||
' }\n' +
|
||
' .game-back {\n' +
|
||
' display: inline-flex;\n' +
|
||
' align-items: center;\n' +
|
||
' gap: 0.45rem;\n' +
|
||
' margin-bottom: 1.25rem;\n' +
|
||
' padding: 0.5rem 0.95rem 0.5rem 0.75rem;\n' +
|
||
' font-size: 0.8125rem;\n' +
|
||
' font-weight: 600;\n' +
|
||
' color: var(--text-muted);\n' +
|
||
' text-decoration: none;\n' +
|
||
' background: var(--card-bg);\n' +
|
||
' border: 1px solid var(--border);\n' +
|
||
' border-radius: 999px;\n' +
|
||
' box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);\n' +
|
||
' transition: color 0.2s ease, border-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;\n' +
|
||
' }\n' +
|
||
' .game-back svg {\n' +
|
||
' width: 1rem;\n' +
|
||
' height: 1rem;\n' +
|
||
' flex-shrink: 0;\n' +
|
||
' opacity: 0.85;\n' +
|
||
' }\n' +
|
||
' .game-back:hover {\n' +
|
||
' color: var(--accent);\n' +
|
||
' border-color: rgba(232, 165, 75, 0.45);\n' +
|
||
' box-shadow: 0 4px 16px rgba(232, 165, 75, 0.12);\n' +
|
||
' transform: translateX(-2px);\n' +
|
||
' }\n' +
|
||
' .game-topbar {\n' +
|
||
' display: flex;\n' +
|
||
' align-items: center;\n' +
|
||
' justify-content: space-between;\n' +
|
||
' gap: 0.75rem;\n' +
|
||
' margin-bottom: 1.25rem;\n' +
|
||
' }\n' +
|
||
' .game-topbar .game-back {\n' +
|
||
' margin-bottom: 0;\n' +
|
||
' }\n' +
|
||
' .game-owner-actions {\n' +
|
||
' display: flex;\n' +
|
||
' gap: 0.5rem;\n' +
|
||
' flex-shrink: 0;\n' +
|
||
' }\n' +
|
||
' .game-owner-button {\n' +
|
||
' min-height: 2.25rem;\n' +
|
||
' padding: 0 0.8rem;\n' +
|
||
' border: 1px solid var(--border);\n' +
|
||
' border-radius: 999px;\n' +
|
||
' display: inline-flex;\n' +
|
||
' align-items: center;\n' +
|
||
' justify-content: center;\n' +
|
||
' background: var(--card-bg);\n' +
|
||
' color: var(--text);\n' +
|
||
' font-size: 0.8125rem;\n' +
|
||
' font-weight: 800;\n' +
|
||
' text-decoration: none;\n' +
|
||
' cursor: pointer;\n' +
|
||
' box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);\n' +
|
||
' }\n' +
|
||
' .game-owner-button:hover {\n' +
|
||
' border-color: rgba(232, 165, 75, 0.45);\n' +
|
||
' color: var(--accent);\n' +
|
||
' }\n' +
|
||
' .game-owner-button--danger:hover {\n' +
|
||
' border-color: rgba(180, 50, 50, 0.32);\n' +
|
||
' color: #b33;\n' +
|
||
' background: rgba(180, 50, 50, 0.06);\n' +
|
||
' }\n' +
|
||
' @media (max-width: 520px) {\n' +
|
||
' .game-topbar {\n' +
|
||
' align-items: stretch;\n' +
|
||
' flex-direction: column;\n' +
|
||
' }\n' +
|
||
' .game-owner-actions {\n' +
|
||
' justify-content: flex-end;\n' +
|
||
' }\n' +
|
||
' }\n' +
|
||
'\n' +
|
||
' .game-info-card {\n' +
|
||
' margin-bottom: 1.75rem;\n' +
|
||
' padding: 1.5rem 1.35rem 1.6rem;\n' +
|
||
' background: var(--card-bg);\n' +
|
||
' border: 1px solid var(--border);\n' +
|
||
' border-radius: var(--radius);\n' +
|
||
' box-shadow: var(--panel-shadow);\n' +
|
||
' position: relative;\n' +
|
||
' overflow: hidden;\n' +
|
||
' }\n' +
|
||
' .game-info-card::after {\n' +
|
||
' content: "";\n' +
|
||
' position: absolute;\n' +
|
||
' top: 0;\n' +
|
||
' left: 0;\n' +
|
||
' right: 0;\n' +
|
||
' height: 3px;\n' +
|
||
' background: linear-gradient(90deg, var(--accent), rgba(232, 165, 75, 0.25), transparent);\n' +
|
||
' opacity: 0.9;\n' +
|
||
' }\n' +
|
||
' .game-info-card h1 {\n' +
|
||
' margin: 0 0 1.25rem;\n' +
|
||
' padding-bottom: 0.85rem;\n' +
|
||
' font-size: clamp(1.5rem, 4.5vw, 1.85rem);\n' +
|
||
' font-weight: 800;\n' +
|
||
' letter-spacing: -0.04em;\n' +
|
||
' line-height: 1.2;\n' +
|
||
' border-bottom: 1px solid var(--border);\n' +
|
||
' }\n' +
|
||
'\n' +
|
||
' .game-meta-grid {\n' +
|
||
' display: grid;\n' +
|
||
' grid-template-columns: repeat(3, minmax(0, 1fr));\n' +
|
||
' gap: 0.65rem;\n' +
|
||
' }\n' +
|
||
' @media (max-width: 640px) {\n' +
|
||
' .game-meta-grid {\n' +
|
||
' grid-template-columns: 1fr;\n' +
|
||
' }\n' +
|
||
' }\n' +
|
||
' .game-meta-item {\n' +
|
||
' display: flex;\n' +
|
||
' flex-direction: column;\n' +
|
||
' gap: 0.35rem;\n' +
|
||
' padding: 0.85rem 0.9rem;\n' +
|
||
' background: var(--surface);\n' +
|
||
' border: 1px solid var(--border);\n' +
|
||
' border-radius: 12px;\n' +
|
||
' transition: border-color 0.2s ease, box-shadow 0.2s ease;\n' +
|
||
' }\n' +
|
||
' .game-meta-item:hover {\n' +
|
||
' border-color: rgba(232, 165, 75, 0.25);\n' +
|
||
' box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);\n' +
|
||
' }\n' +
|
||
' html[data-theme="dark"] .game-meta-item:hover {\n' +
|
||
' box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);\n' +
|
||
' }\n' +
|
||
' .game-meta-item__row {\n' +
|
||
' display: flex;\n' +
|
||
' align-items: center;\n' +
|
||
' gap: 0.45rem;\n' +
|
||
' }\n' +
|
||
' .game-meta-item__icon {\n' +
|
||
' display: flex;\n' +
|
||
' align-items: center;\n' +
|
||
' justify-content: center;\n' +
|
||
' width: 1.75rem;\n' +
|
||
' height: 1.75rem;\n' +
|
||
' border-radius: 8px;\n' +
|
||
' background: var(--accent-soft);\n' +
|
||
' color: var(--accent);\n' +
|
||
' flex-shrink: 0;\n' +
|
||
' }\n' +
|
||
' .game-meta-item__icon svg {\n' +
|
||
' width: 0.95rem;\n' +
|
||
' height: 0.95rem;\n' +
|
||
' }\n' +
|
||
' .game-meta-item__label {\n' +
|
||
' font-size: 0.6875rem;\n' +
|
||
' font-weight: 700;\n' +
|
||
' letter-spacing: 0.06em;\n' +
|
||
' text-transform: uppercase;\n' +
|
||
' color: var(--text-muted);\n' +
|
||
' }\n' +
|
||
' .game-meta-item__value {\n' +
|
||
' font-size: 0.9375rem;\n' +
|
||
' font-weight: 700;\n' +
|
||
' letter-spacing: -0.02em;\n' +
|
||
' color: var(--text);\n' +
|
||
' word-break: break-word;\n' +
|
||
' }\n' +
|
||
' .game-meta-item--likes {\n' +
|
||
' padding: 0.65rem 0.75rem 0.75rem;\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn {\n' +
|
||
' display: inline-flex;\n' +
|
||
' align-items: center;\n' +
|
||
' justify-content: center;\n' +
|
||
' gap: 0.45rem;\n' +
|
||
' width: 100%;\n' +
|
||
' margin: 0;\n' +
|
||
' margin-top: 0.1rem;\n' +
|
||
' padding: 0.55rem 0.7rem;\n' +
|
||
' font: inherit;\n' +
|
||
' border: 1px solid var(--border);\n' +
|
||
' border-radius: 10px;\n' +
|
||
' background: var(--card-bg);\n' +
|
||
' color: var(--text);\n' +
|
||
' cursor: pointer;\n' +
|
||
' transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.12s ease;\n' +
|
||
' -webkit-tap-highlight-color: transparent;\n' +
|
||
' box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn:hover {\n' +
|
||
' border-color: rgba(232, 165, 75, 0.45);\n' +
|
||
' box-shadow: 0 3px 12px rgba(232, 165, 75, 0.12);\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn:active {\n' +
|
||
' transform: scale(0.98);\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn[aria-pressed="true"] {\n' +
|
||
' background: var(--accent-soft);\n' +
|
||
' border-color: rgba(232, 165, 75, 0.55);\n' +
|
||
' box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);\n' +
|
||
' }\n' +
|
||
' .game-meta-item--likes:has(.game-meta-like-btn[aria-pressed="true"]) {\n' +
|
||
' border-color: rgba(232, 165, 75, 0.38);\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn__heart {\n' +
|
||
' width: 1.15rem;\n' +
|
||
' height: 1.15rem;\n' +
|
||
' flex-shrink: 0;\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn__heart path {\n' +
|
||
' fill: none;\n' +
|
||
' stroke: var(--accent);\n' +
|
||
' stroke-width: 2;\n' +
|
||
' stroke-linecap: round;\n' +
|
||
' stroke-linejoin: round;\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn[aria-pressed="true"] .game-meta-like-btn__heart path {\n' +
|
||
' fill: var(--accent);\n' +
|
||
' stroke: none;\n' +
|
||
' }\n' +
|
||
' .game-meta-like-btn__count {\n' +
|
||
' font-size: 1.0625rem;\n' +
|
||
' font-weight: 800;\n' +
|
||
' letter-spacing: -0.03em;\n' +
|
||
' font-variant-numeric: tabular-nums;\n' +
|
||
' color: var(--accent);\n' +
|
||
' }\n' +
|
||
' html[data-theme="dark"] .game-meta-like-btn[aria-pressed="true"] .game-meta-like-btn__'... 77596 more characters
|
||
|
||
code: 'ERR_ASSERTION'
|
||
name: 'AssertionError'
|
||
expected:
|
||
actual: |-
|
||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||
<%@ page import="org.springframework.web.util.HtmlUtils" %>
|
||
<%
|
||
boolean owner = Boolean.TRUE.equals(request.getAttribute("owner"));
|
||
String ctx = request.getContextPath();
|
||
String gameIdValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("gameId")));
|
||
String gameNameValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("gameName")));
|
||
String creatorValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("creator")));
|
||
String likeCountFormattedValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("likeCountFormatted")));
|
||
String creatorNoteValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("creatorNote")));
|
||
String gitUrlValue = HtmlUtils.htmlEscape(String.valueOf(request.getAttribute("gitUrl")));
|
||
String webglFrameSrc = String.valueOf(request.getAttribute("webglFrameSrc"));
|
||
if (webglFrameSrc == null || webglFrameSrc.isBlank() || "null".equals(webglFrameSrc)) {
|
||
webglFrameSrc = String.valueOf(request.getAttribute("webglUrl"));
|
||
}
|
||
if (webglFrameSrc == null || webglFrameSrc.isBlank() || "null".equals(webglFrameSrc)) {
|
||
webglFrameSrc = "";
|
||
} else if (webglFrameSrc.startsWith("/")) {
|
||
webglFrameSrc = ctx + webglFrameSrc;
|
||
}
|
||
String webglFrameSrcValue = HtmlUtils.htmlEscape(webglFrameSrc);
|
||
%>
|
||
<!DOCTYPE html>
|
||
<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><%= gameNameValue %> — bibimbap</title>
|
||
<style>
|
||
html {
|
||
color-scheme: light;
|
||
--surface: #faf8f5;
|
||
--card-bg: #fff;
|
||
--text: #1a1a1a;
|
||
--text-muted: #5c5c5c;
|
||
--accent: #e8a54b;
|
||
--accent-soft: rgba(232, 165, 75, 0.14);
|
||
--border: rgba(0, 0, 0, 0.08);
|
||
--webgl-bg: #0d0d0d;
|
||
--panel-shadow: 0 4px 24px rgba(26, 26, 26, 0.06);
|
||
--radius: 16px;
|
||
}
|
||
html[data-theme="dark"] {
|
||
color-scheme: dark;
|
||
--surface: #121212;
|
||
--card-bg: #1e1e1e;
|
||
--text: #ece8e1;
|
||
--text-muted: #a39e96;
|
||
--border: rgba(255, 255, 255, 0.1);
|
||
--accent-soft: rgba(232, 165, 75, 0.12);
|
||
--webgl-bg: #080808;
|
||
--panel-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
||
}
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
}
|
||
html[data-theme="dark"] body {
|
||
background: linear-gradient(165deg, #1c1a18 0%, var(--surface) 38%, #0e0e0e 100%);
|
||
background-attachment: fixed;
|
||
}
|
||
html:not([data-theme="dark"]) body {
|
||
background: linear-gradient(165deg, #fff9f0 0%, var(--surface) 45%, #f3ede6 100%);
|
||
background-attachment: fixed;
|
||
}
|
||
.game-page {
|
||
max-width: 56rem;
|
||
margin: 0 auto;
|
||
padding: 1.25rem max(1rem, env(safe-area-inset-left)) 2.75rem max(1rem, env(safe-area-inset-right));
|
||
}
|
||
.game-back {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
margin-bottom: 1.25rem;
|
||
padding: 0.5rem 0.95rem 0.5rem 0.75rem;
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
text-decoration: none;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||
transition: color 0.2s ease, border-color 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
|
||
}
|
||
.game-back svg {
|
||
width: 1rem;
|
||
height: 1rem;
|
||
flex-shrink: 0;
|
||
opacity: 0.85;
|
||
}
|
||
.game-back:hover {
|
||
color: var(--accent);
|
||
border-color: rgba(232, 165, 75, 0.45);
|
||
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;
|
||
padding: 1.5rem 1.35rem 1.6rem;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--panel-shadow);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.game-info-card::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--accent), rgba(232, 165, 75, 0.25), transparent);
|
||
opacity: 0.9;
|
||
}
|
||
.game-info-card h1 {
|
||
margin: 0 0 1.25rem;
|
||
padding-bottom: 0.85rem;
|
||
font-size: clamp(1.5rem, 4.5vw, 1.85rem);
|
||
font-weight: 800;
|
||
letter-spacing: -0.04em;
|
||
line-height: 1.2;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.game-meta-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 0.65rem;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.game-meta-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
.game-meta-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.35rem;
|
||
padding: 0.85rem 0.9rem;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
.game-meta-item:hover {
|
||
border-color: rgba(232, 165, 75, 0.25);
|
||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
||
}
|
||
html[data-theme="dark"] .game-meta-item:hover {
|
||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
|
||
}
|
||
.game-meta-item__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
}
|
||
.game-meta-item__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 1.75rem;
|
||
height: 1.75rem;
|
||
border-radius: 8px;
|
||
background: var(--accent-soft);
|
||
color: var(--accent);
|
||
flex-shrink: 0;
|
||
}
|
||
.game-meta-item__icon svg {
|
||
width: 0.95rem;
|
||
height: 0.95rem;
|
||
}
|
||
.game-meta-item__label {
|
||
font-size: 0.6875rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-meta-item__value {
|
||
font-size: 0.9375rem;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
color: var(--text);
|
||
word-break: break-word;
|
||
}
|
||
.game-meta-item--likes {
|
||
padding: 0.65rem 0.75rem 0.75rem;
|
||
}
|
||
.game-meta-like-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.45rem;
|
||
width: 100%;
|
||
margin: 0;
|
||
margin-top: 0.1rem;
|
||
padding: 0.55rem 0.7rem;
|
||
font: inherit;
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
background: var(--card-bg);
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.12s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||
}
|
||
.game-meta-like-btn:hover {
|
||
border-color: rgba(232, 165, 75, 0.45);
|
||
box-shadow: 0 3px 12px rgba(232, 165, 75, 0.12);
|
||
}
|
||
.game-meta-like-btn:active {
|
||
transform: scale(0.98);
|
||
}
|
||
.game-meta-like-btn[aria-pressed="true"] {
|
||
background: var(--accent-soft);
|
||
border-color: rgba(232, 165, 75, 0.55);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||
}
|
||
.game-meta-item--likes:has(.game-meta-like-btn[aria-pressed="true"]) {
|
||
border-color: rgba(232, 165, 75, 0.38);
|
||
}
|
||
.game-meta-like-btn__heart {
|
||
width: 1.15rem;
|
||
height: 1.15rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.game-meta-like-btn__heart path {
|
||
fill: none;
|
||
stroke: var(--accent);
|
||
stroke-width: 2;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
}
|
||
.game-meta-like-btn[aria-pressed="true"] .game-meta-like-btn__heart path {
|
||
fill: var(--accent);
|
||
stroke: none;
|
||
}
|
||
.game-meta-like-btn__count {
|
||
font-size: 1.0625rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.03em;
|
||
font-variant-numeric: tabular-nums;
|
||
color: var(--accent);
|
||
}
|
||
html[data-theme="dark"] .game-meta-like-btn[aria-pressed="true"] .game-meta-like-btn__count {
|
||
color: #f0c47a;
|
||
}
|
||
|
||
.game-webgl {
|
||
margin-bottom: 1.75rem;
|
||
}
|
||
.game-webgl__head {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.5rem 0.75rem;
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
.game-webgl__head h2 {
|
||
margin: 0;
|
||
font-size: 1.05rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.03em;
|
||
}
|
||
.game-webgl__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 2rem;
|
||
height: 2rem;
|
||
border-radius: 10px;
|
||
background: var(--accent-soft);
|
||
color: var(--accent);
|
||
}
|
||
.game-webgl__icon svg {
|
||
width: 1.1rem;
|
||
height: 1.1rem;
|
||
}
|
||
.game-webgl__badge {
|
||
margin-left: auto;
|
||
padding: 0.2rem 0.55rem;
|
||
font-size: 0.625rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
border: 1px solid rgba(232, 165, 75, 0.35);
|
||
border-radius: 6px;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.game-webgl__badge {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
.game-webgl__shell {
|
||
position: relative;
|
||
padding: 3px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(135deg, rgba(232, 165, 75, 0.45) 0%, rgba(232, 165, 75, 0.08) 50%, rgba(255, 255, 255, 0.06) 100%);
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||
}
|
||
html:not([data-theme="dark"]) .game-webgl__shell {
|
||
box-shadow: 0 12px 36px rgba(26, 26, 26, 0.12);
|
||
}
|
||
.game-webgl__frame-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 16 / 9;
|
||
max-height: min(70vh, 520px);
|
||
background: var(--webgl-bg);
|
||
border-radius: 15px;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(0, 0, 0, 0.35);
|
||
}
|
||
html:not([data-theme="dark"]) .game-webgl__frame-wrap {
|
||
border-color: rgba(0, 0, 0, 0.12);
|
||
}
|
||
.game-webgl__frame-wrap iframe {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
}
|
||
/* 제작자 한마디 ~ 덧글: 공통 패널 */
|
||
.game-community {
|
||
margin-top: 2rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1.25rem;
|
||
}
|
||
.game-panel {
|
||
position: relative;
|
||
padding: 1.35rem 1.35rem 1.5rem;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--panel-shadow);
|
||
overflow: hidden;
|
||
}
|
||
.game-panel::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 4px;
|
||
background: linear-gradient(180deg, var(--accent) 0%, rgba(232, 165, 75, 0.35) 100%);
|
||
border-radius: 4px 0 0 4px;
|
||
}
|
||
.game-panel__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.game-panel__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 2.25rem;
|
||
height: 2.25rem;
|
||
border-radius: 10px;
|
||
background: var(--accent-soft);
|
||
color: var(--accent);
|
||
flex-shrink: 0;
|
||
}
|
||
.game-panel__icon svg {
|
||
width: 1.15rem;
|
||
height: 1.15rem;
|
||
}
|
||
.game-panel__title {
|
||
margin: 0;
|
||
font-size: 0.9375rem;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
color: var(--text);
|
||
}
|
||
.game-panel__subtitle {
|
||
margin: 0.15rem 0 0;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.game-panel--creator .game-panel__quote {
|
||
margin: 0;
|
||
padding: 1rem 1rem 1rem 1.15rem;
|
||
font-size: 0.9375rem;
|
||
line-height: 1.75;
|
||
font-style: normal;
|
||
color: var(--text);
|
||
background: linear-gradient(135deg, var(--surface) 0%, transparent 55%);
|
||
border-radius: 12px;
|
||
border-left: 3px solid var(--accent);
|
||
}
|
||
.game-panel__git-wrap {
|
||
margin-top: 1.15rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px dashed var(--border);
|
||
}
|
||
.game-panel__git {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.55rem 1rem;
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||
word-break: break-all;
|
||
}
|
||
.game-panel__git svg {
|
||
flex-shrink: 0;
|
||
width: 1.1rem;
|
||
height: 1.1rem;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-panel__git:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
}
|
||
.game-panel__git:hover svg {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.game-comments__composer {
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.game-comments__label {
|
||
display: block;
|
||
margin-bottom: 0.45rem;
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-comments__form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.65rem;
|
||
}
|
||
.game-comments__form textarea {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
min-height: 6.5rem;
|
||
padding: 0.85rem 1rem;
|
||
font-size: 0.875rem;
|
||
font-family: inherit;
|
||
line-height: 1.55;
|
||
color: var(--text);
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
resize: vertical;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
.game-comments__form textarea::placeholder {
|
||
color: var(--text-muted);
|
||
opacity: 0.85;
|
||
}
|
||
.game-comments__form textarea:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 4px rgba(232, 165, 75, 0.18);
|
||
}
|
||
.game-comments__actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.5rem;
|
||
}
|
||
.game-comments__form button[type="submit"] {
|
||
padding: 0.55rem 1.5rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 700;
|
||
color: #1a1a1a;
|
||
background: linear-gradient(180deg, #f0c978 0%, var(--accent) 100%);
|
||
border: none;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
box-shadow: 0 2px 8px rgba(232, 165, 75, 0.35);
|
||
transition: transform 0.15s ease, box-shadow 0.2s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
.game-comments__form button[type="submit"]:hover {
|
||
box-shadow: 0 4px 14px rgba(232, 165, 75, 0.45);
|
||
}
|
||
.game-comments__form button[type="submit"]:active {
|
||
transform: scale(0.98);
|
||
}
|
||
.game-comments__hint {
|
||
font-size: 0.6875rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.game-comments__list {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.85rem;
|
||
}
|
||
.game-comments__empty {
|
||
margin: 0;
|
||
padding: 1.5rem 1rem;
|
||
font-size: 0.875rem;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
background: var(--surface);
|
||
border-radius: 12px;
|
||
border: 1px dashed var(--border);
|
||
}
|
||
.game-comments__item {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
align-items: flex-start;
|
||
padding: 0.85rem 1rem;
|
||
font-size: 0.875rem;
|
||
line-height: 1.55;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
.game-comments__item:hover {
|
||
border-color: rgba(232, 165, 75, 0.2);
|
||
}
|
||
.game-comments__avatar {
|
||
flex-shrink: 0;
|
||
width: 2.25rem;
|
||
height: 2.25rem;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.75rem;
|
||
font-weight: 800;
|
||
color: var(--accent);
|
||
background: var(--accent-soft);
|
||
border: 1px solid rgba(232, 165, 75, 0.25);
|
||
}
|
||
.game-comments__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.game-comments__meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 0.35rem 0.65rem;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
.game-comments__nick {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
color: var(--text);
|
||
}
|
||
.game-comments__meta time {
|
||
font-size: 0.6875rem;
|
||
font-weight: 500;
|
||
letter-spacing: 0.02em;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-comments__item p {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
color: var(--text);
|
||
}
|
||
.game-comments__footer {
|
||
margin-top: 0.5rem;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
.game-comments__delete {
|
||
padding: 0.25rem 0.6rem;
|
||
font-size: 0.6875rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
||
}
|
||
.game-comments__delete:hover {
|
||
color: #b33;
|
||
border-color: rgba(180, 50, 50, 0.25);
|
||
background: rgba(180, 50, 50, 0.06);
|
||
}
|
||
.sr-only {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
margin: -1px;
|
||
overflow: hidden;
|
||
clip: rect(0, 0, 0, 0);
|
||
border: 0;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.game-panel {
|
||
padding: 1.15rem 1.1rem 1.35rem;
|
||
}
|
||
.game-comments__actions {
|
||
flex-direction: column-reverse;
|
||
align-items: stretch;
|
||
}
|
||
.game-comments__form button[type="submit"] {
|
||
width: 100%;
|
||
}
|
||
.game-comments__hint {
|
||
text-align: center;
|
||
}
|
||
.game-reviews__form-buttons {
|
||
width: 100%;
|
||
flex-direction: column-reverse;
|
||
}
|
||
.game-reviews__cancel,
|
||
.game-reviews__form-buttons #game-review-submit {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* ── 로그인 게이트 (덧글/리뷰 공통) ───────────────────── */
|
||
.game-comments__login-gate {
|
||
margin: 0 0 0.25rem;
|
||
padding: 1.1rem 1rem;
|
||
font-size: 0.875rem;
|
||
text-align: center;
|
||
color: var(--text-muted);
|
||
background: var(--surface);
|
||
border: 1px dashed var(--border);
|
||
border-radius: 12px;
|
||
}
|
||
.game-comments__login-gate a {
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
text-decoration: none;
|
||
border-bottom: 1px solid rgba(232, 165, 75, 0.4);
|
||
}
|
||
.game-comments__login-gate a:hover {
|
||
border-bottom-color: var(--accent);
|
||
}
|
||
|
||
/* ── 별점 위젯 ───────────────────────────────────────── */
|
||
.game-stars {
|
||
display: inline-flex;
|
||
gap: 0.15rem;
|
||
line-height: 1;
|
||
}
|
||
.game-stars__btn {
|
||
padding: 0.1rem;
|
||
font-size: 1.6rem;
|
||
line-height: 1;
|
||
color: var(--border);
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
transition: color 0.12s ease, transform 0.12s ease;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
.game-stars__btn:hover {
|
||
transform: scale(1.12);
|
||
}
|
||
.game-stars__btn.is-on {
|
||
color: var(--accent);
|
||
text-shadow: 0 1px 4px rgba(232, 165, 75, 0.45);
|
||
}
|
||
.game-stars__btn:active {
|
||
transform: scale(0.88);
|
||
}
|
||
.game-stars--input:focus-within .game-stars__btn:focus-visible {
|
||
outline: 2px solid var(--accent);
|
||
outline-offset: 2px;
|
||
border-radius: 4px;
|
||
}
|
||
/* 읽기 전용 별 표시 (리뷰 카드/요약) */
|
||
.game-stars--display,
|
||
.game-stars--avg {
|
||
gap: 0.05rem;
|
||
font-size: 0.95rem;
|
||
}
|
||
.game-stars--display .game-star,
|
||
.game-stars--avg .game-star {
|
||
color: var(--border);
|
||
}
|
||
.game-stars--display .game-star.is-on,
|
||
.game-stars--avg .game-star.is-on {
|
||
color: var(--accent);
|
||
}
|
||
|
||
/* ── 리뷰 작성/수정 폼 ───────────────────────────────── */
|
||
.game-reviews__composer {
|
||
margin-bottom: 1.25rem;
|
||
padding: 1.1rem 1.15rem 1.2rem;
|
||
background:
|
||
radial-gradient(140% 120% at 100% 0%, rgba(232, 165, 75, 0.16), transparent 60%),
|
||
var(--accent-soft);
|
||
border: 1px solid rgba(232, 165, 75, 0.22);
|
||
border-radius: 14px;
|
||
}
|
||
.game-reviews__rating-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.85rem;
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
.game-reviews__rating-row .game-comments__label {
|
||
margin-bottom: 0;
|
||
}
|
||
.game-reviews__form-buttons {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
}
|
||
.game-reviews__cancel {
|
||
padding: 0.55rem 1.1rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: border-color 0.15s ease, color 0.15s ease;
|
||
}
|
||
.game-reviews__cancel:hover {
|
||
color: var(--text);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
/* ── 리뷰 요약 (평균 별점) ───────────────────────────── */
|
||
.game-reviews__summary {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
.game-reviews__avg {
|
||
font-size: 1.35rem;
|
||
font-weight: 800;
|
||
letter-spacing: -0.02em;
|
||
color: var(--accent);
|
||
}
|
||
.game-reviews__count {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ── 리뷰 목록 (덧글 카드 패턴 확장) ─────────────────── */
|
||
.game-reviews__list {
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.85rem;
|
||
}
|
||
.game-reviews__item {
|
||
padding: 0.95rem 1.1rem;
|
||
font-size: 0.875rem;
|
||
line-height: 1.55;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
.game-reviews__item.is-mine {
|
||
border-color: rgba(232, 165, 75, 0.4);
|
||
background: var(--accent-soft);
|
||
}
|
||
.game-reviews__item-head {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.4rem 0.65rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.game-reviews__nick {
|
||
font-size: 0.8125rem;
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
color: var(--text);
|
||
}
|
||
.game-reviews__mine-badge {
|
||
padding: 0.05rem 0.45rem;
|
||
font-size: 0.625rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
background: rgba(232, 165, 75, 0.16);
|
||
border-radius: 999px;
|
||
}
|
||
.game-reviews__badges {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.3rem;
|
||
}
|
||
.game-reviews__badge {
|
||
padding: 0.05rem 0.45rem;
|
||
font-size: 0.625rem;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
background: var(--accent-soft);
|
||
border-radius: 999px;
|
||
}
|
||
.game-reviews__badge--reviewer {
|
||
color: #2563eb;
|
||
background: rgba(37, 99, 235, 0.14);
|
||
}
|
||
.game-reviews__badge--technician {
|
||
color: #16a34a;
|
||
background: rgba(22, 163, 74, 0.14);
|
||
}
|
||
.game-reviews__edited {
|
||
font-size: 0.6875rem;
|
||
font-weight: 500;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-reviews__time {
|
||
margin-left: auto;
|
||
font-size: 0.6875rem;
|
||
font-weight: 500;
|
||
letter-spacing: 0.02em;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-reviews__body {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
color: var(--text);
|
||
}
|
||
.game-reviews__footer {
|
||
margin-top: 0.6rem;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 0.4rem;
|
||
}
|
||
.game-reviews__action {
|
||
padding: 0.25rem 0.6rem;
|
||
font-size: 0.6875rem;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
||
}
|
||
.game-reviews__action--edit:hover {
|
||
color: var(--accent);
|
||
border-color: rgba(232, 165, 75, 0.3);
|
||
background: var(--accent-soft);
|
||
}
|
||
.game-reviews__action--delete:hover {
|
||
color: #b33;
|
||
border-color: rgba(180, 50, 50, 0.25);
|
||
background: rgba(180, 50, 50, 0.06);
|
||
}
|
||
/* 덧글 수정 폼 (인라인) */
|
||
.game-comments__edit-form {
|
||
margin-top: 0.6rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
.game-comments__edit-form textarea {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
min-height: 4.5rem;
|
||
padding: 0.65rem 0.85rem;
|
||
font-size: 0.875rem;
|
||
font-family: inherit;
|
||
line-height: 1.55;
|
||
color: var(--text);
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
resize: vertical;
|
||
}
|
||
.game-comments__edit-form textarea:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.18);
|
||
}
|
||
html[data-theme="dark"] .game-comments__form textarea,
|
||
html[data-theme="dark"] .game-comments__edit-form textarea {
|
||
border-color: rgba(255, 255, 255, 0.26);
|
||
}
|
||
|
||
/* ── 6축 평가 입력 그리드 ─────────────────────────────── */
|
||
.game-reviews__axes {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 0.5rem 1.1rem;
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.game-reviews__axes {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
.game-reviews__axis {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 0.5rem;
|
||
padding: 0.3rem 0.5rem;
|
||
border-radius: 10px;
|
||
transition: background 0.15s ease;
|
||
}
|
||
.game-reviews__axis:hover {
|
||
background: rgba(232, 165, 75, 0.08);
|
||
}
|
||
.game-reviews__axis-label {
|
||
font-size: 0.8125rem;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
.game-stars--axis {
|
||
gap: 0.1rem;
|
||
}
|
||
.game-stars--axis .game-stars__btn {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
/* ── 육각형 레이더 ───────────────────────────────────── */
|
||
.game-reviews__summary-panel {
|
||
margin: 0 0 1.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1.25rem;
|
||
padding: 1rem 1.25rem;
|
||
background:
|
||
radial-gradient(125% 125% at 0% 0%, var(--accent-soft), transparent 62%),
|
||
var(--card-bg);
|
||
border: 1px solid rgba(232, 165, 75, 0.22);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--panel-shadow);
|
||
}
|
||
@media (max-width: 480px) {
|
||
.game-reviews__summary-panel {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 0.85rem;
|
||
}
|
||
.game-reviews__radar { justify-content: center; }
|
||
}
|
||
.game-reviews__radar {
|
||
display: inline-flex;
|
||
flex-shrink: 0;
|
||
gap: 1.75rem;
|
||
}
|
||
.game-reviews__radar svg {
|
||
display: block;
|
||
}
|
||
/* drop-shadow 가 viewBox 밖으로 번질 수 있어 클립 방지 */
|
||
.game-axis-radar {
|
||
overflow: visible;
|
||
}
|
||
.game-reviews__radar .game-axis-grid {
|
||
fill: none;
|
||
stroke: var(--border);
|
||
stroke-width: 1;
|
||
}
|
||
.game-reviews__radar .game-axis-grid--outer {
|
||
stroke: rgba(232, 165, 75, 0.4);
|
||
stroke-width: 1.25;
|
||
}
|
||
.game-reviews__radar .game-axis-line {
|
||
stroke: var(--border);
|
||
stroke-width: 1;
|
||
stroke-dasharray: 2 3;
|
||
}
|
||
/* fill 은 JS 가 url(#gradient) 프레젠테이션 속성으로 지정 — 여기서 fill 지정 금지(우선순위 역전) */
|
||
.game-reviews__radar .game-axis-poly {
|
||
stroke: var(--accent);
|
||
stroke-width: 1.75;
|
||
stroke-linejoin: round;
|
||
filter: drop-shadow(0 2px 7px rgba(232, 165, 75, 0.38));
|
||
transform-box: fill-box;
|
||
transform-origin: center;
|
||
animation: game-radar-pop 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||
}
|
||
.game-reviews__radar .game-axis-dot {
|
||
fill: var(--accent);
|
||
stroke: var(--card-bg);
|
||
stroke-width: 1.5;
|
||
animation: game-radar-fade 0.7s ease both;
|
||
}
|
||
.game-reviews__radar .game-axis-axislabel {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
fill: var(--text-muted);
|
||
}
|
||
.game-reviews__radar .game-axis-scorelabel {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
fill: var(--accent);
|
||
font-variant-numeric: tabular-nums;
|
||
animation: game-radar-fade 0.7s ease both;
|
||
}
|
||
@keyframes game-radar-pop {
|
||
from { opacity: 0; transform: scale(0.55); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
@keyframes game-radar-fade {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
.game-reviews__radar-empty {
|
||
font-size: 0.75rem;
|
||
color: var(--text-muted);
|
||
}
|
||
.game-reviews__title-meter {
|
||
display: inline-flex;
|
||
align-items: flex-end;
|
||
gap: 0.15rem;
|
||
height: 0.95rem;
|
||
}
|
||
.game-reviews__title-meter .stick {
|
||
position: relative;
|
||
width: 0.2rem;
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
background: var(--border);
|
||
overflow: hidden;
|
||
}
|
||
.game-reviews__title-meter .stick span {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
border-radius: 2px;
|
||
background: linear-gradient(180deg, #f0c978, var(--accent));
|
||
animation: game-title-meter-grow 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||
transform-origin: bottom;
|
||
}
|
||
@keyframes game-title-meter-grow {
|
||
from { transform: scaleY(0); }
|
||
to { transform: scaleY(1); }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.game-reviews__radar .game-axis-poly,
|
||
.game-reviews__radar .game-axis-dot,
|
||
.game-reviews__title-meter .stick span { animation: none; }
|
||
}
|
||
|
||
/* ── sort 토글 / 더보기 ──────────────────────────────── */
|
||
.game-comments__toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
.game-comments__sort {
|
||
display: inline-flex;
|
||
gap: 0.25rem;
|
||
padding: 0.2rem;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
}
|
||
.game-comments__sort-btn {
|
||
padding: 0.3rem 0.7rem;
|
||
font-size: 0.6875rem;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
transition: color 0.15s ease, background 0.15s ease;
|
||
}
|
||
.game-comments__sort-btn.is-active {
|
||
color: #1a1a1a;
|
||
background: linear-gradient(180deg, #f0c978, var(--accent));
|
||
box-shadow: 0 1px 5px rgba(232, 165, 75, 0.4);
|
||
}
|
||
html[data-theme="dark"] .game-comments__sort-btn.is-active {
|
||
color: #1a1a1a;
|
||
}
|
||
.game-comments__more {
|
||
margin-top: 1rem;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.game-comments__more-btn {
|
||
padding: 0.5rem 1.4rem;
|
||
font-size: 0.8125rem;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
transition: border-color 0.15s ease, color 0.15s ease;
|
||
}
|
||
.game-comments__more-btn:hover {
|
||
border-color: var(--accent);
|
||
color: var(--accent);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 5px 16px rgba(232, 165, 75, 0.16);
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.game-comments__more-btn:hover { transform: none; }
|
||
}
|
||
|
||
/* ── 제출 잠금/로딩 ──────────────────────────────────── */
|
||
.game-comments__hint.is-warn {
|
||
color: #b33;
|
||
font-weight: 700;
|
||
}
|
||
button[aria-busy="true"] {
|
||
opacity: 0.65;
|
||
cursor: progress;
|
||
}
|
||
.game-comments__edited {
|
||
margin-left: 0.35rem;
|
||
font-size: 0.6875rem;
|
||
font-weight: 500;
|
||
color: var(--text-muted);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<jsp:include page="/WEB-INF/views/header.jsp"/>
|
||
<main class="game-page">
|
||
<div class="game-topbar">
|
||
<a class="game-back" href="<%= ctx %>/">
|
||
<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="<%= ctx %>/game/<%= gameIdValue %>/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"><%= gameNameValue %></h1>
|
||
<div class="game-meta-grid">
|
||
<div class="game-meta-item">
|
||
<div class="game-meta-item__row">
|
||
<span class="game-meta-item__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 9h16M4 15h16M10 3L8 21M16 3l-2 18"/></svg>
|
||
</span>
|
||
<span class="game-meta-item__label">번호</span>
|
||
</div>
|
||
<span class="game-meta-item__value">#<%= gameIdValue %></span>
|
||
</div>
|
||
<div class="game-meta-item">
|
||
<div class="game-meta-item__row">
|
||
<span class="game-meta-item__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||
</span>
|
||
<span class="game-meta-item__label">제작자</span>
|
||
</div>
|
||
<span class="game-meta-item__value"><%= creatorValue %></span>
|
||
</div>
|
||
<div class="game-meta-item game-meta-item--likes">
|
||
<div class="game-meta-item__row">
|
||
<span class="game-meta-item__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
|
||
</span>
|
||
<span class="game-meta-item__label">좋아요</span>
|
||
</div>
|
||
<button type="button" id="game-like-btn" class="game-meta-like-btn" aria-pressed="false" aria-label="좋아요">
|
||
<svg class="game-meta-like-btn__heart" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
|
||
</svg>
|
||
<span id="game-like-count" class="game-meta-like-btn__count"><%= likeCountFormattedValue %></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="game-webgl" aria-labelledby="webgl-heading">
|
||
<div class="game-webgl__head">
|
||
<span class="game-webgl__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||
</span>
|
||
<h2 id="webgl-heading">플레이</h2>
|
||
<span class="game-webgl__badge">WebGL</span>
|
||
</div>
|
||
<div class="game-webgl__shell">
|
||
<div class="game-webgl__frame-wrap">
|
||
<iframe title="<%= gameNameValue %> WebGL"
|
||
src="<%= webglFrameSrcValue %>"
|
||
sandbox="allow-scripts allow-same-origin allow-pointer-lock"
|
||
allow="fullscreen; autoplay; xr-spatial-tracking"
|
||
loading="eager"
|
||
referrerpolicy="same-origin"></iframe>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="game-community">
|
||
<section class="game-panel game-panel--creator" aria-labelledby="creator-heading">
|
||
<div class="game-panel__head">
|
||
<span class="game-panel__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/></svg>
|
||
</span>
|
||
<div>
|
||
<h2 id="creator-heading" class="game-panel__title">제작자의 한마디</h2>
|
||
<p class="game-panel__subtitle">이 게임을 만들며 전하고 싶었던 이야기예요.</p>
|
||
</div>
|
||
</div>
|
||
<blockquote class="game-panel__quote"><%= creatorNoteValue %></blockquote>
|
||
<div class="game-panel__git-wrap">
|
||
<a class="game-panel__git" href="<%= gitUrlValue %>" target="_blank" rel="noopener noreferrer">
|
||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||
소스 코드 · GitHub
|
||
</a>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="game-panel game-panel--reviews" aria-labelledby="reviews-heading">
|
||
<div class="game-panel__head">
|
||
<span class="game-panel__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l2.9 6.2 6.8.9-5 4.7 1.3 6.7L12 17.8 5.9 21.2 7.2 14.5l-5-4.7 6.8-.9z"/></svg>
|
||
</span>
|
||
<div>
|
||
<h2 id="reviews-heading" class="game-panel__title">리뷰</h2>
|
||
<p class="game-panel__subtitle">별점과 함께 플레이 평가를 남겨 주세요. 게임당 한 번 작성할 수 있어요.</p>
|
||
</div>
|
||
<div class="game-reviews__summary" id="game-reviews-summary" hidden>
|
||
<span class="game-reviews__avg" id="game-reviews-avg">–</span>
|
||
<div class="game-stars game-stars--avg" id="game-reviews-avg-stars" aria-hidden="true"></div>
|
||
<span class="game-reviews__count" id="game-reviews-count"></span>
|
||
</div>
|
||
</div>
|
||
<div class="game-reviews__summary-panel" id="game-reviews-summary-panel" hidden>
|
||
<div class="game-reviews__radar" id="game-reviews-summary-radar"></div>
|
||
</div>
|
||
<div class="game-reviews__composer" id="game-review-composer" hidden>
|
||
<form id="game-review-form" class="game-comments__form" novalidate>
|
||
<div class="game-reviews__axes" id="game-review-axes" aria-label="6축 평가"></div>
|
||
<div class="game-reviews__rating-row">
|
||
<span class="game-comments__label" id="game-review-stars-label">종합 별점 (선택)</span>
|
||
<div class="game-stars game-stars--input" id="game-review-stars" role="radiogroup" aria-labelledby="game-review-stars-label"></div>
|
||
</div>
|
||
<label class="game-comments__label" for="game-review-input">평가</label>
|
||
<textarea id="game-review-input" name="body" rows="4" maxlength="1000" placeholder="이 게임을 어떻게 즐기셨나요? (최소 10자)"></textarea>
|
||
<div class="game-comments__actions">
|
||
<span class="game-comments__hint" id="game-review-hint"><span id="game-review-counter">0/1000</span> · 6축은 필수, 종합 별점은 선택이에요</span>
|
||
<div class="game-reviews__form-buttons">
|
||
<button type="button" class="game-reviews__cancel" id="game-review-cancel" hidden>취소</button>
|
||
<button type="submit" id="game-review-submit">리뷰 등록</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<p class="game-comments__login-gate" id="game-review-login-gate" hidden>리뷰를 남기려면 <a href="<%= ctx %>/login">로그인</a>이 필요해요.</p>
|
||
<div class="game-comments__toolbar" id="game-reviews-toolbar" hidden>
|
||
<div class="game-comments__sort" role="group" aria-label="리뷰 정렬">
|
||
<button type="button" class="game-comments__sort-btn is-active" data-sort="newest" aria-pressed="true">최신순</button>
|
||
<button type="button" class="game-comments__sort-btn" data-sort="rating_desc" aria-pressed="false">별점 높은순</button>
|
||
<button type="button" class="game-comments__sort-btn" data-sort="rating_asc" aria-pressed="false">별점 낮은순</button>
|
||
</div>
|
||
</div>
|
||
<p id="game-reviews-empty" class="game-comments__empty" hidden>아직 리뷰가 없습니다.<br>첫 번째 리뷰를 남겨 보세요.</p>
|
||
<ul id="game-review-list" class="game-reviews__list" aria-labelledby="reviews-heading"></ul>
|
||
<div class="game-comments__more" hidden>
|
||
<button type="button" class="game-comments__more-btn" id="game-reviews-more">더보기</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="game-panel game-panel--comments" aria-labelledby="comments-heading">
|
||
<div class="game-panel__head">
|
||
<span class="game-panel__icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||
</span>
|
||
<div>
|
||
<h2 id="comments-heading" class="game-panel__title">덧글</h2>
|
||
<p class="game-panel__subtitle">플레이 소감이나 버그 제보를 남겨 주세요.</p>
|
||
</div>
|
||
</div>
|
||
<div class="game-comments__composer" id="game-comment-composer" hidden>
|
||
<form id="game-comment-form" class="game-comments__form" novalidate>
|
||
<label class="game-comments__label" for="game-comment-input">내용</label>
|
||
<textarea id="game-comment-input" name="comment" rows="4" maxlength="200" placeholder="여기에 덧글을 작성해 주세요." required></textarea>
|
||
<div class="game-comments__actions">
|
||
<span class="game-comments__hint"><span id="game-comment-counter">0/200</span></span>
|
||
<button type="submit" id="game-comment-submit">덧글 등록</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<p class="game-comments__login-gate" id="game-comment-login-gate" hidden>덧글을 남기려면 <a href="<%= ctx %>/login">로그인</a>이 필요해요.</p>
|
||
<div class="game-comments__toolbar" id="game-comments-toolbar" hidden>
|
||
<div class="game-comments__sort" role="group" aria-label="덧글 정렬">
|
||
<button type="button" class="game-comments__sort-btn is-active" data-sort="oldest" aria-pressed="true">등록순</button>
|
||
<button type="button" class="game-comments__sort-btn" data-sort="newest" aria-pressed="false">최신순</button>
|
||
</div>
|
||
</div>
|
||
<p id="game-comments-empty" class="game-comments__empty" hidden>아직 덧글이 없습니다.<br>첫 번째 덧글을 남겨 보세요.</p>
|
||
<ul id="game-comment-list" class="game-comments__list" aria-labelledby="comments-heading"></ul>
|
||
<div class="game-comments__more" hidden>
|
||
<button type="button" class="game-comments__more-btn" id="game-comments-more">더보기</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
<jsp:include page="/WEB-INF/views/footer.jsp"/>
|
||
|
||
<script>
|
||
(function () {
|
||
var ctx = '${pageContext.request.contextPath}';
|
||
var gameId = ${gameId};
|
||
var liked = ${empty liked ? 'false' : liked};
|
||
var viewerId = ${empty currentUserId ? 'null' : currentUserId};
|
||
var viewerRole = '${empty userRole ? "" : userRole}';
|
||
|
||
function formatCount(n) {
|
||
return n.toLocaleString('ko-KR');
|
||
}
|
||
|
||
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: window.BibimbapCsrf ? window.BibimbapCsrf.headers({
|
||
'Accept': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}) : {
|
||
'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 applyLikeUi() {
|
||
likeBtn.setAttribute('aria-pressed', liked ? 'true' : 'false');
|
||
likeBtn.setAttribute('aria-label', liked ? '좋아요 취소' : '좋아요');
|
||
}
|
||
|
||
var pending = false;
|
||
|
||
likeBtn.addEventListener('click', function () {
|
||
if (pending) return;
|
||
pending = true;
|
||
fetch(ctx + '/game/' + encodeURIComponent(gid) + '/like', {
|
||
method: 'POST',
|
||
headers: window.BibimbapCsrf ? window.BibimbapCsrf.headers({
|
||
'Accept': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}) : {
|
||
'Accept': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
}).then(function (res) {
|
||
if (res.status === 401) {
|
||
if (window.BibimbapModal && typeof window.BibimbapModal.alert === 'function') {
|
||
window.BibimbapModal.alert({ title: '로그인 필요', message: '좋아요는 로그인 후 이용할 수 있습니다.', confirmText: '확인' });
|
||
} else {
|
||
alert('좋아요는 로그인 후 이용할 수 있습니다.');
|
||
}
|
||
return null;
|
||
}
|
||
if (res.status === 403) {
|
||
if (window.BibimbapModal && typeof window.BibimbapModal.alert === 'function') {
|
||
window.BibimbapModal.alert({ title: '요청 실패', message: '요청 보안 토큰이 유효하지 않습니다. 새로고침 후 다시 시도해 주세요.', confirmText: '확인' });
|
||
} else {
|
||
alert('요청 보안 토큰이 유효하지 않습니다.');
|
||
}
|
||
return null;
|
||
}
|
||
if (!res.ok) { return null; }
|
||
return res.json().catch(function () { return null; });
|
||
}).then(function (data) {
|
||
if (data && typeof data.liked !== 'undefined') {
|
||
liked = !!data.liked;
|
||
applyLikeUi();
|
||
if (typeof data.likeCount === 'number') {
|
||
likeCountEl.textContent = formatCount(data.likeCount);
|
||
}
|
||
}
|
||
}).catch(function () {
|
||
// 네트워크 실패 — 상태 원복(현재 liked 유지). 별도 처리 없음.
|
||
}).then(function () {
|
||
pending = false;
|
||
});
|
||
});
|
||
applyLikeUi();
|
||
|
||
// ===== 공통 헬퍼 (서버 연동 덧글/리뷰) =====
|
||
var AXIS_KEYS = ['immersion', 'creativity', 'controls', 'completeness', 'sound', 'visual'];
|
||
var AXIS_LABELS_KO = {
|
||
immersion: '몰입성', creativity: '창의성', controls: '조작성',
|
||
completeness: '완성도', sound: '사운드', visual: '비주얼'
|
||
};
|
||
var SVG_NS = 'http://www.w3.org/2000/svg';
|
||
var labelSeq = 0;
|
||
|
||
function fmtAbsolute(iso) {
|
||
try { return iso ? new Date(iso).toLocaleString('ko-KR') : ''; }
|
||
catch (e) { return ''; }
|
||
}
|
||
// 상대시각: 7일 이내는 "n분/시간/일 전", 초과는 절대 날짜
|
||
function fmtRelative(iso) {
|
||
if (!iso) return '';
|
||
var d = new Date(iso);
|
||
if (isNaN(d.getTime())) return '';
|
||
var diff = Date.now() - d.getTime();
|
||
if (diff < 0) diff = 0;
|
||
var sec = Math.floor(diff / 1000);
|
||
if (sec < 60) return '방금 전';
|
||
var min = Math.floor(sec / 60);
|
||
if (min < 60) return min + '분 전';
|
||
var hr = Math.floor(min / 60);
|
||
if (hr < 24) return hr + '시간 전';
|
||
var day = Math.floor(hr / 24);
|
||
if (day <= 7) return day + '일 전';
|
||
return d.toLocaleDateString('ko-KR');
|
||
}
|
||
// edited 면 "(수정됨)" 뱃지 동반. <time datetime=ISO title=절대시각>
|
||
function buildTimeEl(iso, edited) {
|
||
var t = document.createElement('time');
|
||
t.dateTime = iso || '';
|
||
t.title = fmtAbsolute(iso);
|
||
t.textContent = fmtRelative(iso);
|
||
if (!edited) return t;
|
||
var frag = document.createDocumentFragment();
|
||
frag.appendChild(t);
|
||
var badge = document.createElement('span');
|
||
badge.className = 'game-comments__edited';
|
||
badge.textContent = '(수정됨)';
|
||
frag.appendChild(badge);
|
||
return frag;
|
||
}
|
||
function notifyError(title, message) {
|
||
if (window.BibimbapModal && typeof window.BibimbapModal.alert === 'function') {
|
||
window.BibimbapModal.alert({ title: title, message: message, confirmText: '확인' });
|
||
} else { alert(message); }
|
||
}
|
||
function confirmAction(title, message, okText, onOk) {
|
||
if (window.BibimbapModal && typeof window.BibimbapModal.confirm === 'function') {
|
||
window.BibimbapModal.confirm({ title: title, message: message, confirmText: okText, cancelText: '취소', onConfirm: onOk });
|
||
} else if (confirm(message)) { onOk(); }
|
||
}
|
||
function baseHeaders(extra) {
|
||
var h = { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' };
|
||
if (extra) { Object.keys(extra).forEach(function (k) { h[k] = extra[k]; }); }
|
||
return window.BibimbapCsrf ? window.BibimbapCsrf.headers(h) : h;
|
||
}
|
||
function formHeaders() {
|
||
return baseHeaders({ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' });
|
||
}
|
||
function encodeForm(obj) {
|
||
return Object.keys(obj).map(function (k) {
|
||
return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k] == null ? '' : obj[k]);
|
||
}).join('&');
|
||
}
|
||
function api(url, opts) {
|
||
return fetch(url, opts).then(function (res) {
|
||
return res.json().catch(function () { return {}; }).then(function (data) {
|
||
if (!res.ok) { throw new Error((data && data.message) || ('요청을 처리하지 못했습니다. (' + res.status + ')')); }
|
||
return data;
|
||
});
|
||
});
|
||
}
|
||
// 본인(작성자) 또는 운영자(ADMIN)만 수정/삭제 버튼 노출 — 서버가 최종 권한 재검증
|
||
function canActOn(authorUserId) {
|
||
if (viewerId == null) return false;
|
||
if (authorUserId != null && Number(authorUserId) === Number(viewerId)) return true;
|
||
return viewerRole === 'ADMIN';
|
||
}
|
||
|
||
// 제출 버튼 잠금/로딩 — 연타 차단. lock() 호출 후 unlock() 으로 해제
|
||
function lockButton(btn) {
|
||
if (!btn) return function () {};
|
||
var origText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.setAttribute('aria-busy', 'true');
|
||
btn.textContent = '처리 중…';
|
||
return function () {
|
||
btn.disabled = false;
|
||
btn.removeAttribute('aria-busy');
|
||
btn.textContent = origText;
|
||
};
|
||
}
|
||
|
||
// ===== 육각형 레이더 SVG (요약/카드 공용) =====
|
||
// scores: AXIS_KEYS 순 6값(1~5, null/0=데이터 없음). cx/cy/R 좌표.
|
||
var hexSeq = 0; // svg 별 그라디언트 id 고유화
|
||
function buildHexRadar(scores, cx, cy, R, withLabels) {
|
||
var canvas = cx * 2; // cx==cy 가정. 요약 200 / 카드 120
|
||
var svg = document.createElementNS(SVG_NS, 'svg');
|
||
svg.setAttribute('viewBox', '0 0 ' + canvas + ' ' + canvas);
|
||
svg.setAttribute('width', String(Math.round(canvas * 0.85)));
|
||
svg.setAttribute('height', String(Math.round(canvas * 0.85)));
|
||
svg.setAttribute('role', 'img');
|
||
svg.setAttribute('class', 'game-axis-radar');
|
||
|
||
var nums = AXIS_KEYS.map(function (k, i) {
|
||
var v = scores ? Number(scores[i]) : 0;
|
||
return (isFinite(v) && v > 0) ? v : 0;
|
||
});
|
||
var hasData = nums.some(function (v) { return v > 0; });
|
||
|
||
function ptOnAxis(i, ratio) {
|
||
var ang = (-90 + 60 * i) * Math.PI / 180;
|
||
return {
|
||
x: cx + R * ratio * Math.cos(ang),
|
||
y: cy + R * ratio * Math.sin(ang)
|
||
};
|
||
}
|
||
function polyPoints(ratioFn) {
|
||
return AXIS_KEYS.map(function (k, i) {
|
||
var p = ptOnAxis(i, ratioFn(i));
|
||
return p.x.toFixed(2) + ',' + p.y.toFixed(2);
|
||
}).join(' ');
|
||
}
|
||
|
||
// 채움용 방사형 앰버 그라디언트 (svg 별 고유 id — 한 페이지 다중 레이더 충돌 방지)
|
||
var gid = 'hexfill-' + (++hexSeq);
|
||
var defs = document.createElementNS(SVG_NS, 'defs');
|
||
var grad = document.createElementNS(SVG_NS, 'radialGradient');
|
||
grad.setAttribute('id', gid);
|
||
grad.setAttribute('cx', '50%');
|
||
grad.setAttribute('cy', '50%');
|
||
grad.setAttribute('r', '62%');
|
||
[['0%', 'rgba(240, 201, 120, 0.55)'], ['100%', 'rgba(232, 165, 75, 0.26)']].forEach(function (s) {
|
||
var st = document.createElementNS(SVG_NS, 'stop');
|
||
st.setAttribute('offset', s[0]);
|
||
st.setAttribute('stop-color', s[1]);
|
||
grad.appendChild(st);
|
||
});
|
||
defs.appendChild(grad);
|
||
svg.appendChild(defs);
|
||
|
||
// 동심 그리드 (5단계 = 1~5점 정수 등급과 1:1 대응, 외곽 강조)
|
||
for (var level = 1; level <= 5; level++) {
|
||
var grid = document.createElementNS(SVG_NS, 'polygon');
|
||
grid.setAttribute('class', 'game-axis-grid' + (level === 5 ? ' game-axis-grid--outer' : ''));
|
||
(function (lv) {
|
||
grid.setAttribute('points', polyPoints(function () { return lv / 5; }));
|
||
})(level);
|
||
svg.appendChild(grid);
|
||
}
|
||
// 축선 + (withLabels 시) 축 이름 라벨 — 육각형만으론 어떤 축인지 식별 불가(§6.6)
|
||
// 6축 순서상 위치 고정: 0=위, 1=우상, 2=우하, 3=아래, 4=좌하, 5=좌상
|
||
var AXIS_LABEL_ANCHOR = ['middle', 'start', 'start', 'middle', 'end', 'end'];
|
||
AXIS_KEYS.forEach(function (k, i) {
|
||
var line = document.createElementNS(SVG_NS, 'line');
|
||
line.setAttribute('class', 'game-axis-line');
|
||
var p = ptOnAxis(i, 1);
|
||
line.setAttribute('x1', String(cx));
|
||
line.setAttribute('y1', String(cy));
|
||
line.setAttribute('x2', p.x.toFixed(2));
|
||
line.setAttribute('y2', p.y.toFixed(2));
|
||
svg.appendChild(line);
|
||
|
||
if (withLabels) {
|
||
var lp = ptOnAxis(i, 1.14);
|
||
var label = document.createElementNS(SVG_NS, 'text');
|
||
label.setAttribute('class', 'game-axis-axislabel');
|
||
label.setAttribute('x', lp.x.toFixed(2));
|
||
label.setAttribute('y', lp.y.toFixed(2));
|
||
label.setAttribute('text-anchor', AXIS_LABEL_ANCHOR[i]);
|
||
label.setAttribute('dominant-baseline', i === 0 ? 'text-after-edge' : (i === 3 ? 'text-before-edge' : 'middle'));
|
||
label.textContent = AXIS_LABELS_KO[k];
|
||
svg.appendChild(label);
|
||
}
|
||
});
|
||
// score 폴리곤 + 꼭지점 점 (데이터 있을 때만)
|
||
if (hasData) {
|
||
var poly = document.createElementNS(SVG_NS, 'polygon');
|
||
poly.setAttribute('class', 'game-axis-poly');
|
||
poly.setAttribute('points', polyPoints(function (i) { return nums[i] / 5; }));
|
||
poly.setAttribute('fill', 'url(#' + gid + ')');
|
||
svg.appendChild(poly);
|
||
AXIS_KEYS.forEach(function (k, i) {
|
||
if (nums[i] <= 0) return;
|
||
var ratio = nums[i] / 5;
|
||
var p = ptOnAxis(i, ratio);
|
||
var dot = document.createElementNS(SVG_NS, 'circle');
|
||
dot.setAttribute('class', 'game-axis-dot');
|
||
dot.setAttribute('cx', p.x.toFixed(2));
|
||
dot.setAttribute('cy', p.y.toFixed(2));
|
||
dot.setAttribute('r', Math.max(1.6, R * 0.05).toFixed(2));
|
||
svg.appendChild(dot);
|
||
|
||
if (withLabels) {
|
||
// 꼭지점에서 살짝 바깥으로 띄워 점 위에 겹치지 않게 배치
|
||
var sp = ptOnAxis(i, ratio + 14 / R);
|
||
var score = document.createElementNS(SVG_NS, 'text');
|
||
score.setAttribute('class', 'game-axis-scorelabel');
|
||
score.setAttribute('x', sp.x.toFixed(2));
|
||
score.setAttribute('y', sp.y.toFixed(2));
|
||
score.setAttribute('text-anchor', AXIS_LABEL_ANCHOR[i]);
|
||
score.setAttribute('dominant-baseline', 'middle');
|
||
score.textContent = String(nums[i]);
|
||
svg.appendChild(score);
|
||
}
|
||
});
|
||
}
|
||
|
||
// a11y aria-label
|
||
if (hasData) {
|
||
svg.setAttribute('aria-label',
|
||
'6축 평가: ' + AXIS_KEYS.map(function (k, i) {
|
||
return AXIS_LABELS_KO[k] + ' ' + nums[i];
|
||
}).join(', '));
|
||
} else {
|
||
svg.setAttribute('aria-label', '6축 평가 없음');
|
||
}
|
||
return { svg: svg, nums: nums, hasData: hasData };
|
||
}
|
||
|
||
// 리뷰 카드 제목행 삽입용 6축 미니 미터바 — axes 없으면 null(§6.6: 그래프는 요약에만, 카드는 이 미터바로 대체)
|
||
function buildTitleMeter(axes) {
|
||
var nums = AXIS_KEYS.map(function (k) {
|
||
var v = axes ? Number(axes[k]) : 0;
|
||
return (isFinite(v) && v > 0) ? v : 0;
|
||
});
|
||
var hasData = nums.some(function (v) { return v > 0; });
|
||
if (!hasData) return null;
|
||
var wrap = document.createElement('div');
|
||
wrap.className = 'game-reviews__title-meter';
|
||
wrap.setAttribute('role', 'img');
|
||
wrap.setAttribute('aria-label', '6축 평가: ' + AXIS_KEYS.map(function (k, i) {
|
||
return AXIS_LABELS_KO[k] + ' ' + nums[i];
|
||
}).join(', '));
|
||
nums.forEach(function (v) {
|
||
var stick = document.createElement('span');
|
||
stick.className = 'stick';
|
||
var fill = document.createElement('span');
|
||
fill.style.height = (v / 5 * 100) + '%';
|
||
stick.appendChild(fill);
|
||
wrap.appendChild(stick);
|
||
});
|
||
return wrap;
|
||
}
|
||
|
||
// ===== 별점 radiogroup 완전 패턴 (C6) =====
|
||
// axisScores: axisKey 별 선택값(0=미선택). buildStarRadioGroup 으로 위젯 생성.
|
||
var axisScores = {};
|
||
function buildStarRadioGroup(axisKey, labelText) {
|
||
var labelId = 'star-label-' + axisKey + '-' + (++labelSeq);
|
||
var group = document.createElement('div');
|
||
group.className = 'game-stars game-stars--input game-stars--axis';
|
||
group.setAttribute('role', 'radiogroup');
|
||
group.setAttribute('aria-labelledby', labelId);
|
||
|
||
var radios = [];
|
||
function paint() {
|
||
var sel = axisScores[axisKey] || 0;
|
||
radios.forEach(function (b, idx) {
|
||
var v = idx + 1;
|
||
b.classList.toggle('is-on', v <= sel);
|
||
b.setAttribute('aria-checked', v === sel ? 'true' : 'false');
|
||
b.tabIndex = (v === sel) ? 0 : -1;
|
||
});
|
||
if (!sel) radios[0].tabIndex = 0; // 미선택 시 첫 radio tabindex=0
|
||
}
|
||
function select(v) {
|
||
axisScores[axisKey] = v;
|
||
paint();
|
||
}
|
||
|
||
for (var i = 1; i <= 5; i++) {
|
||
(function (v) {
|
||
var b = document.createElement('button');
|
||
b.type = 'button';
|
||
b.className = 'game-stars__btn';
|
||
b.setAttribute('role', 'radio');
|
||
b.setAttribute('aria-checked', 'false');
|
||
b.setAttribute('aria-label', labelText + ' ' + v + '점');
|
||
b.tabIndex = -1;
|
||
b.textContent = '★';
|
||
b.addEventListener('click', function () { select(v); b.focus(); });
|
||
b.addEventListener('keydown', function (ev) {
|
||
var cur = axisScores[axisKey] || 0;
|
||
var next = null;
|
||
switch (ev.key) {
|
||
case 'ArrowRight': case 'ArrowUp': next = Math.min(5, (cur || v) + 1); break;
|
||
case 'ArrowLeft': case 'ArrowDown': next = Math.max(1, (cur || v) - 1); break;
|
||
case 'Home': next = 1; break;
|
||
case 'End': next = 5; break;
|
||
case ' ': case 'Spacebar': case 'Enter': next = v; break;
|
||
default: return;
|
||
}
|
||
ev.preventDefault();
|
||
select(next);
|
||
radios[next - 1].focus();
|
||
});
|
||
radios.push(b);
|
||
group.appendChild(b);
|
||
})(i);
|
||
}
|
||
paint();
|
||
return { group: group, labelId: labelId, set: select, paint: paint };
|
||
}
|
||
|
||
// ===== 덧글 (서버 연동) =====
|
||
var cListEl = document.getElementById('game-comment-list');
|
||
var cEmptyEl = document.getElementById('game-comments-empty');
|
||
var cForm = document.getElementById('game-comment-form');
|
||
var cInput = document.getElementById('game-comment-input');
|
||
var cSubmit = document.getElementById('game-comment-submit');
|
||
var cCounter = document.getElementById('game-comment-counter');
|
||
var cComposer = document.getElementById('game-comment-composer');
|
||
var cLoginGate = document.getElementById('game-comment-login-gate');
|
||
var cToolbar = document.getElementById('game-comments-toolbar');
|
||
var cMoreBtn = document.getElementById('game-comments-more');
|
||
var cMoreWrap = cMoreBtn ? cMoreBtn.parentElement : null;
|
||
var commentsUrl = ctx + '/game/' + encodeURIComponent(gid) + '/comments';
|
||
var currentCommentPage = 0;
|
||
var currentCommentSort = 'oldest';
|
||
var COMMENT_MAX = 200;
|
||
|
||
function openCommentEdit(li, body, p, c) {
|
||
if (li.querySelector('.game-comments__edit-form')) return;
|
||
var ef = document.createElement('form');
|
||
ef.className = 'game-comments__edit-form';
|
||
var ta = document.createElement('textarea');
|
||
ta.maxLength = 200;
|
||
ta.value = c.content || '';
|
||
var actions = document.createElement('div');
|
||
actions.className = 'game-comments__actions';
|
||
var hint = document.createElement('span');
|
||
hint.className = 'game-comments__hint';
|
||
hint.textContent = '최대 200자';
|
||
var btnWrap = document.createElement('div');
|
||
btnWrap.className = 'game-reviews__form-buttons';
|
||
var cancel = document.createElement('button');
|
||
cancel.type = 'button';
|
||
cancel.className = 'game-reviews__cancel';
|
||
cancel.textContent = '취소';
|
||
var save = document.createElement('button');
|
||
save.type = 'submit';
|
||
save.textContent = '저장';
|
||
btnWrap.appendChild(cancel);
|
||
btnWrap.appendChild(save);
|
||
actions.appendChild(hint);
|
||
actions.appendChild(btnWrap);
|
||
ef.appendChild(ta);
|
||
ef.appendChild(actions);
|
||
p.hidden = true;
|
||
body.appendChild(ef);
|
||
ta.focus();
|
||
cancel.addEventListener('click', function () { ef.remove(); p.hidden = false; });
|
||
ta.addEventListener('input', function () { hint.textContent = ta.value.length + '/' + COMMENT_MAX; });
|
||
hint.textContent = ta.value.length + '/' + COMMENT_MAX;
|
||
ef.addEventListener('submit', function (ev) {
|
||
ev.preventDefault();
|
||
var text = (ta.value || '').trim();
|
||
if (!text) { notifyError('입력 필요', '덧글 내용을 입력해 주세요.'); return; }
|
||
if (text.length > COMMENT_MAX) { notifyError('입력 초과', '덧글은 200자 이내로 입력해 주세요.'); return; }
|
||
var unlock = lockButton(save);
|
||
// A1: PUT 응답 commentView 로 해당 li 갱신 (전체 재조회 대신 단건 교체)
|
||
api(commentsUrl + '/' + encodeURIComponent(c.commentId), {
|
||
method: 'PUT', headers: formHeaders(), body: encodeForm({ content: text })
|
||
}).then(function (data) {
|
||
unlock();
|
||
var view = data && data.commentId != null ? data : null;
|
||
if (view) {
|
||
var fresh = buildCommentItem(view);
|
||
li.parentNode.replaceChild(fresh, li);
|
||
} else {
|
||
ef.remove(); p.hidden = false;
|
||
}
|
||
}).catch(function (err) { unlock(); notifyError('수정 실패', err.message); });
|
||
});
|
||
}
|
||
|
||
function buildCommentItem(c) {
|
||
var li = document.createElement('li');
|
||
li.className = 'game-comments__item';
|
||
var nick = (c.authorName && String(c.authorName).trim()) ? String(c.authorName).trim() : '익명';
|
||
|
||
var av = document.createElement('div');
|
||
av.className = 'game-comments__avatar';
|
||
av.setAttribute('aria-hidden', 'true');
|
||
av.textContent = nick.charAt(0).toUpperCase();
|
||
|
||
var body = document.createElement('div');
|
||
body.className = 'game-comments__body';
|
||
var meta = document.createElement('div');
|
||
meta.className = 'game-comments__meta';
|
||
var nickEl = document.createElement('span');
|
||
nickEl.className = 'game-comments__nick';
|
||
nickEl.textContent = nick;
|
||
meta.appendChild(nickEl);
|
||
meta.appendChild(buildTimeEl(c.updatedAt || c.createdAt, !!c.edited));
|
||
|
||
var p = document.createElement('p');
|
||
p.textContent = c.content || '';
|
||
|
||
body.appendChild(meta);
|
||
body.appendChild(p);
|
||
|
||
if (canActOn(c.userId)) {
|
||
var foot = document.createElement('div');
|
||
foot.className = 'game-comments__footer';
|
||
var editBtn = document.createElement('button');
|
||
editBtn.type = 'button';
|
||
editBtn.className = 'game-reviews__action game-reviews__action--edit';
|
||
editBtn.textContent = '수정';
|
||
editBtn.addEventListener('click', function () { openCommentEdit(li, body, p, c); });
|
||
var del = document.createElement('button');
|
||
del.type = 'button';
|
||
del.className = 'game-reviews__action game-reviews__action--delete';
|
||
del.textContent = '삭제';
|
||
del.setAttribute('aria-label', '이 덧글 삭제');
|
||
del.addEventListener('click', function () {
|
||
confirmAction('덧글 삭제', '이 덧글을 삭제할까요?', '삭제', function () {
|
||
api(commentsUrl + '/' + encodeURIComponent(c.commentId), { method: 'DELETE', headers: baseHeaders() })
|
||
.then(function () {
|
||
li.remove(); // C5: DOM 완전 제거 (tombstone 잔재 없음)
|
||
cEmptyEl.hidden = cListEl.children.length > 0;
|
||
})
|
||
.catch(function (err) { notifyError('삭제 실패', err.message); });
|
||
});
|
||
});
|
||
foot.appendChild(editBtn);
|
||
foot.appendChild(del);
|
||
body.appendChild(foot);
|
||
}
|
||
|
||
li.appendChild(av);
|
||
li.appendChild(body);
|
||
return li;
|
||
}
|
||
|
||
function setCommentMore(hasMore) {
|
||
if (cMoreWrap) cMoreWrap.hidden = !hasMore;
|
||
}
|
||
function appendComments(list) {
|
||
list.forEach(function (c) { cListEl.appendChild(buildCommentItem(c)); });
|
||
cEmptyEl.hidden = cListEl.children.length > 0;
|
||
}
|
||
// page=0 일 때 목록 초기화, 이후 page 는 append
|
||
function loadComments(page) {
|
||
var p = page || 0;
|
||
if (p === 0) { cListEl.innerHTML = ''; }
|
||
var url = commentsUrl + '?page=' + p + '&sort=' + encodeURIComponent(currentCommentSort);
|
||
return api(url, { headers: baseHeaders() })
|
||
.then(function (data) {
|
||
currentCommentPage = p;
|
||
appendComments(Array.isArray(data.comments) ? data.comments : []);
|
||
setCommentMore(!!data.hasMore);
|
||
if (cToolbar) cToolbar.hidden = false;
|
||
})
|
||
.catch(function () { if (p === 0) cEmptyEl.hidden = false; });
|
||
}
|
||
|
||
if (cMoreBtn) {
|
||
cMoreBtn.addEventListener('click', function () { loadComments(currentCommentPage + 1); });
|
||
}
|
||
if (cToolbar) {
|
||
Array.prototype.forEach.call(cToolbar.querySelectorAll('.game-comments__sort-btn'), function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
var sort = btn.getAttribute('data-sort');
|
||
if (sort === currentCommentSort) return;
|
||
currentCommentSort = sort;
|
||
Array.prototype.forEach.call(cToolbar.querySelectorAll('.game-comments__sort-btn'), function (b) {
|
||
var on = b === btn;
|
||
b.classList.toggle('is-active', on);
|
||
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||
});
|
||
loadComments(0);
|
||
});
|
||
});
|
||
}
|
||
|
||
if (cForm && cInput) {
|
||
if (cCounter) {
|
||
cInput.addEventListener('input', function () { cCounter.textContent = cInput.value.length + '/' + COMMENT_MAX; });
|
||
}
|
||
if (viewerId == null) {
|
||
if (cComposer) cComposer.hidden = true;
|
||
if (cLoginGate) cLoginGate.hidden = false;
|
||
} else {
|
||
if (cComposer) cComposer.hidden = false;
|
||
if (cLoginGate) cLoginGate.hidden = true;
|
||
cForm.addEventListener('submit', function (ev) {
|
||
ev.preventDefault();
|
||
var text = (cInput.value || '').trim();
|
||
if (!text) return;
|
||
if (text.length > COMMENT_MAX) { notifyError('입력 초과', '덧글은 200자 이내로 입력해 주세요.'); return; }
|
||
var unlock = lockButton(cSubmit);
|
||
// A1: POST 응답 commentView 로 새 li 생성·삽입 (oldest=append / newest=prepend)
|
||
api(commentsUrl, { method: 'POST', headers: formHeaders(), body: encodeForm({ content: text }) })
|
||
.then(function (data) {
|
||
unlock();
|
||
cInput.value = '';
|
||
if (cCounter) cCounter.textContent = '0/' + COMMENT_MAX;
|
||
var view = data && data.commentId != null ? data : null;
|
||
if (view) {
|
||
var li = buildCommentItem(view);
|
||
if (currentCommentSort === 'newest') cListEl.insertBefore(li, cListEl.firstChild);
|
||
else cListEl.appendChild(li);
|
||
cEmptyEl.hidden = true;
|
||
} else {
|
||
loadComments(0);
|
||
}
|
||
})
|
||
.catch(function (err) { unlock(); notifyError('등록 실패', err.message); });
|
||
});
|
||
}
|
||
}
|
||
loadComments(0);
|
||
|
||
// ===== 리뷰 (서버 연동) =====
|
||
var rListEl = document.getElementById('game-review-list');
|
||
var rEmptyEl = document.getElementById('game-reviews-empty');
|
||
var rComposer = document.getElementById('game-review-composer');
|
||
var rLoginGate = document.getElementById('game-review-login-gate');
|
||
var rForm = document.getElementById('game-review-form');
|
||
var rInput = document.getElementById('game-review-input');
|
||
var rCounter = document.getElementById('game-review-counter');
|
||
var rHint = document.getElementById('game-review-hint');
|
||
var rAxesEl = document.getElementById('game-review-axes');
|
||
var rStarsEl = document.getElementById('game-review-stars');
|
||
var rStarsLabel = document.getElementById('game-review-stars-label');
|
||
var rSubmit = document.getElementById('game-review-submit');
|
||
var rCancel = document.getElementById('game-review-cancel');
|
||
var rSummary = document.getElementById('game-reviews-summary');
|
||
var rSummaryPanel = document.getElementById('game-reviews-summary-panel');
|
||
var rSummaryRadar = document.getElementById('game-reviews-summary-radar');
|
||
var rAvg = document.getElementById('game-reviews-avg');
|
||
var rAvgStars = document.getElementById('game-reviews-avg-stars');
|
||
var rCount = document.getElementById('game-reviews-count');
|
||
var rToolbar = document.getElementById('game-reviews-toolbar');
|
||
var rMoreBtn = document.getElementById('game-reviews-more');
|
||
var rMoreWrap = rMoreBtn ? rMoreBtn.parentElement : null;
|
||
var reviewsUrl = ctx + '/game/' + encodeURIComponent(gid) + '/reviews';
|
||
var editingReviewId = null; // null = 작성 모드, 값 = 수정 모드
|
||
var currentReviewPage = 0;
|
||
var currentReviewSort = 'newest';
|
||
var REVIEW_MAX = 1000;
|
||
var REVIEW_MIN = 10;
|
||
|
||
// overall(종합) 별점 위젯 — 선택사항(빈값 허용)
|
||
var overallWidget = null;
|
||
// 6축 위젯 핸들 (axisKey -> widget)
|
||
var axisWidgets = {};
|
||
|
||
function buildAxisWidgets() {
|
||
if (!rAxesEl) return;
|
||
rAxesEl.innerHTML = '';
|
||
axisWidgets = {};
|
||
AXIS_KEYS.forEach(function (k) {
|
||
var row = document.createElement('div');
|
||
row.className = 'game-reviews__axis';
|
||
var label = document.createElement('span');
|
||
label.className = 'game-reviews__axis-label';
|
||
label.textContent = AXIS_LABELS_KO[k];
|
||
var w = buildStarRadioGroup(k, AXIS_LABELS_KO[k]);
|
||
label.id = w.labelId;
|
||
row.appendChild(label);
|
||
row.appendChild(w.group);
|
||
rAxesEl.appendChild(row);
|
||
axisWidgets[k] = w;
|
||
});
|
||
}
|
||
function buildOverallWidget() {
|
||
if (!rStarsEl) return;
|
||
rStarsEl.innerHTML = '';
|
||
// overall 은 axisScores['overall'] 키 사용. 빈값 허용(미선택 시 rating 미전송)
|
||
overallWidget = buildStarRadioGroup('overall', '종합 별점');
|
||
rStarsEl.setAttribute('aria-labelledby', rStarsLabel ? rStarsLabel.id : '');
|
||
// group wrapper 대신 rStarsEl 자체(마크업의 radiogroup)에 radio 들을 이식
|
||
while (overallWidget.group.firstChild) {
|
||
rStarsEl.appendChild(overallWidget.group.firstChild);
|
||
}
|
||
}
|
||
|
||
function fillStars(container, rating) {
|
||
container.innerHTML = '';
|
||
for (var i = 1; i <= 5; i++) {
|
||
var s = document.createElement('span');
|
||
s.className = 'game-star' + (i <= rating ? ' is-on' : '');
|
||
s.textContent = '★';
|
||
container.appendChild(s);
|
||
}
|
||
}
|
||
function buildStarDisplay(rating) {
|
||
var wrap = document.createElement('div');
|
||
wrap.className = 'game-stars game-stars--display';
|
||
wrap.setAttribute('aria-hidden', 'true');
|
||
fillStars(wrap, rating);
|
||
return wrap;
|
||
}
|
||
|
||
function resetAxisInputs() {
|
||
AXIS_KEYS.forEach(function (k) { axisScores[k] = 0; if (axisWidgets[k]) axisWidgets[k].paint(); });
|
||
axisScores.overall = 0;
|
||
repaintOverall();
|
||
}
|
||
function repaintOverall() {
|
||
if (!rStarsEl) return;
|
||
var sel = axisScores.overall || 0;
|
||
var btns = rStarsEl.querySelectorAll('.game-stars__btn');
|
||
Array.prototype.forEach.call(btns, function (b, idx) {
|
||
var v = idx + 1;
|
||
b.classList.toggle('is-on', v <= sel);
|
||
b.setAttribute('aria-checked', v === sel ? 'true' : 'false');
|
||
b.tabIndex = (v === sel) ? 0 : -1;
|
||
});
|
||
if (!sel && btns[0]) btns[0].tabIndex = 0;
|
||
}
|
||
|
||
function updateReviewCounter() {
|
||
var len = ((rInput && rInput.value) || '').length;
|
||
if (rCounter) rCounter.textContent = len + '/' + REVIEW_MAX;
|
||
if (rHint) {
|
||
var tooShort = len > 0 && len < REVIEW_MIN;
|
||
rHint.classList.toggle('is-warn', tooShort);
|
||
}
|
||
}
|
||
|
||
function startReviewEdit(r) {
|
||
editingReviewId = r.reviewId;
|
||
// 6축 prefill
|
||
var ax = r.axes || {};
|
||
AXIS_KEYS.forEach(function (k) {
|
||
var v = Number(ax[k]) || 0;
|
||
axisScores[k] = v;
|
||
if (axisWidgets[k]) axisWidgets[k].paint();
|
||
});
|
||
// ratingManual=true 면 overall prefill, 아니면 미선택
|
||
axisScores.overall = (r.ratingManual && Number(r.rating)) ? Number(r.rating) : 0;
|
||
repaintOverall();
|
||
if (rInput) rInput.value = r.body || '';
|
||
updateReviewCounter();
|
||
if (rSubmit) rSubmit.textContent = '리뷰 수정';
|
||
if (rCancel) rCancel.hidden = false;
|
||
if (rComposer) {
|
||
rComposer.hidden = false;
|
||
if (rComposer.scrollIntoView) rComposer.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
if (rInput) rInput.focus();
|
||
}
|
||
function showComposerWrite() {
|
||
editingReviewId = null;
|
||
resetAxisInputs();
|
||
if (rInput) rInput.value = '';
|
||
updateReviewCounter();
|
||
if (rSubmit) rSubmit.textContent = '리뷰 등록';
|
||
if (rCancel) rCancel.hidden = true;
|
||
if (rComposer) rComposer.hidden = false;
|
||
}
|
||
|
||
// W4 배지 키→표시명. 서버 카탈로그(display_name)와 동일 한글명.
|
||
function badgeLabel(key) {
|
||
switch (String(key)) {
|
||
case 'REVIEWER': return '리뷰어';
|
||
case 'TECHNICIAN': return '테크니션';
|
||
default: return String(key);
|
||
}
|
||
}
|
||
|
||
function buildReviewItem(r) {
|
||
var mine = viewerId != null && r.userId != null && Number(r.userId) === Number(viewerId);
|
||
var li = document.createElement('li');
|
||
li.className = 'game-reviews__item' + (mine ? ' is-mine' : '');
|
||
|
||
var head = document.createElement('div');
|
||
head.className = 'game-reviews__item-head';
|
||
head.appendChild(buildStarDisplay(Number(r.rating) || 0));
|
||
|
||
var nick = (r.authorName && String(r.authorName).trim()) ? String(r.authorName).trim() : '익명';
|
||
var nickEl = document.createElement('span');
|
||
nickEl.className = 'game-reviews__nick';
|
||
nickEl.textContent = nick;
|
||
head.appendChild(nickEl);
|
||
|
||
// W4 유저 배지 표시: 작성자 활성 배지(REVIEWER/TECHNICIAN) 칩. textContent 로 XSS 안전.
|
||
if (Array.isArray(r.authorBadges) && r.authorBadges.length) {
|
||
var badgeWrap = document.createElement('span');
|
||
badgeWrap.className = 'game-reviews__badges';
|
||
r.authorBadges.forEach(function (key) {
|
||
var chip = document.createElement('span');
|
||
chip.className = 'game-reviews__badge game-reviews__badge--' + String(key).toLowerCase();
|
||
chip.textContent = badgeLabel(key);
|
||
badgeWrap.appendChild(chip);
|
||
});
|
||
head.appendChild(badgeWrap);
|
||
}
|
||
|
||
if (mine) {
|
||
var mb = document.createElement('span');
|
||
mb.className = 'game-reviews__mine-badge';
|
||
mb.textContent = '내 리뷰';
|
||
head.appendChild(mb);
|
||
}
|
||
// 6축 미니 미터바 — 제목행 노출(§6.6: 요약 그래프와 별개로 카드 단위 시각화)
|
||
var titleMeter = buildTitleMeter(r.axes);
|
||
if (titleMeter) head.appendChild(titleMeter);
|
||
var t = document.createElement('time');
|
||
t.className = 'game-reviews__time';
|
||
t.dateTime = (r.updatedAt || r.createdAt) || '';
|
||
t.title = fmtAbsolute(r.updatedAt || r.createdAt);
|
||
t.textContent = fmtRelative(r.updatedAt || r.createdAt);
|
||
head.appendChild(t);
|
||
if (r.edited) {
|
||
var ed = document.createElement('span');
|
||
ed.className = 'game-reviews__edited';
|
||
ed.textContent = '(수정됨)';
|
||
head.appendChild(ed);
|
||
}
|
||
li.appendChild(head);
|
||
|
||
if (r.body && String(r.body).trim()) {
|
||
var p = document.createElement('p');
|
||
p.className = 'game-reviews__body';
|
||
p.textContent = r.body;
|
||
li.appendChild(p);
|
||
}
|
||
|
||
if (canActOn(r.userId)) {
|
||
var foot = document.createElement('div');
|
||
foot.className = 'game-reviews__footer';
|
||
var editBtn = document.createElement('button');
|
||
editBtn.type = 'button';
|
||
editBtn.className = 'game-reviews__action game-reviews__action--edit';
|
||
editBtn.textContent = '수정';
|
||
editBtn.addEventListener('click', function () { startReviewEdit(r); });
|
||
var del = document.createElement('button');
|
||
del.type = 'button';
|
||
del.className = 'game-reviews__action game-reviews__action--delete';
|
||
del.textContent = '삭제';
|
||
del.addEventListener('click', function () {
|
||
confirmAction('리뷰 삭제', '이 리뷰를 삭제할까요?', '삭제', function () {
|
||
api(reviewsUrl + '/' + encodeURIComponent(r.reviewId), { method: 'DELETE', headers: baseHeaders() })
|
||
.then(function () { loadReviews(0); }) // C5: page0 재조회로 완전 제거 + summary 갱신
|
||
.catch(function (err) { notifyError('삭제 실패', err.message); });
|
||
});
|
||
});
|
||
foot.appendChild(editBtn);
|
||
foot.appendChild(del);
|
||
li.appendChild(foot);
|
||
}
|
||
return li;
|
||
}
|
||
|
||
// C3: 서버 summary 로 표시 (클라 평균계산 폐기)
|
||
function renderSummary(summary) {
|
||
if (!rSummary) return;
|
||
if (!summary || !summary.reviewCount) {
|
||
rSummary.hidden = true;
|
||
if (rSummaryPanel) rSummaryPanel.hidden = true;
|
||
return;
|
||
}
|
||
rSummary.hidden = false;
|
||
var avg = Number(summary.avgRating) || 0;
|
||
if (rAvg) rAvg.textContent = avg.toFixed(1);
|
||
if (rCount) rCount.textContent = '(' + summary.reviewCount + ')';
|
||
if (rAvgStars) fillStars(rAvgStars, Math.round(avg));
|
||
|
||
// 요약 육각형 (200 viewBox). axes 값 null 이면 데이터 없음
|
||
if (rSummaryPanel && rSummaryRadar) {
|
||
rSummaryRadar.innerHTML = '';
|
||
var scores = AXIS_KEYS.map(function (k) { return summary.axes ? Number(summary.axes[k]) || 0 : 0; });
|
||
var radar = buildHexRadar(scores, 100, 100, 80, true);
|
||
rSummaryPanel.hidden = false;
|
||
if (radar.hasData) {
|
||
rSummaryRadar.appendChild(radar.svg);
|
||
} else {
|
||
var none = document.createElement('span');
|
||
none.className = 'game-reviews__radar-empty';
|
||
none.textContent = '6축 평가 데이터 없음';
|
||
rSummaryRadar.appendChild(none);
|
||
}
|
||
}
|
||
}
|
||
|
||
function applyReviewGate(mineExists) {
|
||
if (viewerId == null) {
|
||
if (rComposer) rComposer.hidden = true;
|
||
if (rLoginGate) rLoginGate.hidden = false;
|
||
return;
|
||
}
|
||
if (rLoginGate) rLoginGate.hidden = true;
|
||
if (editingReviewId != null) return; // 수정 진행 중이면 폼 유지
|
||
if (mineExists) {
|
||
if (rComposer) rComposer.hidden = true; // 이미 작성 — 수정은 본인 카드의 '수정' 버튼으로
|
||
} else {
|
||
showComposerWrite(); // 미작성 — 작성 폼 노출
|
||
}
|
||
}
|
||
|
||
var reviewMineSeen = false;
|
||
function appendReviews(list) {
|
||
list.forEach(function (r) {
|
||
rListEl.appendChild(buildReviewItem(r));
|
||
if (r.userId != null && viewerId != null && Number(r.userId) === Number(viewerId)) reviewMineSeen = true;
|
||
});
|
||
rEmptyEl.hidden = rListEl.children.length > 0;
|
||
}
|
||
function setReviewMore(hasMore) {
|
||
if (rMoreWrap) rMoreWrap.hidden = !hasMore;
|
||
}
|
||
function loadReviews(page) {
|
||
var p = page || 0;
|
||
if (p === 0) { rListEl.innerHTML = ''; reviewMineSeen = false; editingReviewId = null; }
|
||
var url = reviewsUrl + '?page=' + p + '&sort=' + encodeURIComponent(currentReviewSort);
|
||
return api(url, { headers: baseHeaders() })
|
||
.then(function (data) {
|
||
currentReviewPage = p;
|
||
appendReviews(Array.isArray(data.reviews) ? data.reviews : []);
|
||
setReviewMore(!!data.hasMore);
|
||
if (rToolbar) rToolbar.hidden = false;
|
||
if (p === 0) renderSummary(data.summary);
|
||
if (!data.hasMore) applyReviewGate(reviewMineSeen); // 전체 로드 후 작성 폼 노출 판단
|
||
})
|
||
.catch(function () { if (p === 0) rEmptyEl.hidden = false; });
|
||
}
|
||
|
||
if (rMoreBtn) {
|
||
rMoreBtn.addEventListener('click', function () { loadReviews(currentReviewPage + 1); });
|
||
}
|
||
if (rToolbar) {
|
||
Array.prototype.forEach.call(rToolbar.querySelectorAll('.game-comments__sort-btn'), function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
var sort = btn.getAttribute('data-sort');
|
||
if (sort === currentReviewSort) return;
|
||
currentReviewSort = sort;
|
||
Array.prototype.forEach.call(rToolbar.querySelectorAll('.game-comments__sort-btn'), function (b) {
|
||
var on = b === btn;
|
||
b.classList.toggle('is-active', on);
|
||
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||
});
|
||
loadReviews(0);
|
||
});
|
||
});
|
||
}
|
||
|
||
// 위젯 초기화 (6축 + overall)
|
||
buildAxisWidgets();
|
||
buildOverallWidget();
|
||
resetAxisInputs();
|
||
|
||
if (rInput) {
|
||
rInput.addEventListener('input', updateReviewCounter);
|
||
updateReviewCounter();
|
||
}
|
||
if (rCancel) {
|
||
rCancel.addEventListener('click', function () { loadReviews(0); });
|
||
}
|
||
if (rForm) {
|
||
rForm.addEventListener('submit', function (ev) {
|
||
ev.preventDefault();
|
||
if (viewerId == null) { notifyError('로그인 필요', '리뷰를 남기려면 로그인이 필요합니다.'); return; }
|
||
// 6축 전부 필수 (UX 선검증, 서버도 400 대칭)
|
||
var missing = AXIS_KEYS.filter(function (k) { return !(axisScores[k] >= 1 && axisScores[k] <= 5); });
|
||
if (missing.length) {
|
||
notifyError('평가 필요', '6개 항목(' + missing.map(function (k) { return AXIS_LABELS_KO[k]; }).join(', ') + ')을 모두 선택해 주세요.');
|
||
return;
|
||
}
|
||
var bodyText = ((rInput && rInput.value) || '').trim();
|
||
if (bodyText.length < REVIEW_MIN) { notifyError('입력 필요', '평가는 최소 ' + REVIEW_MIN + '자 이상 입력해 주세요.'); return; }
|
||
if (bodyText.length > REVIEW_MAX) { notifyError('입력 초과', '평가는 1,000자 이내로 입력해 주세요.'); return; }
|
||
|
||
var payload = { body: bodyText };
|
||
AXIS_KEYS.forEach(function (k) { payload[k] = axisScores[k]; });
|
||
// overall 선택됐을 때만 rating 전송 (미선택이면 서버 6축 자동평균)
|
||
if (axisScores.overall >= 1 && axisScores.overall <= 5) { payload.rating = axisScores.overall; }
|
||
|
||
var isEdit = editingReviewId != null;
|
||
var url = isEdit ? (reviewsUrl + '/' + encodeURIComponent(editingReviewId)) : reviewsUrl;
|
||
var unlock = lockButton(rSubmit);
|
||
api(url, { method: isEdit ? 'PUT' : 'POST', headers: formHeaders(), body: encodeForm(payload) })
|
||
.then(function () { unlock(); loadReviews(0); })
|
||
.catch(function (err) { unlock(); notifyError(isEdit ? '수정 실패' : '등록 실패', err.message); });
|
||
});
|
||
}
|
||
loadReviews(0);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|
||
operator: 'match'
|
||
stack: |-
|
||
TestContext.<anonymous> (/private/var/folders/g5/hclggccx15qdjf89b6vn1fw80000gn/T/bibimbap-prechange-oo0st4cd/src/test/js/webgl-player.test.cjs:32:10)
|
||
Test.runInAsyncScope (node:async_hooks:214:14)
|
||
Test.run (node:internal/test_runner/test:1047:25)
|
||
Test.start (node:internal/test_runner/test:944:17)
|
||
startSubtestAfterBootstrap (node:internal/test_runner/harness:296:17)
|
||
...
|
||
1..1
|
||
# tests 1
|
||
# suites 0
|
||
# pass 0
|
||
# fail 1
|
||
# cancelled 0
|
||
# skipped 0
|
||
# todo 0
|
||
# duration_ms 46.247958
|