웹사이트 1차 제작
This commit is contained in:
parent
524417f10d
commit
838db494d1
|
|
@ -1,4 +1,3 @@
|
||||||
HELP.md
|
|
||||||
target/
|
target/
|
||||||
.mvn/wrapper/maven-wrapper.jar
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
!**/src/main/**/target/
|
!**/src/main/**/target/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,84 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="96b9b4a5-b1bc-45b3-baf2-17942ad056f6" name="변경" comment="3차 초기화">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/HELP.md" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/resources/application.properties" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/resources/dev/application.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/dev/application.properties" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/resources/live/application.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/live/application.properties" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="MavenImportPreferences">
|
||||||
|
<option name="explicitlyEnabledProfiles" value="dev" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo"><![CDATA[{
|
||||||
|
"associatedIndex": 2
|
||||||
|
}]]></component>
|
||||||
<component name="ProjectId" id="3COUL5RY7op3FL1tvImKjMX11Vl" />
|
<component name="ProjectId" id="3COUL5RY7op3FL1tvImKjMX11Vl" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
<option name="showMembers" value="true" />
|
||||||
|
<option name="showVisibilityIcons" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"Spring Boot.이름이 지정되지 않았습니다.executor": "Run",
|
||||||
|
"git-widget-placeholder": "main",
|
||||||
|
"kotlin-language-version-configured": "true"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="이름이 지정되지 않았습니다" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
|
<module name="bibimbap" />
|
||||||
|
<option name="SPRING_BOOT_MAIN_CLASS" value="com.pandoli365.bibimbap.BibimbapApplication" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-jdk-30f59d01ecdd-26cb7f24e5b0-intellij.indexing.shared.core-IU-253.29346.240" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task id="LOCAL-00001" summary="3차 초기화">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1776256387267</created>
|
||||||
|
<option name="number" value="00001" />
|
||||||
|
<option name="presentableId" value="LOCAL-00001" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1776256387267</updated>
|
||||||
|
</task>
|
||||||
|
<task active="true" id="Default" summary="디폴트 작업">
|
||||||
|
<changelist id="96b9b4a5-b1bc-45b3-baf2-17942ad056f6" name="변경" comment="3차 초기화" />
|
||||||
|
<created>1776256595415</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1776256595415</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="2" />
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="VcsManagerConfiguration">
|
||||||
|
<MESSAGE value="3차 초기화" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="3차 초기화" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
18
HELP.md
18
HELP.md
|
|
@ -1,18 +0,0 @@
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
### Reference Documentation
|
|
||||||
|
|
||||||
For further reference, please consider the following sections:
|
|
||||||
|
|
||||||
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
|
|
||||||
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.14-SNAPSHOT/maven-plugin)
|
|
||||||
* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.14-SNAPSHOT/maven-plugin/build-image.html)
|
|
||||||
|
|
||||||
### Maven Parent overrides
|
|
||||||
|
|
||||||
Due to Maven's design, elements are inherited from the parent POM to the project POM.
|
|
||||||
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the
|
|
||||||
parent.
|
|
||||||
To prevent this, the project POM contains empty overrides for these elements.
|
|
||||||
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
|
|
||||||
|
|
||||||
39
pom.xml
39
pom.xml
|
|
@ -35,6 +35,10 @@
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat.embed</groupId>
|
||||||
|
<artifactId>tomcat-embed-jasper</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
|
|
@ -126,4 +130,39 @@
|
||||||
</pluginRepository>
|
</pluginRepository>
|
||||||
</pluginRepositories>
|
</pluginRepositories>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>dev</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<profiles>
|
||||||
|
<profile>dev</profile>
|
||||||
|
</profiles>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>live</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<profiles>
|
||||||
|
<profile>live</profile>
|
||||||
|
</profiles>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.pandoli365.bibimbap.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addViewControllers(ViewControllerRegistry registry) {
|
||||||
|
registry.addViewController("/").setViewName("index");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package com.pandoli365.bibimbap.game;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모용 게임 메타데이터. DB 연동 시 저장소로 대체하면 됩니다.
|
||||||
|
*/
|
||||||
|
public final class GameCatalog {
|
||||||
|
|
||||||
|
private GameCatalog() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int COUNT = 20;
|
||||||
|
|
||||||
|
public static final String[] NAMES = {
|
||||||
|
"별빛 정원", "던전 카페", "픽셀 레이서", "구름 위를 걷다",
|
||||||
|
"마지막 타임라인", "요리하는 용", "네온 시티", "작은 숲의 집",
|
||||||
|
"역전 재판인", "달무리 탐정", "코드 러너", "바다 노래",
|
||||||
|
"시간 상자", "불꽃 학원", "조용한 우주", "종이 비행기",
|
||||||
|
"거울 미로", "손끝 RPG", "노을 역", "꿈의 도서관"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final String[] CREATORS = {
|
||||||
|
"Studio Luna", "김민재", "PixelCat", "이하늘",
|
||||||
|
"Team Horizon", "별작업실", "NEON LAB", "숲그림",
|
||||||
|
"Court Games", "달무리", "dev.han", "wave.sound",
|
||||||
|
"BoxSoft", "학원제작소", "COSMOS", "PaperFly",
|
||||||
|
"mirror.inc", "손끝게임즈", "노을팀", "책벌레"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final int[] LIKE_COUNTS = {
|
||||||
|
1284, 56, 8921, 234,
|
||||||
|
1205, 445, 678, 9012,
|
||||||
|
3400, 12, 567, 89,
|
||||||
|
4456, 223, 7777, 156,
|
||||||
|
990, 34, 2100, 888
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 제작자 한마디 (상세 페이지 하단) */
|
||||||
|
public static final String[] CREATOR_NOTES = {
|
||||||
|
"밤하늘을 걸으며 힐링하고 싶어서 만들었습니다. 플레이 해 주셔서 감사합니다.",
|
||||||
|
"카페 던전은 사랑입니다. 버그 제보는 Git 이슈로 부탁드려요.",
|
||||||
|
"속도감 있는 레이싱! 컨트롤은 WASD, 모바일은 추후 지원 예정이에요.",
|
||||||
|
"구름 위를 걷는 기분을 WebGL로 담아봤습니다.",
|
||||||
|
"스토리에 집중했습니다. 엔딩까지 플레이해 주세요.",
|
||||||
|
"요리 도트에 진심입니다. 레시피 아이디어 환영합니다.",
|
||||||
|
"네온 사인이 마음에 드셨다면 별 하나 부탁드려요.",
|
||||||
|
"작은 집에서 시작하는 하루. 소소한 상호작용을 즐겨 주세요.",
|
||||||
|
"반전 스토리, 스포일러는 삼가 주세요!",
|
||||||
|
"추리는 끝이 없어요. 힌트는 커뮤니티에 올려 두었습니다.",
|
||||||
|
"코딩하듯 플레이하는 러너. PR도 환영합니다.",
|
||||||
|
"바다 소리를 들으며 플레이해 보세요. 이어폰 추천!",
|
||||||
|
"시간 루프가 헷갈리면 메모를 추천합니다.",
|
||||||
|
"학원물이지만 가볍게 즐겨 주세요.",
|
||||||
|
"우주는 넓고 할 일은 많습니다. 업데이트 예정이에요.",
|
||||||
|
"종이비행기처럼 가볍게 날아가 보세요.",
|
||||||
|
"거울 방향이 헷갈릴 수 있어요. 인내심을…",
|
||||||
|
"손끝으로 즐기는 턴제 RPG입니다.",
|
||||||
|
"노을이 지는 역에서 만나요.",
|
||||||
|
"책 속 세계로 오신 걸 환영합니다."
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final String[] GIT_URLS = {
|
||||||
|
"https://github.com/example/starlight-garden",
|
||||||
|
"https://github.com/example/dungeon-cafe",
|
||||||
|
"https://github.com/example/pixel-racer",
|
||||||
|
"https://github.com/example/cloud-walk",
|
||||||
|
"https://github.com/example/last-timeline",
|
||||||
|
"https://github.com/example/cooking-dragon",
|
||||||
|
"https://github.com/example/neon-city",
|
||||||
|
"https://github.com/example/small-forest",
|
||||||
|
"https://github.com/example/court-game",
|
||||||
|
"https://github.com/example/moon-detective",
|
||||||
|
"https://github.com/example/code-runner",
|
||||||
|
"https://github.com/example/sea-song",
|
||||||
|
"https://github.com/example/time-box",
|
||||||
|
"https://github.com/example/flame-school",
|
||||||
|
"https://github.com/example/quiet-space",
|
||||||
|
"https://github.com/example/paper-plane",
|
||||||
|
"https://github.com/example/mirror-maze",
|
||||||
|
"https://github.com/example/fingertip-rpg",
|
||||||
|
"https://github.com/example/sunset-station",
|
||||||
|
"https://github.com/example/dream-library"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static boolean isValidId(int id) {
|
||||||
|
return id >= 1 && id <= COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int toIndex(int id) {
|
||||||
|
return id - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.pandoli365.bibimbap.web;
|
||||||
|
|
||||||
|
import com.pandoli365.bibimbap.game.GameCatalog;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class GameController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unity WebGL 빌드를 넣을 경로 예: /webgl/game-{id}/index.html
|
||||||
|
* 해당 폴더에 Build + TemplateData 를 두면 iframe 으로 로드됩니다.
|
||||||
|
*/
|
||||||
|
public static String webglUrlForGame(int gameId) {
|
||||||
|
return "/webgl/game-" + gameId + "/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/game/{id}")
|
||||||
|
public String gameDetail(@PathVariable("id") int id, Model model) {
|
||||||
|
if (!GameCatalog.isValidId(id)) {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
int idx = GameCatalog.toIndex(id);
|
||||||
|
model.addAttribute("gameId", id);
|
||||||
|
model.addAttribute("gameName", GameCatalog.NAMES[idx]);
|
||||||
|
model.addAttribute("creator", GameCatalog.CREATORS[idx]);
|
||||||
|
model.addAttribute("likeCount", GameCatalog.LIKE_COUNTS[idx]);
|
||||||
|
model.addAttribute("likeCountFormatted", String.format("%,d", GameCatalog.LIKE_COUNTS[idx]));
|
||||||
|
model.addAttribute("creatorNote", GameCatalog.CREATOR_NOTES[idx]);
|
||||||
|
model.addAttribute("gitUrl", GameCatalog.GIT_URLS[idx]);
|
||||||
|
/* 데모: 공통 플레이스홀더. 실제 빌드는 static/webgl/game-{id}/ 에 두고 아래 한 줄을 webglUrlForGame(id) 로 바꾸면 됩니다. */
|
||||||
|
model.addAttribute("webglUrl", "/webgl/placeholder/index.html");
|
||||||
|
model.addAttribute("webglDeployPath", webglUrlForGame(id));
|
||||||
|
return "game-detail";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,8 @@
|
||||||
spring.application.name=bibimbap
|
spring.application.name=bibimbap
|
||||||
|
|
||||||
|
# JSP 뷰 (루트에 두어 항상 로드되도록 함)
|
||||||
|
spring.mvc.view.prefix=/WEB-INF/views/
|
||||||
|
spring.mvc.view.suffix=.jsp
|
||||||
|
|
||||||
|
spring.profiles.active=dev
|
||||||
|
spring.config.import=optional:classpath:dev/application.properties
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ spring.mvc.view.prefix=/WEB-INF/views/
|
||||||
spring.mvc.view.suffix=.jsp
|
spring.mvc.view.suffix=.jsp
|
||||||
|
|
||||||
# common
|
# common
|
||||||
spring.config.import=classpath:db.properties, classpath:common.properties
|
spring.config.import=classpath:dev/db.properties
|
||||||
|
|
||||||
# IP
|
# IP
|
||||||
server.address=0.0.0.0
|
server.address=0.0.0.0
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ spring.mvc.view.prefix=/WEB-INF/views/
|
||||||
spring.mvc.view.suffix=.jsp
|
spring.mvc.view.suffix=.jsp
|
||||||
|
|
||||||
# common
|
# common
|
||||||
spring.config.import=classpath:db.properties, classpath:common.properties
|
spring.config.import=classpath:live/db.properties
|
||||||
|
|
||||||
# IP
|
# IP
|
||||||
server.address=0.0.0.0
|
server.address=0.0.0.0
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGL placeholder</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; }
|
||||||
|
html, body { height: 100%; background: #1a1a1a; color: #e8e8e8; font-family: system-ui, sans-serif; }
|
||||||
|
.wrap {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
width: min(90vw, 480px);
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
border: 2px dashed rgba(232, 165, 75, 0.45);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(ellipse at center, #2a2a2a 0%, #141414 100%);
|
||||||
|
}
|
||||||
|
p { font-size: 0.875rem; line-height: 1.5; opacity: 0.9; max-width: 28rem; }
|
||||||
|
code { font-size: 0.8rem; color: #e8a54b; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="box" role="img" aria-label="WebGL 캔버스 영역"></div>
|
||||||
|
<p>Unity WebGL 빌드 출력물(<code>index.html</code>, <code>Build</code>, <code>TemplateData</code>)을<br>
|
||||||
|
<code>src/main/resources/static/webgl/game-번호/</code> 에 넣고 상세 페이지에서 iframe 경로를 연결하세요.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<style>
|
||||||
|
.site-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 1px solid var(--footer-border, rgba(0, 0, 0, 0.08));
|
||||||
|
background: var(--footer-bg, rgba(0, 0, 0, 0.02));
|
||||||
|
color: var(--footer-text, #1a1a1a);
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .site-footer {
|
||||||
|
--footer-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--footer-bg: rgba(0, 0, 0, 0.35);
|
||||||
|
--footer-text: #ece8e1;
|
||||||
|
--footer-muted: #a39e96;
|
||||||
|
}
|
||||||
|
html:not([data-theme="dark"]) .site-footer {
|
||||||
|
--footer-muted: #5c5c5c;
|
||||||
|
}
|
||||||
|
.site-footer__inner {
|
||||||
|
max-width: 72rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.75rem max(1rem, env(safe-area-inset-left)) calc(1.75rem + env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
.site-footer__brand {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--footer-text);
|
||||||
|
}
|
||||||
|
.site-footer__note {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--footer-muted, #5c5c5c);
|
||||||
|
}
|
||||||
|
.site-footer__meta {
|
||||||
|
margin: 0 0 1.15rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--footer-muted, #5c5c5c);
|
||||||
|
}
|
||||||
|
.site-footer__meta a {
|
||||||
|
color: #e8a54b;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.site-footer__meta a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.site-footer__links {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem 1.25rem;
|
||||||
|
}
|
||||||
|
.site-footer__links a {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--footer-text);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.site-footer__links a:hover {
|
||||||
|
color: #e8a54b;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.site-footer__sep {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 0.75rem;
|
||||||
|
background: var(--footer-border, rgba(0, 0, 0, 0.12));
|
||||||
|
margin: 0 0.15rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<footer class="site-footer" role="contentinfo">
|
||||||
|
<div class="site-footer__inner">
|
||||||
|
<p class="site-footer__brand">© 2026 유니티 개발자 모임</p>
|
||||||
|
<p class="site-footer__note">본 웹사이트는 비영리 단체 퍼슈트도서관의 후원으로 운영됩니다.</p>
|
||||||
|
<p class="site-footer__meta">
|
||||||
|
문의: <a href="mailto:admin@pandoli365.com">admin@pandoli365.com</a>
|
||||||
|
<span class="site-footer__sep" aria-hidden="true"></span>
|
||||||
|
제작: 김판돌
|
||||||
|
</p>
|
||||||
|
<ul class="site-footer__links">
|
||||||
|
<li><a href="https://x.com/Fursuit_Library" target="_blank" rel="noopener noreferrer">X</a></li>
|
||||||
|
<li><a href="https://furlib.pandoli365.com" target="_blank" rel="noopener noreferrer">퍼슈트도서관</a></li>
|
||||||
|
<li><a href="https://gitea.pandoli365.com/pandoli365/bibimbap" target="_blank" rel="noopener noreferrer">소스 코드</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
@ -0,0 +1,900 @@
|
||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<!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>${gameName} — 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-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-webgl__hint {
|
||||||
|
margin: 0.85rem 0 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.game-webgl__hint code {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
word-break: break-all;
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0.1em 0.35em;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
/* 제작자 한마디 ~ 덧글: 공통 패널 */
|
||||||
|
.game-community {
|
||||||
|
margin-top: 2rem;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<jsp:include page="/WEB-INF/views/header.jsp"/>
|
||||||
|
<main class="game-page">
|
||||||
|
<a class="game-back" href="${pageContext.request.contextPath}/">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||||
|
목록으로
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<section class="game-info-card" aria-labelledby="game-title">
|
||||||
|
<h1 id="game-title">${gameName}</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">#${gameId}</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">${creator}</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">${likeCountFormatted}</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="${gameName} WebGL"
|
||||||
|
src="${pageContext.request.contextPath}${webglUrl}"
|
||||||
|
allow="fullscreen; autoplay; xr-spatial-tracking"
|
||||||
|
loading="eager"
|
||||||
|
referrerpolicy="same-origin"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="game-webgl__hint">실제 Unity WebGL 빌드는 리소스 폴더에 두세요. 예: <code>static${webglDeployPath}</code><br>
|
||||||
|
준비되면 <code>GameController</code>에서 <code>webglUrl</code>을 해당 경로(<code>webglUrlForGame(id)</code>)로 바꾸면 됩니다.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="game-community">
|
||||||
|
<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">${creatorNote}</blockquote>
|
||||||
|
<div class="game-panel__git-wrap">
|
||||||
|
<a class="game-panel__git" href="${gitUrl}" 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--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">
|
||||||
|
<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="1000" placeholder="여기에 덧글을 작성해 주세요." required></textarea>
|
||||||
|
<div class="game-comments__actions">
|
||||||
|
<span class="game-comments__hint">최대 1,000자</span>
|
||||||
|
<button type="submit">덧글 등록</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<jsp:include page="/WEB-INF/views/footer.jsp"/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var gameId = ${gameId};
|
||||||
|
var baseLikes = ${likeCount};
|
||||||
|
var LIKE_KEY = 'bibimbap-game-liked';
|
||||||
|
var COMMENT_KEY = 'bibimbap-game-comments';
|
||||||
|
|
||||||
|
function getLikedMap() {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(LIKE_KEY);
|
||||||
|
var o = raw ? JSON.parse(raw) : {};
|
||||||
|
return o && typeof o === 'object' ? o : {};
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLiked(gameIdStr, liked) {
|
||||||
|
var m = getLikedMap();
|
||||||
|
if (liked) m[gameIdStr] = true;
|
||||||
|
else delete m[gameIdStr];
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LIKE_KEY, JSON.stringify(m));
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLiked(gameIdStr) {
|
||||||
|
return !!getLikedMap()[gameIdStr];
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
function syncLike() {
|
||||||
|
var liked = isLiked(gid);
|
||||||
|
likeBtn.setAttribute('aria-pressed', liked ? 'true' : 'false');
|
||||||
|
likeBtn.setAttribute('aria-label', liked ? '좋아요 취소' : '좋아요');
|
||||||
|
likeCountEl.textContent = formatCount(baseLikes + (liked ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
likeBtn.addEventListener('click', function () {
|
||||||
|
var next = !isLiked(gid);
|
||||||
|
setLiked(gid, next);
|
||||||
|
syncLike();
|
||||||
|
});
|
||||||
|
syncLike();
|
||||||
|
|
||||||
|
function getComments() {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(COMMENT_KEY);
|
||||||
|
var o = raw ? JSON.parse(raw) : {};
|
||||||
|
var list = o[gid];
|
||||||
|
return Array.isArray(list) ? list : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveComments(list) {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(COMMENT_KEY);
|
||||||
|
var o = raw ? JSON.parse(raw) : {};
|
||||||
|
if (typeof o !== 'object' || o === null) o = {};
|
||||||
|
o[gid] = list;
|
||||||
|
localStorage.setItem(COMMENT_KEY, JSON.stringify(o));
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var listEl = document.getElementById('game-comment-list');
|
||||||
|
var emptyEl = document.getElementById('game-comments-empty');
|
||||||
|
var form = document.getElementById('game-comment-form');
|
||||||
|
var input = document.getElementById('game-comment-input');
|
||||||
|
var DEFAULT_NICK = 'test';
|
||||||
|
|
||||||
|
function renderComments() {
|
||||||
|
var items = getComments().slice().sort(function (a, b) {
|
||||||
|
return (b.at || '').localeCompare(a.at || '');
|
||||||
|
});
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
emptyEl.hidden = items.length > 0;
|
||||||
|
items.forEach(function (c) {
|
||||||
|
var li = document.createElement('li');
|
||||||
|
li.className = 'game-comments__item';
|
||||||
|
var av = document.createElement('div');
|
||||||
|
var nick = (c.nickname && String(c.nickname).trim()) ? String(c.nickname).trim() : DEFAULT_NICK;
|
||||||
|
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;
|
||||||
|
var t = document.createElement('time');
|
||||||
|
t.dateTime = c.at || '';
|
||||||
|
try {
|
||||||
|
t.textContent = c.at ? new Date(c.at).toLocaleString('ko-KR') : '';
|
||||||
|
} catch (e) {
|
||||||
|
t.textContent = '';
|
||||||
|
}
|
||||||
|
meta.appendChild(nickEl);
|
||||||
|
meta.appendChild(t);
|
||||||
|
var p = document.createElement('p');
|
||||||
|
p.textContent = c.text || '';
|
||||||
|
body.appendChild(meta);
|
||||||
|
body.appendChild(p);
|
||||||
|
if (c.id) {
|
||||||
|
var foot = document.createElement('div');
|
||||||
|
foot.className = 'game-comments__footer';
|
||||||
|
var del = document.createElement('button');
|
||||||
|
del.type = 'button';
|
||||||
|
del.className = 'game-comments__delete';
|
||||||
|
del.textContent = '삭제';
|
||||||
|
del.setAttribute('aria-label', '이 덧글 삭제');
|
||||||
|
del.addEventListener('click', function () {
|
||||||
|
var next = getComments().filter(function (x) { return x.id !== c.id; });
|
||||||
|
saveComments(next);
|
||||||
|
renderComments();
|
||||||
|
});
|
||||||
|
foot.appendChild(del);
|
||||||
|
body.appendChild(foot);
|
||||||
|
}
|
||||||
|
li.appendChild(av);
|
||||||
|
li.appendChild(body);
|
||||||
|
listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var text = (input.value || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
var id = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random();
|
||||||
|
var entry = { id: id, text: text, at: new Date().toISOString(), nickname: DEFAULT_NICK };
|
||||||
|
var list = getComments();
|
||||||
|
list.push(entry);
|
||||||
|
saveComments(list);
|
||||||
|
input.value = '';
|
||||||
|
renderComments();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComments();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
|
||||||
|
<style>
|
||||||
|
/* 기본(라이트) 헤더 — data-theme 없을 때도 동일 톤 */
|
||||||
|
.site-header {
|
||||||
|
--header-bg: #ffffff;
|
||||||
|
--header-text: #1a1a1a;
|
||||||
|
--header-muted: rgba(26, 26, 26, 0.55);
|
||||||
|
--header-border: rgba(0, 0, 0, 0.08);
|
||||||
|
--header-btn-hover: rgba(0, 0, 0, 0.06);
|
||||||
|
box-shadow: 0 1px 0 var(--header-border), 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .site-header {
|
||||||
|
--header-bg: #1a1a1a;
|
||||||
|
--header-text: #f5f0e8;
|
||||||
|
--header-muted: rgba(245, 240, 232, 0.65);
|
||||||
|
--header-border: rgba(255, 255, 255, 0.06);
|
||||||
|
--header-btn-hover: rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 1px 0 var(--header-border), 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
--accent: #e8a54b;
|
||||||
|
--header-h: 4rem;
|
||||||
|
background: var(--header-bg);
|
||||||
|
color: var(--header-text);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.site-header__inner {
|
||||||
|
max-width: 72rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 max(1rem, env(safe-area-inset-left)) 0 max(1rem, env(safe-area-inset-right));
|
||||||
|
height: var(--header-h);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.site-header__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.site-header__brand:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.site-header__logo {
|
||||||
|
height: 2.25rem;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.site-header__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.site-header__icon-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
.site-header__icon-btn:hover {
|
||||||
|
background: var(--header-btn-hover);
|
||||||
|
}
|
||||||
|
.site-header__icon-btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
.site-header__icon-btn svg {
|
||||||
|
width: 1.375rem;
|
||||||
|
height: 1.375rem;
|
||||||
|
}
|
||||||
|
.site-header__icon-btn .icon-sun {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.site-header__icon-btn .icon-moon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .site-header__icon-btn .icon-moon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .site-header__icon-btn .icon-sun {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.site-header__profile {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--header-btn-hover);
|
||||||
|
color: var(--header-text);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s ease, transform 0.15s ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.site-header__profile:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
.site-header__profile:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
.site-header__profile svg {
|
||||||
|
width: 1.35rem;
|
||||||
|
height: 1.35rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<header class="site-header" role="banner">
|
||||||
|
<div class="site-header__inner">
|
||||||
|
<a class="site-header__brand" href="${pageContext.request.contextPath}/">
|
||||||
|
<img class="site-header__logo" src="${pageContext.request.contextPath}/images/logo.png" alt="" width="120" height="36" />
|
||||||
|
<span>bibimbap</span>
|
||||||
|
</a>
|
||||||
|
<div class="site-header__actions">
|
||||||
|
<button type="button" class="site-header__icon-btn" id="theme-toggle" aria-label="다크 모드로 전환" title="테마 전환">
|
||||||
|
<%-- 라이트 모드일 때 달(다크로), 다크 모드일 때 해(라이트로) --%>
|
||||||
|
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="4"/>
|
||||||
|
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<a class="site-header__profile" href="${pageContext.request.contextPath}/#" aria-label="프로필" title="프로필">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var btn = document.getElementById('theme-toggle');
|
||||||
|
if (!btn) return;
|
||||||
|
function label() {
|
||||||
|
var dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||||
|
btn.setAttribute('aria-label', dark ? '라이트 모드로 전환' : '다크 모드로 전환');
|
||||||
|
btn.setAttribute('title', dark ? '라이트 모드' : '다크 모드');
|
||||||
|
}
|
||||||
|
label();
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
try { localStorage.setItem('bibimbap-theme', next); } catch (e) {}
|
||||||
|
label();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,535 @@
|
||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" language="java" %>
|
||||||
|
<%@ page import="com.pandoli365.bibimbap.game.GameCatalog" %>
|
||||||
|
<!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>bibimbap</title>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
color-scheme: light;
|
||||||
|
--surface: #faf8f5;
|
||||||
|
--card-bg: #fff;
|
||||||
|
--text: #1a1a1a;
|
||||||
|
--text-muted: #5c5c5c;
|
||||||
|
--accent: #e8a54b;
|
||||||
|
--border: rgba(0, 0, 0, 0.08);
|
||||||
|
--card-media-1: #f0ebe3;
|
||||||
|
--card-media-2: #e5ddd2;
|
||||||
|
--card-media-3: #dccfb8;
|
||||||
|
--card-shadow: rgba(0, 0, 0, 0.06);
|
||||||
|
--search-btn-text: #1a1a1a;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--surface: #121212;
|
||||||
|
--card-bg: #1e1e1e;
|
||||||
|
--text: #ece8e1;
|
||||||
|
--text-muted: #a39e96;
|
||||||
|
--border: rgba(255, 255, 255, 0.1);
|
||||||
|
--card-media-1: #2a2620;
|
||||||
|
--card-media-2: #1f1c18;
|
||||||
|
--card-media-3: #3d3528;
|
||||||
|
--card-shadow: rgba(0, 0, 0, 0.35);
|
||||||
|
--search-btn-text: #1a1a1a;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
.page-main {
|
||||||
|
max-width: 72rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem max(1rem, env(safe-area-inset-left)) 2.5rem max(1rem, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.page-main {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 검색 */
|
||||||
|
.search-section {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.search-form__field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.search-form__label {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.search-form__input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 3rem;
|
||||||
|
padding: 0 1rem 0 2.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0.875rem 50%;
|
||||||
|
background-size: 1.125rem;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' fill='none' stroke='%23999' stroke-width='2'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cpath d='M12 12l5 5'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .search-form__input {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' fill='none' stroke='%23aaa' stroke-width='2'%3E%3Ccircle cx='8.5' cy='8.5' r='5.5'/%3E%3Cpath d='M12 12l5 5'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
.search-form__input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.search-form__input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.25);
|
||||||
|
}
|
||||||
|
.search-form__submit {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 4.5rem;
|
||||||
|
height: 3rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--search-btn-text);
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.search-form__submit:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-history {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
.search-history[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.search-history__head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.search-history__label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.search-history__clear {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.search-history__clear:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(232, 165, 75, 0.12);
|
||||||
|
}
|
||||||
|
.search-history__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
.search-history__chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.15rem;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.2rem 0.35rem 0.2rem 0.65rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.search-history__chip-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 12rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-history__chip-text:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.search-history__chip-remove {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-history__chip-remove:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .search-history__chip-remove:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 카드 그리드: 모바일 2열 → 태블릿 3열 → 데스크톱 4~5열 */
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
@media (min-width: 480px) {
|
||||||
|
.card-grid {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 2px 8px var(--card-shadow);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
|
||||||
|
}
|
||||||
|
a.card:hover {
|
||||||
|
box-shadow: 0 6px 16px var(--card-shadow);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(232, 165, 75, 0.35);
|
||||||
|
}
|
||||||
|
a.card:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
a.card:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
/* 가로:세로 = 4:5 (포스터/썸네일에 흔한 비율, 3:5보다 덜 길쭉해 모바일 그리드에 균형 있음) */
|
||||||
|
.card__media {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 4 / 5;
|
||||||
|
background: var(--card-media-2);
|
||||||
|
}
|
||||||
|
.card__index {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.45rem;
|
||||||
|
left: 0.45rem;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: #1a1a1a;
|
||||||
|
background: rgba(255, 255, 255, 0.93);
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .card__index {
|
||||||
|
background: rgba(30, 30, 30, 0.93);
|
||||||
|
color: #ece8e1;
|
||||||
|
}
|
||||||
|
.card__img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
/* 이미지 없음: 로고만 중앙 */
|
||||||
|
.card__media-empty {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14%;
|
||||||
|
background: linear-gradient(160deg, var(--card-media-1) 0%, var(--card-media-2) 45%, var(--card-media-3) 100%);
|
||||||
|
}
|
||||||
|
.card__logo-fallback {
|
||||||
|
width: min(52%, 7.5rem);
|
||||||
|
height: auto;
|
||||||
|
max-height: 42%;
|
||||||
|
object-fit: contain;
|
||||||
|
opacity: 0.88;
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.12));
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .card__logo-fallback {
|
||||||
|
opacity: 0.92;
|
||||||
|
filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.45));
|
||||||
|
}
|
||||||
|
.card__body {
|
||||||
|
padding: 0.5rem 0.625rem 0.625rem;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
.card__game-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.card__creator {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card__likes {
|
||||||
|
margin: 0.15rem 0 0;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<jsp:include page="/WEB-INF/views/header.jsp"/>
|
||||||
|
<main class="page-main">
|
||||||
|
<section class="search-section" aria-label="게임·제작자 검색">
|
||||||
|
<form class="search-form" role="search" action="#" method="get">
|
||||||
|
<div class="search-form__field">
|
||||||
|
<label class="search-form__label" for="q">게임·제작자 검색</label>
|
||||||
|
<input class="search-form__input" type="search" id="q" name="q" placeholder="게임·제작자 검색" autocomplete="off" enterkeyhint="search" />
|
||||||
|
</div>
|
||||||
|
<button class="search-form__submit" type="submit">검색</button>
|
||||||
|
</form>
|
||||||
|
<div class="search-history" id="search-history" hidden>
|
||||||
|
<div class="search-history__head">
|
||||||
|
<span class="search-history__label">최근 검색</span>
|
||||||
|
<button type="button" class="search-history__clear" id="search-history-clear">전체 삭제</button>
|
||||||
|
</div>
|
||||||
|
<div class="search-history__chips" id="search-history-chips" role="list"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card-grid" aria-label="추천 목록">
|
||||||
|
<%-- 카드 데이터: GameCatalog (DB 연동 시 교체) --%>
|
||||||
|
<%
|
||||||
|
String ctx = request.getContextPath();
|
||||||
|
for (int i = 0; i < GameCatalog.COUNT; i++) {
|
||||||
|
int displayIndex = i + 1;
|
||||||
|
/* 썸네일 URL — DB 연동 시 null·빈 문자열이면 로고 폴백 */
|
||||||
|
String thumbUrl = null; // 예: list.get(i).getThumbnailUrl()
|
||||||
|
boolean hasImage = thumbUrl != null && !thumbUrl.isBlank();
|
||||||
|
%>
|
||||||
|
<a class="card" href="<%= ctx %>/game/<%= displayIndex %>" aria-labelledby="game-title-<%= i %>">
|
||||||
|
<div class="card__media">
|
||||||
|
<span class="card__index" aria-hidden="true">#<%= displayIndex %></span>
|
||||||
|
<% if (hasImage) { %>
|
||||||
|
<img class="card__img" src="<%= thumbUrl %>" alt="" loading="lazy" decoding="async" />
|
||||||
|
<% } else { %>
|
||||||
|
<div class="card__media-empty" role="presentation">
|
||||||
|
<img class="card__logo-fallback" src="<%= ctx %>/images/logo.png" alt="" width="120" height="120" />
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<div class="card__body">
|
||||||
|
<h2 class="card__game-name" id="game-title-<%= i %>"><%= GameCatalog.NAMES[i] %></h2>
|
||||||
|
<p class="card__creator"><%= GameCatalog.CREATORS[i] %></p>
|
||||||
|
<p class="card__likes">좋아요 <%= String.format("%,d", GameCatalog.LIKE_COUNTS[i]) %></p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<%
|
||||||
|
}
|
||||||
|
%>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<jsp:include page="/WEB-INF/views/footer.jsp"/>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var STORAGE_KEY = 'bibimbap-search-history';
|
||||||
|
var MAX_ITEMS = 10;
|
||||||
|
|
||||||
|
function loadHistory() {
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return [];
|
||||||
|
var parsed = JSON.parse(raw);
|
||||||
|
var list = Array.isArray(parsed) ? parsed.filter(function (s) { return typeof s === 'string' && s.trim(); }) : [];
|
||||||
|
if (list.length > MAX_ITEMS) {
|
||||||
|
list = list.slice(0, MAX_ITEMS);
|
||||||
|
saveHistory(list);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHistory(items) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
|
||||||
|
} catch (e) {
|
||||||
|
/* 용량 초과·비공개 모드 등 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 검색어 추가: 앞에 넣고, 동일 문구(대소문자 무시)는 제거 후 최대 개수 유지 */
|
||||||
|
function addSearchQuery(query) {
|
||||||
|
var q = (query || '').trim();
|
||||||
|
if (!q) return;
|
||||||
|
var list = loadHistory();
|
||||||
|
var lower = q.toLowerCase();
|
||||||
|
list = list.filter(function (item) { return item.toLowerCase() !== lower; });
|
||||||
|
list.unshift(q);
|
||||||
|
if (list.length > MAX_ITEMS) list = list.slice(0, MAX_ITEMS);
|
||||||
|
saveHistory(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSearchQuery(query) {
|
||||||
|
var lower = (query || '').toLowerCase();
|
||||||
|
var list = loadHistory().filter(function (item) { return item.toLowerCase() !== lower; });
|
||||||
|
saveHistory(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var form = document.querySelector('.search-form');
|
||||||
|
var input = document.getElementById('q');
|
||||||
|
var historyEl = document.getElementById('search-history');
|
||||||
|
var chipsEl = document.getElementById('search-history-chips');
|
||||||
|
var clearBtn = document.getElementById('search-history-clear');
|
||||||
|
|
||||||
|
function renderChips() {
|
||||||
|
var list = loadHistory();
|
||||||
|
chipsEl.innerHTML = '';
|
||||||
|
if (!list.length) {
|
||||||
|
historyEl.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
historyEl.hidden = false;
|
||||||
|
list.forEach(function (term) {
|
||||||
|
var chip = document.createElement('div');
|
||||||
|
chip.className = 'search-history__chip';
|
||||||
|
chip.setAttribute('role', 'listitem');
|
||||||
|
|
||||||
|
var textBtn = document.createElement('button');
|
||||||
|
textBtn.type = 'button';
|
||||||
|
textBtn.className = 'search-history__chip-text';
|
||||||
|
textBtn.textContent = term;
|
||||||
|
textBtn.title = term;
|
||||||
|
textBtn.addEventListener('click', function () {
|
||||||
|
input.value = term;
|
||||||
|
input.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
var removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className = 'search-history__chip-remove';
|
||||||
|
removeBtn.setAttribute('aria-label', '삭제: ' + term);
|
||||||
|
removeBtn.textContent = '\u00D7';
|
||||||
|
removeBtn.addEventListener('click', function (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
removeSearchQuery(term);
|
||||||
|
renderChips();
|
||||||
|
});
|
||||||
|
|
||||||
|
chip.appendChild(textBtn);
|
||||||
|
chip.appendChild(removeBtn);
|
||||||
|
chipsEl.appendChild(chip);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form && input) {
|
||||||
|
form.addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var q = input.value;
|
||||||
|
addSearchQuery(q);
|
||||||
|
renderChips();
|
||||||
|
/* 실제 검색 URL 연동 시: location.href = '...?q=' + encodeURIComponent(q.trim()); */
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn.addEventListener('click', function () {
|
||||||
|
clearHistory();
|
||||||
|
renderChips();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderChips();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var k = 'bibimbap-theme';
|
||||||
|
var t = localStorage.getItem(k);
|
||||||
|
if (t !== 'light' && t !== 'dark') {
|
||||||
|
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 433 KiB |
Loading…
Reference in New Issue