...
This commit is contained in:
parent
0ce20e4bf1
commit
46dda0e02a
1
ads.txt
Normal file
1
ads.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
google.com, pub-9504446465764716, DIRECT, f08c47fec0942fa0
|
||||||
@ -14,7 +14,7 @@ class GlobalEnvironment : EnvironmentAware {
|
|||||||
val EncType10 = "T2"
|
val EncType10 = "T2"
|
||||||
val EncType01 = "T1"
|
val EncType01 = "T1"
|
||||||
val ApiKeyWordKey = "keyword"
|
val ApiKeyWordKey = "keyword"
|
||||||
private val pad = "%7C%2A-%2A%7C"
|
private val pad = "|*-*|"
|
||||||
fun padding(key : String) = pad.plus(key).plus(pad)
|
fun padding(key : String) = pad.plus(key).plus(pad)
|
||||||
}
|
}
|
||||||
@Value("\${telegram.bot.key}")
|
@Value("\${telegram.bot.key}")
|
||||||
|
|||||||
@ -112,6 +112,8 @@ class SecurityConfig(
|
|||||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } // API는 세션을 사용하지 않음
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } // API는 세션을 사용하지 않음
|
||||||
.authorizeHttpRequests { auth ->
|
.authorizeHttpRequests { auth ->
|
||||||
auth
|
auth
|
||||||
|
.requestMatchers("/api/ranks/**").permitAll()
|
||||||
|
.requestMatchers("/api/stats/visitors").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/images/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/images/**").permitAll()
|
||||||
.requestMatchers("/api/auth/login").permitAll() // 로그인 API는 모두 허용
|
.requestMatchers("/api/auth/login").permitAll() // 로그인 API는 모두 허용
|
||||||
.anyRequest().authenticated() // 나머지 API는 인증 필요
|
.anyRequest().authenticated() // 나머지 API는 인증 필요
|
||||||
@ -174,6 +176,7 @@ class SecurityConfig(
|
|||||||
"/puzzle/images/**",
|
"/puzzle/images/**",
|
||||||
"/bums/face.bs", // [추가] 사이트 소개 페이지
|
"/bums/face.bs", // [추가] 사이트 소개 페이지
|
||||||
"/bookmarks/**", // [추가] 북마크 목록 페이지
|
"/bookmarks/**", // [추가] 북마크 목록 페이지
|
||||||
|
"/ads.txt"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
|
|
||||||
// 3. 공개 POST API = permitAll
|
// 3. 공개 POST API = permitAll
|
||||||
|
|||||||
@ -74,7 +74,8 @@ class BlogController(
|
|||||||
private val imageMetaService: ImageMetaService,
|
private val imageMetaService: ImageMetaService,
|
||||||
private val logService: LogService,
|
private val logService: LogService,
|
||||||
private val commentService: CommentService,
|
private val commentService: CommentService,
|
||||||
private val objectMapper: ObjectMapper // JSON 직렬화/역직렬화를 위해 추가
|
private val objectMapper: ObjectMapper, // JSON 직렬화/역직렬화를 위해 추가
|
||||||
|
private val visitorLogService: VisitorLogService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// --- Helper Properties & Data Classes ---
|
// --- Helper Properties & Data Classes ---
|
||||||
@ -301,7 +302,8 @@ class BlogController(
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/", "/home.bs")
|
@GetMapping("/", "/home.bs")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
suspend fun home(): ResultMV {
|
suspend fun home(request: jakarta.servlet.http.HttpServletRequest): ResultMV {
|
||||||
|
visitorLogService.recordVisit(request).subscribe()
|
||||||
val vm = ResultMV("content/home")
|
val vm = ResultMV("content/home")
|
||||||
val defaultBannerImage = "/api/images/0e2bf8b1-1848-4650-b084-5b52d0815be9.jpg?type=banner"
|
val defaultBannerImage = "/api/images/0e2bf8b1-1848-4650-b084-5b52d0815be9.jpg?type=banner"
|
||||||
|
|
||||||
@ -583,7 +585,7 @@ class BlogController(
|
|||||||
if (user == null) {
|
if (user == null) {
|
||||||
return Mono.just(PostSaveResponse(401, "Authentication required", null))
|
return Mono.just(PostSaveResponse(401, "Authentication required", null))
|
||||||
}
|
}
|
||||||
|
println("rawPayload ${rawPayload}")
|
||||||
val incomingPost = PayloadDecoder.decode(rawPayload, Post::class.java, objectMapper)
|
val incomingPost = PayloadDecoder.decode(rawPayload, Post::class.java, objectMapper)
|
||||||
|
|
||||||
// 새 글 작성
|
// 새 글 작성
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
package kr.lunaticbum.back.lun.controllers
|
||||||
|
|
||||||
|
import kr.lunaticbum.back.lun.model.VisitorLogService
|
||||||
|
import kr.lunaticbum.back.lun.model.VisitorStatsDto
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/stats")
|
||||||
|
class VisitorStatsController(private val visitorLogService: VisitorLogService) {
|
||||||
|
|
||||||
|
@GetMapping("/visitors")
|
||||||
|
fun getVisitorStatistics(): Mono<VisitorStatsDto> {
|
||||||
|
return visitorLogService.getVisitorStats()
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/main/kotlin/kr/lunaticbum/back/lun/model/VisitorLog.kt
Normal file
120
src/main/kotlin/kr/lunaticbum/back/lun/model/VisitorLog.kt
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package kr.lunaticbum.back.lun.model
|
||||||
|
|
||||||
|
import org.bson.BsonType
|
||||||
|
import org.bson.codecs.pojo.annotations.BsonId
|
||||||
|
import org.bson.codecs.pojo.annotations.BsonRepresentation
|
||||||
|
import org.springframework.data.mongodb.core.index.CompoundIndex
|
||||||
|
import org.springframework.data.mongodb.core.index.Indexed
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.temporal.TemporalAdjusters
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class VisitorLogService(private val repository: VisitorLogRepository) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 방문을 기록합니다. (하루에 한 번만)
|
||||||
|
*/
|
||||||
|
fun recordVisit(request: HttpServletRequest): Mono<Void> {
|
||||||
|
val ipAddress = request.remoteAddr
|
||||||
|
val user = SecurityContextHolder.getContext().authentication
|
||||||
|
|
||||||
|
val today = LocalDate.now(ZoneId.systemDefault())
|
||||||
|
val startOfDay = today.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
|
val endOfDay = today.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
|
|
||||||
|
val hasVisitedToday: Mono<Boolean>
|
||||||
|
val visitorLog: VisitorLog
|
||||||
|
|
||||||
|
if (user != null && user.isAuthenticated && user.principal is org.springframework.security.core.userdetails.UserDetails) {
|
||||||
|
val userDetails = user.principal as org.springframework.security.core.userdetails.UserDetails
|
||||||
|
val userId = userDetails.username
|
||||||
|
hasVisitedToday = repository.findFirstByUserIdAndVisitTimestampBetween(userId, startOfDay, endOfDay).hasElement()
|
||||||
|
visitorLog = VisitorLog(ipAddress = ipAddress, userId = userId)
|
||||||
|
} else {
|
||||||
|
hasVisitedToday = repository.findFirstByIpAddressAndVisitTimestampBetween(ipAddress, startOfDay, endOfDay).hasElement()
|
||||||
|
visitorLog = VisitorLog(ipAddress = ipAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasVisitedToday.flatMap { visited ->
|
||||||
|
if (!visited) {
|
||||||
|
repository.save(visitorLog).then()
|
||||||
|
} else {
|
||||||
|
Mono.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방문자 통계를 계산하여 반환합니다.
|
||||||
|
*/
|
||||||
|
fun getVisitorStats(): Mono<VisitorStatsDto> {
|
||||||
|
val now = LocalDate.now(ZoneId.systemDefault())
|
||||||
|
|
||||||
|
val todayStart = now.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
|
val weekStart = now.with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY)).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
|
val monthStart = now.withDayOfMonth(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
|
val yearStart = now.withDayOfYear(1).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||||
|
|
||||||
|
val todayCountMono = repository.countByVisitTimestampGreaterThanEqual(todayStart)
|
||||||
|
val weekCountMono = repository.countByVisitTimestampGreaterThanEqual(weekStart)
|
||||||
|
val monthCountMono = repository.countByVisitTimestampGreaterThanEqual(monthStart)
|
||||||
|
val yearCountMono = repository.countByVisitTimestampGreaterThanEqual(yearStart)
|
||||||
|
val totalCountMono = repository.count()
|
||||||
|
|
||||||
|
return Mono.zip(todayCountMono, weekCountMono, monthCountMono, yearCountMono, totalCountMono)
|
||||||
|
.map { tuple ->
|
||||||
|
VisitorStatsDto(
|
||||||
|
today = tuple.t1,
|
||||||
|
week = tuple.t2,
|
||||||
|
month = tuple.t3,
|
||||||
|
year = tuple.t4,
|
||||||
|
total = tuple.t5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 방문 기록을 저장할 MongoDB 문서
|
||||||
|
@Document(collection = "VisitorLog")
|
||||||
|
// IP와 날짜, 또는 사용자와 날짜로 중복 조회를 빠르게 하기 위해 인덱스 추가
|
||||||
|
@CompoundIndex(name = "ip_timestamp_idx", def = "{'ipAddress': 1, 'visitTimestamp': -1}")
|
||||||
|
@CompoundIndex(name = "user_timestamp_idx", def = "{'userId': 1, 'visitTimestamp': -1}")
|
||||||
|
data class VisitorLog(
|
||||||
|
@BsonId
|
||||||
|
@BsonRepresentation(BsonType.OBJECT_ID)
|
||||||
|
var id: String? = null,
|
||||||
|
val ipAddress: String,
|
||||||
|
val userId: String? = null, // 로그인 사용자일 경우 ID 저장
|
||||||
|
@Indexed
|
||||||
|
val visitTimestamp: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
// API 응답으로 사용할 데이터 클래스
|
||||||
|
data class VisitorStatsDto(
|
||||||
|
val today: Long,
|
||||||
|
val week: Long,
|
||||||
|
val month: Long,
|
||||||
|
val year: Long,
|
||||||
|
val total: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
// VisitorLog 데이터 접근을 위한 리포지토리
|
||||||
|
@Repository
|
||||||
|
interface VisitorLogRepository : ReactiveMongoRepository<VisitorLog, String> {
|
||||||
|
// 특정 IP가 특정 기간 내에 방문한 기록이 있는지 확인
|
||||||
|
fun findFirstByIpAddressAndVisitTimestampBetween(ipAddress: String, start: Long, end: Long): Mono<VisitorLog>
|
||||||
|
|
||||||
|
// 특정 사용자가 특정 기간 내에 방문한 기록이 있는지 확인
|
||||||
|
fun findFirstByUserIdAndVisitTimestampBetween(userId: String, start: Long, end: Long): Mono<VisitorLog>
|
||||||
|
|
||||||
|
// 특정 시간 이후의 방문 기록 수 계산
|
||||||
|
fun countByVisitTimestampGreaterThanEqual(timestamp: Long): Mono<Long>
|
||||||
|
}
|
||||||
1
src/main/resources/static/ads.txt
Normal file
1
src/main/resources/static/ads.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
google.com, pub-9504446465764716, DIRECT, f08c47fec0942fa0
|
||||||
@ -989,7 +989,7 @@ function postLogin(target, type, data, key, callBackResult) {
|
|||||||
function unformat(type, data, key) {
|
function unformat(type, data, key) {
|
||||||
var even = [], odd = [];
|
var even = [], odd = [];
|
||||||
data.split("").forEach((v, idx) => (idx % 2 === 0 ? even.push(v) : odd.push(v)));
|
data.split("").forEach((v, idx) => (idx % 2 === 0 ? even.push(v) : odd.push(v)));
|
||||||
const dividerStr = ["%7C%2A-%2A%7C", key, "%7C%2A-%2A%7C"].join("");
|
const dividerStr = ["|*-*|", key, "|*-*|"].join("");
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "T0":
|
case "T0":
|
||||||
return [odd.join(""), dividerStr, even.join("")].join("");
|
return [odd.join(""), dividerStr, even.join("")].join("");
|
||||||
|
|||||||
@ -198,5 +198,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-9504446465764716" data-ad-slot="1234567890" data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
@ -13,23 +13,32 @@
|
|||||||
</header>
|
</header>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="wrapper style2">
|
<!-- <section class="wrapper style2">-->
|
||||||
<div class="container">
|
<!-- <div class="container">-->
|
||||||
<header class="major">
|
<!-- <header class="major">-->
|
||||||
<h2>A gigantic heading you can use for whatever</h2>
|
<!-- <h2>A gigantic heading you can use for whatever</h2>-->
|
||||||
<p>With a much smaller subtitle hanging out just below it</p>
|
<!-- <p>With a much smaller subtitle hanging out just below it</p>-->
|
||||||
</header>
|
<!-- </header>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</section>
|
<!-- </section>-->
|
||||||
<section id="cta2" class="wrapper style3">
|
<!-- <section id="cta2" class="wrapper style3">-->
|
||||||
<div class="container">
|
<!-- <div class="container">-->
|
||||||
<header>
|
<!-- <header>-->
|
||||||
<h2>Are you ready to continue your quest?</h2>
|
<!-- <h2>Are you ready to continue your quest?</h2>-->
|
||||||
</header>
|
<!-- </header>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</section>
|
<!-- </section>-->
|
||||||
|
|
||||||
<section class="wrapper style1">
|
<section class="wrapper style1">
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-9504446465764716" data-ad-slot="1234567890" data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@ -16,7 +16,8 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div id="content_inner">
|
<div id="content_inner">
|
||||||
<article>
|
<article>
|
||||||
<section th:each="post : ${postsPage.content}">
|
<th:block th:each="post, iterStat : ${postsPage.content}">
|
||||||
|
<section>
|
||||||
<div class="box post" th:id="${post.id}">
|
<div class="box post" th:id="${post.id}">
|
||||||
<a href="javascript:void(0);" th:onclick="'goToViewer(this.parentNode)'" class="image left">
|
<a href="javascript:void(0);" th:onclick="'goToViewer(this.parentNode)'" class="image left">
|
||||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? ${apiBaseUrl + post.thumb} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? ${apiBaseUrl + post.thumb} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||||
@ -34,11 +35,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<p style="font-size: 0.9em; color: #555; margin-bottom: 0.5em;" th:text="${'by ' + post.writer}"></p>
|
<p style="font-size: 0.9em; color: #555; margin-bottom: 0.5em;" th:text="${'by ' + post.writer}"></p>
|
||||||
|
|
||||||
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
|
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer sec:authorize="isAuthenticated()"
|
<footer sec:authorize="isAuthenticated()"
|
||||||
th:if="${#authentication.name == post.writer or #authorization.expression('hasRole(''ADMIN'')')}"
|
th:if="${#authentication.name == post.writer or #authorization.expression('hasRole(''ADMIN'')')}"
|
||||||
style="text-align: right; margin-top: 1em;">
|
style="text-align: right; margin-top: 1em;">
|
||||||
@ -46,6 +44,24 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<th:block th:if="${iterStat.count % 3 == 0}">
|
||||||
|
<section>
|
||||||
|
<div class="box" style="padding: 2em; text-align: center;">
|
||||||
|
<p style="margin-bottom: 1em; color: #888;">- Advertisement -</p>
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-xxxxxxxxxxxxxxxx"
|
||||||
|
data-ad-slot="yyyyyyyyyy"
|
||||||
|
data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<nav aria-label="Page navigation" th:if="${postsPage.totalPages > 1}" style="text-align: center; margin-top: 2.5em; font-size: 0.9em;">
|
<nav aria-label="Page navigation" th:if="${postsPage.totalPages > 1}" style="text-align: center; margin-top: 2.5em; font-size: 0.9em;">
|
||||||
@ -72,7 +88,8 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div> </div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -143,7 +143,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
|
|
||||||
<div class="game-body-wrapper"> <h1>2048</h1>
|
<div class="game-body-wrapper">
|
||||||
<p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p>
|
<p>화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!</p>
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
<div class="score-container">
|
<div class="score-container">
|
||||||
@ -161,6 +161,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-9504446465764716" data-ad-slot="1234567890" data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.pageContext = { pageType: 'game', gameType: 'GAME_2048', contextId: null };
|
window.pageContext = { pageType: 'game', gameType: 'GAME_2048', contextId: null };
|
||||||
|
|
||||||
|
|||||||
@ -294,8 +294,6 @@
|
|||||||
</th:block >
|
</th:block >
|
||||||
|
|
||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<h1>Solve the Puzzle! 🧩</h1>
|
|
||||||
|
|
||||||
<div id="game-controls">
|
<div id="game-controls">
|
||||||
<div id="mode-selector">
|
<div id="mode-selector">
|
||||||
<label>
|
<label>
|
||||||
@ -327,6 +325,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-9504446465764716" data-ad-slot="1234567890" data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
|
|||||||
@ -824,5 +824,14 @@
|
|||||||
<canvas id="gameCanvas"></canvas>
|
<canvas id="gameCanvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-9504446465764716" data-ad-slot="1234567890" data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
@ -241,8 +241,6 @@
|
|||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div id="sudoku-game-app">
|
<div id="sudoku-game-app">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>스도쿠를 즐겨보세요!</h1>
|
|
||||||
|
|
||||||
<div id="board-area">
|
<div id="board-area">
|
||||||
<div id="setup-container">
|
<div id="setup-container">
|
||||||
<select id="difficulty-select">
|
<select id="difficulty-select">
|
||||||
@ -299,7 +297,15 @@
|
|||||||
<button id="retry-btn">새 게임 시작</button>
|
<button id="retry-btn">새 게임 시작</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container" style="text-align:center;">
|
||||||
|
<ins class="adsbygoogle"
|
||||||
|
style="display:block"
|
||||||
|
data-ad-client="ca-pub-9504446465764716" data-ad-slot="1234567890" data-ad-format="auto"
|
||||||
|
data-full-width-responsive="true"></ins>
|
||||||
|
<script>
|
||||||
|
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
window.pageContext = { pageType: 'game', gameType: 'SUDOKU', contextId: undefined };
|
window.pageContext = { pageType: 'game', gameType: 'SUDOKU', contextId: undefined };
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<html xmlns:th="http://www.thymeleaf.org"
|
<html xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">>
|
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">>
|
||||||
<th:block th:fragment="footer">
|
<th:block th:fragment="footer">
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
function callSendTlg() {
|
function callSendTlg() {
|
||||||
@ -63,6 +64,13 @@
|
|||||||
<li><a href="#" class="icon brands fa-google-plus-g"><span class="label">Google+</span></a></li>
|
<li><a href="#" class="icon brands fa-google-plus-g"><span class="label">Google+</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class="visitor-stats-inline">
|
||||||
|
Today: <span id="visitor-today">...</span> |
|
||||||
|
Week: <span id="visitor-week">...</span> |
|
||||||
|
Month: <span id="visitor-month">...</span> |
|
||||||
|
Year: <span id="visitor-year">...</span> |
|
||||||
|
Total: <span id="visitor-total">...</span>
|
||||||
|
</div>
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<ul class="menu">
|
<ul class="menu">
|
||||||
@ -112,6 +120,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchVisitorStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/stats/visitors');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('방문자 통계 조회 실패');
|
||||||
|
}
|
||||||
|
const stats = await response.json();
|
||||||
|
|
||||||
|
// 숫자를 콤마 포맷으로 변경하여 화면에 표시
|
||||||
|
document.getElementById('visitor-today').textContent = stats.today.toLocaleString();
|
||||||
|
document.getElementById('visitor-week').textContent = stats.week.toLocaleString();
|
||||||
|
document.getElementById('visitor-month').textContent = stats.month.toLocaleString();
|
||||||
|
document.getElementById('visitor-year').textContent = stats.year.toLocaleString();
|
||||||
|
document.getElementById('visitor-total').textContent = stats.total.toLocaleString();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('방문자 통계 업데이트 중 오류:', error);
|
||||||
|
// 오류 발생 시 모든 통계 필드에 '오류' 표시
|
||||||
|
document.querySelectorAll('.visitor-stats span').forEach(el => el.textContent = '오류');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- 푸터 메인 로직 ---
|
// --- 푸터 메인 로직 ---
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// 현재 페이지가 스스로를 게임 페이지로 정의했는지 확인
|
// 현재 페이지가 스스로를 게임 페이지로 정의했는지 확인
|
||||||
@ -127,9 +157,26 @@
|
|||||||
const rankingList = document.querySelector('.rank_of_view');
|
const rankingList = document.querySelector('.rank_of_view');
|
||||||
if(rankingList) rankingList.innerHTML = '<li>블로그 순위가 여기에 표시됩니다.</li>';
|
if(rankingList) rankingList.innerHTML = '<li>블로그 순위가 여기에 표시됩니다.</li>';
|
||||||
fetchRankOfViews();
|
fetchRankOfViews();
|
||||||
|
fetchVisitorStats();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.visitor-stats-inline {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 2em;
|
||||||
|
margin-top: 2em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #999;
|
||||||
|
border-top: solid 1px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.visitor-stats-inline span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #999;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</th:block>
|
</th:block>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user