Compare commits

..

No commits in common. "b61973f96d575af96504dab264985ea76315adf7" and "fad9c29a553bf8e350622dd0db874caa373fb43a" have entirely different histories.

10 changed files with 104 additions and 350 deletions

43
pom.xml
View File

@ -37,7 +37,6 @@
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version> <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
<maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version> <maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
<maven-war-plugin.version>3.4.0</maven-war-plugin.version> <maven-war-plugin.version>3.4.0</maven-war-plugin.version>
<app.profile>dev</app.profile>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
@ -118,19 +117,11 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>${maven-resources-plugin.version}</version> <version>${maven-resources-plugin.version}</version>
<configuration>
<propertiesEncoding>${project.build.sourceEncoding}</propertiesEncoding>
</configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version> <version>${maven-surefire-plugin.version}</version>
<configuration>
<excludes>
<exclude>**/DbUpdateQueryGeneratorTest.java</exclude>
</excludes>
</configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@ -221,27 +212,22 @@
<profiles> <profiles>
<profile> <profile>
<id>dev</id> <id>dev</id>
<properties>
<app.profile>dev</app.profile>
</properties>
<build> <build>
<resources> <resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>application.properties</include>
</includes>
<filtering>true</filtering>
</resource>
<resource> <resource>
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
<excludes> <excludes>
<exclude>application.properties</exclude>
<exclude>application-*.properties</exclude> <exclude>application-*.properties</exclude>
<exclude>dev/application.properties</exclude> <exclude>dev/application.properties</exclude>
<exclude>live/**</exclude> <exclude>live/**</exclude>
</excludes> </excludes>
</resource> </resource>
<resource>
<directory>src/main/resources/dev</directory>
<includes>
<include>application.properties</include>
</includes>
</resource>
</resources> </resources>
<plugins> <plugins>
<plugin> <plugin>
@ -258,27 +244,22 @@
</profile> </profile>
<profile> <profile>
<id>live</id> <id>live</id>
<properties>
<app.profile>live</app.profile>
</properties>
<build> <build>
<resources> <resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>application.properties</include>
</includes>
<filtering>true</filtering>
</resource>
<resource> <resource>
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
<excludes> <excludes>
<exclude>application.properties</exclude>
<exclude>application-*.properties</exclude> <exclude>application-*.properties</exclude>
<exclude>dev/**</exclude> <exclude>dev/**</exclude>
<exclude>live/application.properties</exclude> <exclude>live/application.properties</exclude>
</excludes> </excludes>
</resource> </resource>
<resource>
<directory>src/main/resources/live</directory>
<includes>
<include>application.properties</include>
</includes>
</resource>
</resources> </resources>
<plugins> <plugins>
<plugin> <plugin>

View File

@ -17,9 +17,13 @@ public class UploadResourceConfig implements WebMvcConfigurer {
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
Path uploadPath = Paths.get(gameStoragePath).toAbsolutePath().normalize(); Path uploadPath = Paths.get(gameStoragePath).toAbsolutePath().normalize();
String uploadLocation = uploadPath.toUri().toString();
String profileLocation = uploadPath.resolve("profile").toUri().toString(); String profileLocation = uploadPath.resolve("profile").toUri().toString();
String gameLocation = uploadPath.resolve("game").toUri().toString();
registry.addResourceHandler("/profile/**") registry.addResourceHandler("/profile/**")
.addResourceLocations(profileLocation); .addResourceLocations(profileLocation);
registry.addResourceHandler("/game/**")
.addResourceLocations(uploadLocation, gameLocation);
} }
} }

View File

@ -1,11 +1,14 @@
package com.pandoli365.bibimbap.controller.api; package com.pandoli365.bibimbap.controller.api;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -16,6 +19,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Duration;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import java.util.UUID;
@ -26,35 +30,35 @@ public class GameAssetController {
private String uploadStoragePath; private String uploadStoragePath;
@GetMapping("/game/{gameUuid}/**") @GetMapping("/game/{gameUuid}/**")
public void gameAsset( public ResponseEntity<Resource> gameAsset(
@PathVariable("gameUuid") String gameUuid, @PathVariable("gameUuid") String gameUuid,
HttpServletRequest request, HttpServletRequest request
HttpServletResponse response
) throws IOException { ) throws IOException {
String normalizedGameUuid = normalizeGameUuid(gameUuid); String normalizedGameUuid = normalizeGameUuid(gameUuid);
if (normalizedGameUuid == null) { if (normalizedGameUuid == null) {
response.sendError(HttpStatus.NOT_FOUND.value()); return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
return;
} }
Path gameDir = gameRoot().resolve(normalizedGameUuid).normalize(); Path gameDir = gameRoot().resolve(normalizedGameUuid).normalize();
Path assetFile = resolveAssetFile(gameDir, normalizedGameUuid, request); Path assetFile = resolveAssetFile(gameDir, normalizedGameUuid, request);
if (assetFile == null || !Files.isRegularFile(assetFile)) { if (assetFile == null || !Files.isRegularFile(assetFile)) {
response.sendError(HttpStatus.NOT_FOUND.value()); return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
return;
} }
HttpHeaders headers = new HttpHeaders();
String contentEncoding = contentEncoding(assetFile); String contentEncoding = contentEncoding(assetFile);
String contentType = contentType(assetFile); String contentType = contentType(assetFile);
response.setStatus(HttpStatus.OK.value());
if (contentEncoding != null) { if (contentEncoding != null) {
response.setHeader(HttpHeaders.CONTENT_ENCODING, contentEncoding); headers.set(HttpHeaders.CONTENT_ENCODING, contentEncoding);
response.setHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING); headers.set(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
} }
response.setHeader(HttpHeaders.CONTENT_TYPE, contentType); headers.set(HttpHeaders.CONTENT_TYPE, contentType);
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600, public"); headers.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic());
response.setContentLengthLong(Files.size(assetFile)); headers.setContentLength(Files.size(assetFile));
Files.copy(assetFile, response.getOutputStream());
return ResponseEntity.ok()
.headers(headers)
.body(new FileSystemResource(assetFile));
} }
private Path gameRoot() { private Path gameRoot() {

View File

@ -1,5 +0,0 @@
spring.profiles.active=@app.profile@
spring.application.name=bibimbap
# common
spring.config.import=classpath:${spring.profiles.active}/db.properties

View File

@ -394,7 +394,7 @@
<path d="M12 5v14"/> <path d="M12 5v14"/>
<path d="M5 12h14"/> <path d="M5 12h14"/>
</svg> </svg>
<span id="game-register-submit-label">등록</span> <span id="game-register-submit-label">등록 요청</span>
</button> </button>
</div> </div>
</form> </form>

View File

@ -83,36 +83,10 @@
.policy-section p { .policy-section p {
margin: 0.45rem 0 0; margin: 0.45rem 0 0;
} }
.policy-section ul, .policy-section ul {
.policy-section ol {
margin: 0.6rem 0 0; margin: 0.6rem 0 0;
padding-left: 1.2rem; padding-left: 1.2rem;
} }
.policy-table-wrap {
margin-top: 0.75rem;
overflow-x: auto;
}
.policy-table {
width: 100%;
border-collapse: collapse;
min-width: 36rem;
font-size: 0.875rem;
}
.policy-table th,
.policy-table td {
padding: 0.75rem;
border: 1px solid var(--border);
text-align: left;
vertical-align: top;
line-height: 1.6;
}
.policy-table th {
color: var(--text);
background: var(--surface);
}
.policy-table td {
color: var(--text-muted);
}
.policy-section strong { .policy-section strong {
color: var(--text); color: var(--text);
} }
@ -124,88 +98,48 @@
<article class="policy-doc"> <article class="policy-doc">
<header class="policy-doc__head"> <header class="policy-doc__head">
<h1>운영정책</h1> <h1>운영정책</h1>
<p>시행일: 2026년 4월 16일</p> <p>시행일: 2026년 5월 3일</p>
</header> </header>
<div class="policy-doc__body"> <div class="policy-doc__body">
<section class="policy-section"> <section class="policy-section">
<h2>제1조 (목적)</h2> <h2>1. 기본 원칙</h2>
<p>본 운영정책은 이용약관에서 위임한 사항과 서비스 운영에 필요한 구체적인 사항을 규정함을 목적으로 합니다. '비빔밥'(이하 "서비스")을 이용하는 모든 회원은 본 정책을 준수해야 합니다.</p> <p>bibimbap은 이용자가 직접 만든 게임과 관련 콘텐츠를 안전하고 편안하게 공유하는 공간을 지향합니다. 운영자는 이용자의 창작과 표현을 존중하되, 다른 이용자의 권리와 서비스 안정성을 해치는 행위에는 필요한 조치를 합니다.</p>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제2조 (이용자의 권리와 의무)</h2> <h2>2. 게임 등록 기준</h2>
<ol> <ul>
<li>회원은 서비스를 이용하며 발생하는 문제에 대해 이메일(artbiit@naver.com)을 통해 문의하고 개선을 요청할 수 있습니다.</li> <li>게임 제목, 제작자명, 설명, 이미지, 실행 파일 또는 WebGL 경로는 실제 콘텐츠와 관련 있어야 합니다.</li>
<li>회원은 타인의 권리를 침해해서는 안 되며, 쾌적한 커뮤니티 환경 조성을 위해 협조해야 합니다.</li> <li>타인의 저작물을 사용할 경우 필요한 권한을 확보해야 합니다.</li>
</ol> <li>실행 파일, 압축 파일, 스크립트 등에는 악성 코드나 이용자 환경을 훼손하는 동작이 포함되어서는 안 됩니다.</li>
</ul>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제3조 (커뮤니티 이용 규칙)</h2> <h2>3. 댓글 및 커뮤니티 이용</h2>
<p>회원은 게시물 작성 및 채팅 시 다음 각호에 해당하는 행위를 해서는 안 됩니다.</p> <ul>
<ol> <li>비방, 괴롭힘, 혐오 표현, 개인정보 노출, 도배성 댓글은 제한될 수 있습니다.</li>
<li><strong>타인 비방 및 명예훼손:</strong> 특정 개인이나 단체를 비하, 모욕하거나 허위 사실을 유포하는 행위</li> <li>버그 제보와 피드백은 가능한 한 구체적이고 존중하는 방식으로 작성해 주세요.</li>
<li><strong>도배 및 광고:</strong> 동일한 내용을 반복적으로 게시하거나 운영자의 승인 없는 상업적 광고 행위</li> <li>운영자는 분쟁 완화와 서비스 보호를 위해 댓글을 숨김 또는 삭제할 수 있습니다.</li>
<li><strong>음란물 및 혐오 표현:</strong> 선정적인 내용, 잔인한 묘사, 특정 계층에 대한 차별 및 혐오를 조장하는 내용</li> </ul>
<li><strong>개인정보 유출:</strong> 본인 또는 타인의 연락처, 주소 등 개인정보를 노출하는 행위</li>
</ol>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제4조 (게임 및 기술적 서비스 이용 규칙)</h2> <h2>4. 업로드 파일 관리</h2>
<p>운영자가 게재한 웹게임 및 서비스 이용 시 다음 행위를 엄격히 금지합니다.</p> <p>이미지와 게임 파일은 지정된 저장소에 업로드됩니다. 이용자는 업로드한 파일이 본인의 창작물이거나 배포 권한이 있는 자료인지 확인해야 합니다. 운영자는 보안, 용량, 저작권, 정책 위반 문제를 이유로 파일을 삭제하거나 접근을 제한할 수 있습니다.</p>
<ol>
<li><strong>데이터 마이닝 및 리버스 엔지니어링:</strong> 서비스의 소스 코드를 무단으로 추출하거나, 비정상적인 방식으로 데이터를 수집하는 행위</li>
<li><strong>비인가 프로그램 사용:</strong> 운영자가 제공하지 않은 프로그램을 사용하여 게임의 밸런스를 무너뜨리거나 시스템에 부하를 주는 행위</li>
<li><strong>버그 악용:</strong> 시스템 오류를 인지하고도 이를 이득을 취할 목적으로 반복 사용하는 행위</li>
</ol>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제5조 (저작권 보호)</h2> <h2>5. 제재 기준</h2>
<ol> <ul>
<li>회원은 제3자의 저작권을 포함한 지식재산권을 침해하는 결과물을 게시해서는 안 됩니다.</li> <li>경미한 위반은 안내, 수정 요청, 콘텐츠 숨김으로 처리할 수 있습니다.</li>
<li>신고 등을 통해 저작권 침해가 확인된 게시물은 사전 통보 없이 삭제될 수 있으며, 반복될 경우 서비스 이용이 제한됩니다.</li> <li>반복 위반 또는 명백한 피해가 있는 경우 콘텐츠 삭제, 업로드 제한, 계정 정지 조치를 할 수 있습니다.</li>
</ol> <li>보안 위협, 불법 콘텐츠, 심각한 권리 침해가 확인된 경우 즉시 접근 차단될 수 있습니다.</li>
</ul>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제6조 (제재 기준 및 단계)</h2> <h2>6. 신고 및 문의</h2>
<p>운영자는 위반 행위의 경중에 따라 다음과 같이 조치할 수 있습니다.</p> <p>운영정책 위반 콘텐츠, 권리 침해, 계정 문제는 <strong>admin@pandoli365.com</strong>으로 문의할 수 있습니다. 신고 시 문제가 되는 URL, 화면 캡처, 사유를 함께 전달하면 처리가 빨라집니다.</p>
<div class="policy-table-wrap">
<table class="policy-table">
<thead>
<tr>
<th>제재 단계</th>
<th>제재 내용</th>
<th>대상 항목</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>1단계: 주의</strong></td>
<td>게시물 삭제 및 주의 쪽지 발송</td>
<td>단순 게시판 규칙 위반, 경미한 비방</td>
</tr>
<tr>
<td><strong>2단계: 기간 제한</strong></td>
<td>3일 ~ 30일 서비스 이용 정지</td>
<td>반복적인 도배, 욕설, 타인 비방, 버그 악용</td>
</tr>
<tr>
<td><strong>3단계: 영구 제한</strong></td>
<td>서비스 이용 권한 영구 박탈</td>
<td>데이터 마이닝, 해킹, 심각한 저작권 침해, 상습적인 운영 방해</td>
</tr>
</tbody>
</table>
</div>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제7조 (복구 및 신고)</h2> <h2>7. 정책 변경</h2>
<ol> <p>운영정책은 서비스 상황, 법령, 보안 필요에 따라 변경될 수 있습니다. 중요한 변경이 있는 경우 서비스 내 공지 또는 적절한 방법으로 안내합니다.</p>
<li>서비스 오류로 인해 발생한 피해는 운영자가 확인 가능한 범위 내에서 복구를 위해 노력합니다.</li>
<li>타인의 부정 행위를 발견한 경우 이메일을 통해 신고할 수 있으며, 운영자는 이를 객관적으로 조사하여 처리합니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>부칙</h2>
<p>본 운영정책은 2026년 4월 16일부터 시행됩니다.</p>
</section> </section>
</div> </div>
</article> </article>

View File

@ -235,10 +235,7 @@
</div> </div>
<label class="auth-check"> <label class="auth-check">
<input type="checkbox" name="termsAccepted" value="true" required /> <input type="checkbox" name="termsAccepted" value="true" required />
<span> <span>이용약관과 개인정보 처리방침에 동의합니다.</span>
<a class="auth-link" href="<%= ctx %>/terms" target="_blank" rel="noopener noreferrer">이용약관</a>과
<a class="auth-link" href="<%= ctx %>/operation-policy" target="_blank" rel="noopener noreferrer">운영정책</a>에 동의합니다.
</span>
</label> </label>
<button class="auth-button auth-button--primary" type="submit">회원가입</button> <button class="auth-button auth-button--primary" type="submit">회원가입</button>
</form> </form>

View File

@ -83,8 +83,7 @@
.policy-section p { .policy-section p {
margin: 0.45rem 0 0; margin: 0.45rem 0 0;
} }
.policy-section ul, .policy-section ul {
.policy-section ol {
margin: 0.6rem 0 0; margin: 0.6rem 0 0;
padding-left: 1.2rem; padding-left: 1.2rem;
} }
@ -99,94 +98,50 @@
<article class="policy-doc"> <article class="policy-doc">
<header class="policy-doc__head"> <header class="policy-doc__head">
<h1>이용약관</h1> <h1>이용약관</h1>
<p>시행일: 2026년 4월 16일</p> <p>시행일: 2026년 5월 3일</p>
</header> </header>
<div class="policy-doc__body"> <div class="policy-doc__body">
<section class="policy-section"> <section class="policy-section">
<h2>제1조 (목적)</h2> <h2>제1조 목적</h2>
<p>본 약관은 개인 개발자(이하 "운영자")가 제공하는 웹 서비스 '비빔밥'(이하 "서비스")의 이용 조건 및 절차, 이용자와 운영자의 권리, 의무 및 책임 사항을 규정함을 목적으로 합니다.</p> <p>본 약관은 bibimbap이 제공하는 게임 게시, 플레이, 댓글, 좋아요 및 관련 서비스의 이용 조건과 절차, 이용자와 운영자의 권리와 의무를 정합니다.</p>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제2조 (용어의 정의)</h2> <h2>제2조 계정 및 로그인</h2>
<ol> <p>이용자는 게스트 계정 또는 외부 로그인 제공자를 통해 서비스를 이용할 수 있습니다. 이용자는 본인의 계정 접근 권한을 안전하게 관리해야 하며, 계정 사용 중 발생한 활동에 대한 책임은 이용자에게 있습니다.</p>
<li>"서비스"란 운영자가 개발한 웹게임을 게재하고 이용자들이 소통할 수 있는 커뮤니티 공간을 제공하는 웹사이트를 의미합니다.</li>
<li>"이용자"란 본 약관에 따라 운영자가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.</li>
<li>"회원"이란 서비스에 접속하여 본 약관에 동의하고 계정을 생성하여 서비스를 이용하는 자를 말합니다.</li>
</ol>
</section> </section>
<section class="policy-section"> <section class="policy-section">
<h2>제3조 (약관의 게시와 개정)</h2> <h2>제3조 서비스 이용</h2>
<ol>
<li>운영자는 본 약관의 내용을 이용자가 쉽게 알 수 있도록 서비스 초기 화면에 게시합니다.</li>
<li>운영자는 관련 법령을 위배하지 않는 범위에서 본 약관을 개정할 수 있습니다.</li>
<li>약관이 개정될 경우 적용 일자 및 개정 사유를 명시하여 현행 약관과 함께 서비스 내에 공지합니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>제4조 (서비스의 제공 및 변경)</h2>
<ol>
<li>서비스는 웹게임 게재 및 커뮤니티 기능을 기본으로 제공하며, 운영자의 판단에 따라 새로운 기능을 추가하거나 기존 기능을 변경할 수 있습니다.</li>
<li>본 서비스는 이용자에게 무료로 제공됩니다. 다만, 서비스 운영상 필요한 경우 일부 기능을 유료화하거나 광고를 게재할 수 있으며, 이 경우 사전에 공지합니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>제5조 (회원가입 및 계정 관리)</h2>
<ol>
<li>이용자는 운영자가 정한 가입 양식에 따라 회원정보를 기입함으로써 회원가입을 신청합니다.</li>
<li>모든 회원은 반드시 본인의 정보를 제공하여야 하며, 타인의 정보를 도용할 경우 서비스 이용 제한 및 관련 법령에 따른 처벌을 받을 수 있습니다.</li>
<li>회원은 자신의 계정과 비밀번호를 관리할 책임이 있으며, 이를 제3자가 이용하도록 하여서는 안 됩니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>제6조 (이용자의 의무 및 금지 행위)</h2>
<p>이용자는 서비스 이용 시 다음 각호의 행위를 하여서는 안 됩니다.</p>
<ol>
<li>서비스의 안정적인 운영을 방해할 목적으로 하는 데이터 마이닝, 크롤링, 또는 비정상적인 접근 행위</li>
<li>타인을 비방하거나 명예를 훼손하는 게시물을 작성하는 행위</li>
<li>타인의 저작권 등 지식재산권을 침해하는 행위 (저작권 침해 게임물이나 게시물 게재 등)</li>
<li>공공질서 및 미풍양속에 반하는 정보를 유포하는 행위</li>
<li>기타 관계 법령 및 운영정책에 위배되는 행위</li>
</ol>
</section>
<section class="policy-section">
<h2>제7조 (게시물의 저작권)</h2>
<ol>
<li>회원이 서비스 내에 게시한 게시물의 저작권은 해당 게시물의 저작자에게 귀속됩니다.</li>
<li>운영자는 서비스의 홍보 및 운영 목적으로 회원의 게시물을 노출하거나 복제, 수정하여 사용할 수 있습니다.</li>
<li>회원은 서비스에 저작권 문제가 있는 게시물을 올려서는 안 되며, 이로 인해 발생하는 법적 책임은 게시자 본인에게 있습니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>제8조 (서비스 이용 제한 및 해지)</h2>
<ol>
<li>회원은 언제든지 서비스 내 설정 메뉴를 통해 회원 탈퇴를 신청할 수 있습니다.</li>
<li>운영자는 회원이 제6조의 금지 행위를 한 경우, 사전 통보 없이 서비스 이용을 일시 정지하거나 계정을 삭제할 수 있습니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>제9조 (책임 제한)</h2>
<ol>
<li>운영자는 천재지변, 서버 점검, 네트워크 장애 등 불가항력적인 사유로 서비스를 제공할 수 없는 경우 책임이 면제됩니다.</li>
<li>운영자는 이용자 간 또는 이용자와 제3자 상호 간에 서비스를 매개로 하여 발생한 분쟁에 대해 개입할 의무가 없으며 이로 인한 손해를 배상할 책임이 없습니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>제10조 (준거법 및 관할법원)</h2>
<ol>
<li>본 약관과 관련하여 발생한 분쟁에 대해서는 대한민국 법을 준거법으로 합니다.</li>
<li>서비스 이용으로 발생한 분쟁에 대해 소송이 제기될 경우 <strong>서울중앙지방법원</strong>을 전속 관할법원으로 합니다.</li>
</ol>
</section>
<section class="policy-section">
<h2>부칙</h2>
<p>본 약관은 2026년 4월 16일부터 시행됩니다.</p>
</section>
<section class="policy-section">
<h2>문의처</h2>
<ul> <ul>
<li>이메일: artbiit@naver.com</li> <li>이용자는 서비스의 목적과 운영정책에 맞게 게임, 이미지, 댓글 등 콘텐츠를 등록해야 합니다.</li>
<li>운영자는 서비스 품질 유지, 보안, 장애 대응을 위해 필요한 범위에서 서비스 제공 방식을 변경하거나 일시 중단할 수 있습니다.</li>
<li>서비스 내 기능은 개발 상황에 따라 추가, 변경, 제거될 수 있습니다.</li>
</ul> </ul>
</section> </section>
<section class="policy-section">
<h2>제4조 콘텐츠 권리</h2>
<p>이용자가 등록한 콘텐츠의 권리는 원칙적으로 해당 이용자에게 있습니다. 다만 이용자는 서비스 내 노출, 저장, 전송, 소개를 위해 필요한 범위에서 운영자에게 콘텐츠를 사용할 수 있는 권한을 부여합니다.</p>
</section>
<section class="policy-section">
<h2>제5조 금지 행위</h2>
<ul>
<li>타인의 권리, 개인정보, 저작권을 침해하는 행위</li>
<li>악성 코드, 비정상 트래픽, 자동화 도구 등으로 서비스를 방해하는 행위</li>
<li>불법적이거나 혐오, 괴롭힘, 과도한 폭력성, 음란성을 포함한 콘텐츠를 게시하는 행위</li>
<li>운영자 또는 다른 이용자를 사칭하거나 허위 정보를 등록하는 행위</li>
</ul>
</section>
<section class="policy-section">
<h2>제6조 이용 제한</h2>
<p>운영자는 약관 또는 운영정책을 위반한 이용자에게 콘텐츠 삭제, 기능 제한, 계정 정지, 접근 차단 등의 조치를 할 수 있습니다. 긴급한 보안 또는 피해 확산 우려가 있는 경우 사전 안내 없이 조치할 수 있습니다.</p>
</section>
<section class="policy-section">
<h2>제7조 책임의 한계</h2>
<p>운영자는 이용자가 등록한 콘텐츠의 완전성, 정확성, 적법성을 보증하지 않습니다. 또한 천재지변, 외부 서비스 장애, 네트워크 문제 등 운영자의 합리적 통제를 벗어난 사유로 발생한 손해에 대해 책임을 지지 않습니다.</p>
</section>
<section class="policy-section">
<h2>제8조 문의</h2>
<p>서비스 이용과 관련한 문의는 <strong>admin@pandoli365.com</strong>으로 보낼 수 있습니다.</p>
</section>
</div> </div>
</article> </article>
</main> </main>

View File

@ -1,105 +0,0 @@
-- select = dev
-- update = live
-- generated SQL only. Review before running.
-- ONLY IN dev: game_comments
-- ONLY IN dev: game_likes
-- ONLY IN dev: games
-- ONLY IN dev: user_auth_identities
-- ONLY IN dev: users
-- game_comments table does not exist.
CREATE SEQUENCE IF NOT EXISTS "game_comments_id_seq";
CREATE TABLE "game_comments" (
"id" bigint DEFAULT nextval('game_comments_id_seq'::regclass) NOT NULL,
"game_id" bigint NOT NULL,
"nickname" character varying(50) NOT NULL,
"content" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone,
"is_delete" boolean DEFAULT false NOT NULL,
PRIMARY KEY ("id")
);
-- game_likes table does not exist.
CREATE SEQUENCE IF NOT EXISTS "game_likes_id_seq";
CREATE TABLE "game_likes" (
"id" bigint DEFAULT nextval('game_likes_id_seq'::regclass) NOT NULL,
"game_id" bigint NOT NULL,
"user_key" character varying(100) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY ("id")
);
-- games table does not exist.
CREATE SEQUENCE IF NOT EXISTS "games_id_seq";
CREATE TABLE "games" (
"id" bigint DEFAULT nextval('games_id_seq'::regclass) NOT NULL,
"name" character varying(200) NOT NULL,
"creator" character varying(100) NOT NULL,
"creator_note" text,
"git_url" character varying(500),
"webgl_path" character varying(500),
"thumbnail_url" character varying(500),
"like_count" integer DEFAULT 0 NOT NULL,
"is_visible" boolean DEFAULT true NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"is_delete" boolean DEFAULT false NOT NULL,
PRIMARY KEY ("id")
);
-- user_auth_identities table does not exist.
CREATE SEQUENCE IF NOT EXISTS "user_auth_identities_id_seq";
CREATE TABLE "user_auth_identities" (
"id" bigint DEFAULT nextval('user_auth_identities_id_seq'::regclass) NOT NULL,
"user_id" bigint NOT NULL,
"provider" character varying(30) NOT NULL,
"provider_user_id" character varying(255) NOT NULL,
"email" character varying(255),
"display_name" character varying(80),
"avatar_url" character varying(500),
"last_login_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"is_delete" boolean DEFAULT false NOT NULL,
"password_hash" character varying(255),
PRIMARY KEY ("id")
);
COMMENT ON COLUMN "user_auth_identities"."id" IS '로그인 연결 정보 고유 ID';
COMMENT ON COLUMN "user_auth_identities"."user_id" IS '연결된 서비스 내부 사용자 ID';
COMMENT ON COLUMN "user_auth_identities"."provider" IS '로그인 제공자. guest, google, email, kakao, naver, github, apple';
COMMENT ON COLUMN "user_auth_identities"."provider_user_id" IS '로그인 제공자가 내려준 사용자 고유 ID. 게스트는 자체 생성 ID 사용';
COMMENT ON COLUMN "user_auth_identities"."email" IS '해당 로그인 제공자에서 받은 이메일 주소';
COMMENT ON COLUMN "user_auth_identities"."display_name" IS '해당 로그인 제공자에서 받은 표시 이름';
COMMENT ON COLUMN "user_auth_identities"."avatar_url" IS '해당 로그인 제공자에서 받은 프로필 이미지 URL';
COMMENT ON COLUMN "user_auth_identities"."last_login_at" IS '해당 로그인 방식으로 마지막 로그인한 시각';
COMMENT ON COLUMN "user_auth_identities"."created_at" IS '로그인 연결 정보 생성 시각';
COMMENT ON COLUMN "user_auth_identities"."updated_at" IS '로그인 연결 정보 마지막 수정 시각';
-- users table does not exist.
CREATE SEQUENCE IF NOT EXISTS "users_id_seq";
CREATE TABLE "users" (
"id" bigint DEFAULT nextval('users_id_seq'::regclass) NOT NULL,
"display_name" character varying(80) NOT NULL,
"canonical_email" character varying(255),
"avatar_url" character varying(500),
"role" character varying(30) DEFAULT 'USER'::character varying NOT NULL,
"status" character varying(30) DEFAULT 'ACTIVE'::character varying NOT NULL,
"last_login_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"is_delete" boolean DEFAULT false NOT NULL,
PRIMARY KEY ("id")
);
COMMENT ON COLUMN "users"."id" IS '사용자 고유 ID';
COMMENT ON COLUMN "users"."display_name" IS '서비스에서 표시할 사용자 이름';
COMMENT ON COLUMN "users"."canonical_email" IS '대표 이메일 주소. 여러 로그인 제공자 중 기준 이메일로 사용';
COMMENT ON COLUMN "users"."avatar_url" IS '사용자 프로필 이미지 URL';
COMMENT ON COLUMN "users"."role" IS '사용자 권한. USER 또는 ADMIN';
COMMENT ON COLUMN "users"."status" IS '계정 상태. ACTIVE, SUSPENDED, DELETED';
COMMENT ON COLUMN "users"."last_login_at" IS '마지막 로그인 시각';
COMMENT ON COLUMN "users"."created_at" IS '사용자 생성 시각';
COMMENT ON COLUMN "users"."updated_at" IS '사용자 정보 마지막 수정 시각';

View File

@ -396,7 +396,10 @@ class DbUpdateQueryGeneratorTest {
private static DbConfig loadDbConfig(String profile) throws IOException { private static DbConfig loadDbConfig(String profile) throws IOException {
Properties properties = new Properties(); Properties properties = new Properties();
String path = profile + "/db.properties"; String path = profile + "/db.properties";
try (InputStream inputStream = openDbConfig(path)) { try (InputStream inputStream = DbUpdateQueryGeneratorTest.class.getClassLoader().getResourceAsStream(path)) {
if (inputStream == null) {
throw new IOException("Cannot find " + path);
}
properties.load(inputStream); properties.load(inputStream);
} }
@ -408,20 +411,6 @@ class DbUpdateQueryGeneratorTest {
); );
} }
private static InputStream openDbConfig(String path) throws IOException {
InputStream inputStream = DbUpdateQueryGeneratorTest.class.getClassLoader().getResourceAsStream(path);
if (inputStream != null) {
return inputStream;
}
Path sourcePath = Path.of("src", "main", "resources").resolve(path);
if (Files.exists(sourcePath)) {
return Files.newInputStream(sourcePath);
}
throw new IOException("Cannot find " + path);
}
private static boolean sameValue(Object left, Object right) { private static boolean sameValue(Object left, Object right) {
if (left instanceof BigDecimal leftDecimal && right instanceof BigDecimal rightDecimal) { if (left instanceof BigDecimal leftDecimal && right instanceof BigDecimal rightDecimal) {
return leftDecimal.compareTo(rightDecimal) == 0; return leftDecimal.compareTo(rightDecimal) == 0;