From 46dda0e02aa4d11d950be622a0c892db91d7e5cb Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Tue, 23 Sep 2025 17:37:22 +0900 Subject: [PATCH] ... --- ads.txt | 1 + .../back/lun/configs/GlobalEnvironment.kt | 2 +- .../back/lun/configs/SecurityConfig.kt | 3 + .../back/lun/controllers/BlogController.kt | 8 +- .../lun/controllers/VisitorStatsController.kt | 18 +++ .../lunaticbum/back/lun/model/VisitorLog.kt | 120 ++++++++++++++++++ src/main/resources/static/ads.txt | 1 + src/main/resources/static/js/common.js | 2 +- .../templates/content/bookmarks.html | 9 ++ .../resources/templates/content/home.html | 39 +++--- .../resources/templates/content/posts.html | 73 +++++++---- .../templates/content/puzzle/2048.html | 11 +- .../templates/content/puzzle/nonogram.html | 11 +- .../templates/content/puzzle/spider.html | 9 ++ .../templates/content/puzzle/sudoku.html | 12 +- .../resources/templates/fragments/footer.html | 49 ++++++- 16 files changed, 313 insertions(+), 55 deletions(-) create mode 100644 ads.txt create mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/controllers/VisitorStatsController.kt create mode 100644 src/main/kotlin/kr/lunaticbum/back/lun/model/VisitorLog.kt create mode 100644 src/main/resources/static/ads.txt diff --git a/ads.txt b/ads.txt new file mode 100644 index 0000000..1acd2f4 --- /dev/null +++ b/ads.txt @@ -0,0 +1 @@ +google.com, pub-9504446465764716, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt index e6b97d7..c8a651c 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt @@ -14,7 +14,7 @@ class GlobalEnvironment : EnvironmentAware { val EncType10 = "T2" val EncType01 = "T1" val ApiKeyWordKey = "keyword" - private val pad = "%7C%2A-%2A%7C" + private val pad = "|*-*|" fun padding(key : String) = pad.plus(key).plus(pad) } @Value("\${telegram.bot.key}") diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt index 60d0784..fbed4ae 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt @@ -112,6 +112,8 @@ class SecurityConfig( .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } // API는 세션을 사용하지 않음 .authorizeHttpRequests { auth -> auth + .requestMatchers("/api/ranks/**").permitAll() + .requestMatchers("/api/stats/visitors").permitAll() .requestMatchers(HttpMethod.GET, "/api/images/**").permitAll() .requestMatchers("/api/auth/login").permitAll() // 로그인 API는 모두 허용 .anyRequest().authenticated() // 나머지 API는 인증 필요 @@ -174,6 +176,7 @@ class SecurityConfig( "/puzzle/images/**", "/bums/face.bs", // [추가] 사이트 소개 페이지 "/bookmarks/**", // [추가] 북마크 목록 페이지 + "/ads.txt" ).permitAll() // 3. 공개 POST API = permitAll diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt index 1490320..30c3fd9 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/BlogController.kt @@ -74,7 +74,8 @@ class BlogController( private val imageMetaService: ImageMetaService, private val logService: LogService, private val commentService: CommentService, - private val objectMapper: ObjectMapper // JSON 직렬화/역직렬화를 위해 추가 + private val objectMapper: ObjectMapper, // JSON 직렬화/역직렬화를 위해 추가 + private val visitorLogService: VisitorLogService ) { // --- Helper Properties & Data Classes --- @@ -301,7 +302,8 @@ class BlogController( */ @GetMapping("/", "/home.bs") @ResponseBody - suspend fun home(): ResultMV { + suspend fun home(request: jakarta.servlet.http.HttpServletRequest): ResultMV { + visitorLogService.recordVisit(request).subscribe() val vm = ResultMV("content/home") val defaultBannerImage = "/api/images/0e2bf8b1-1848-4650-b084-5b52d0815be9.jpg?type=banner" @@ -583,7 +585,7 @@ class BlogController( if (user == null) { return Mono.just(PostSaveResponse(401, "Authentication required", null)) } - + println("rawPayload ${rawPayload}") val incomingPost = PayloadDecoder.decode(rawPayload, Post::class.java, objectMapper) // 새 글 작성 diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/VisitorStatsController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/VisitorStatsController.kt new file mode 100644 index 0000000..9409c55 --- /dev/null +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/VisitorStatsController.kt @@ -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 { + return visitorLogService.getVisitorStats() + } +} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/VisitorLog.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/VisitorLog.kt new file mode 100644 index 0000000..222ca1c --- /dev/null +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/VisitorLog.kt @@ -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 { + 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 + 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 { + 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 { + // 특정 IP가 특정 기간 내에 방문한 기록이 있는지 확인 + fun findFirstByIpAddressAndVisitTimestampBetween(ipAddress: String, start: Long, end: Long): Mono + + // 특정 사용자가 특정 기간 내에 방문한 기록이 있는지 확인 + fun findFirstByUserIdAndVisitTimestampBetween(userId: String, start: Long, end: Long): Mono + + // 특정 시간 이후의 방문 기록 수 계산 + fun countByVisitTimestampGreaterThanEqual(timestamp: Long): Mono +} \ No newline at end of file diff --git a/src/main/resources/static/ads.txt b/src/main/resources/static/ads.txt new file mode 100644 index 0000000..1acd2f4 --- /dev/null +++ b/src/main/resources/static/ads.txt @@ -0,0 +1 @@ +google.com, pub-9504446465764716, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/src/main/resources/static/js/common.js b/src/main/resources/static/js/common.js index 10dc7c0..c44c6e9 100644 --- a/src/main/resources/static/js/common.js +++ b/src/main/resources/static/js/common.js @@ -989,7 +989,7 @@ function postLogin(target, type, data, key, callBackResult) { function unformat(type, data, key) { var even = [], odd = []; 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) { case "T0": return [odd.join(""), dividerStr, even.join("")].join(""); diff --git a/src/main/resources/templates/content/bookmarks.html b/src/main/resources/templates/content/bookmarks.html index bc51cc2..566968a 100644 --- a/src/main/resources/templates/content/bookmarks.html +++ b/src/main/resources/templates/content/bookmarks.html @@ -198,5 +198,14 @@ +
+ + +
\ No newline at end of file diff --git a/src/main/resources/templates/content/home.html b/src/main/resources/templates/content/home.html index 06686dc..90819bd 100644 --- a/src/main/resources/templates/content/home.html +++ b/src/main/resources/templates/content/home.html @@ -13,23 +13,32 @@ -
-
-
-

A gigantic heading you can use for whatever

-

With a much smaller subtitle hanging out just below it

-
-
-
-
-
-
-

Are you ready to continue your quest?

-
-
-
+ + + + + + + + + + + + + + +
+
+ + +
diff --git a/src/main/resources/templates/content/posts.html b/src/main/resources/templates/content/posts.html index 8ec5f52..3a0001a 100644 --- a/src/main/resources/templates/content/posts.html +++ b/src/main/resources/templates/content/posts.html @@ -16,36 +16,52 @@
-
-
- - Post Thumbnail - -
-

- - - 비공개 + +
+
+ + Post Thumbnail + +
+

+ + + 비공개 + + - - - - (읽음: 0) - -

-

- -

- + + (읽음: 0) + +

+

+

+
+
+
- -
-
+ +
+
+

- Advertisement -

+ + +
+
+
+ - + + diff --git a/src/main/resources/templates/content/puzzle/2048.html b/src/main/resources/templates/content/puzzle/2048.html index 53fd4c5..4f49a73 100644 --- a/src/main/resources/templates/content/puzzle/2048.html +++ b/src/main/resources/templates/content/puzzle/2048.html @@ -143,7 +143,7 @@ -

2048

+

화살표를 터치하거나 키보드 방향키를 이용해 타일을 합쳐보세요!

@@ -161,6 +161,15 @@
+
+ + +
+
+
\ No newline at end of file diff --git a/src/main/resources/templates/content/puzzle/sudoku.html b/src/main/resources/templates/content/puzzle/sudoku.html index 8678881..5173a0e 100644 --- a/src/main/resources/templates/content/puzzle/sudoku.html +++ b/src/main/resources/templates/content/puzzle/sudoku.html @@ -241,8 +241,6 @@
-

스도쿠를 즐겨보세요!

-