...
This commit is contained in:
parent
d0abec2f4b
commit
3a0a29ec02
@ -10,6 +10,7 @@ import kr.lunaticbum.back.lun.configs.GlobalEnvironment.Companion.EncTypeKey
|
||||
import kr.lunaticbum.back.lun.model.UserManager
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.lang.Nullable
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.RememberMeServices
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.stereotype.Service
|
||||
@ -47,12 +48,11 @@ class BumsInterceptor : HandlerInterceptor {
|
||||
handler: Any,
|
||||
@Nullable modelAndView: ModelAndView?
|
||||
) {
|
||||
// if(remeberMe && authResult != null) {
|
||||
// rememberMeServices.loginSuccess(httpServletRequest, responce, authResult)
|
||||
// }
|
||||
|
||||
|
||||
modelAndView?.modelMap?.put(EncTypeKey, EncType11)
|
||||
modelAndView?.modelMap?.put(ApiKeyWordKey,"Def")
|
||||
|
||||
if (modelAndView != null) {
|
||||
modelAndView.modelMap.put(EncTypeKey, EncType11)
|
||||
modelAndView.modelMap.put(ApiKeyWordKey, "Def")
|
||||
|
||||
@ -3,6 +3,7 @@ package kr.lunaticbum.back.lun.configs
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import kr.lunaticbum.back.lun.model.MongoPersistentTokenRepository
|
||||
import kr.lunaticbum.back.lun.model.UserManager
|
||||
import kr.lunaticbum.back.lun.utils.LogService
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@ -23,14 +24,20 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.access.AccessDeniedHandler
|
||||
import org.springframework.security.web.authentication.RememberMeServices
|
||||
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
|
||||
import org.springframework.web.ErrorResponse
|
||||
import javax.sql.DataSource
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig(
|
||||
private val userManager: UserManager,
|
||||
private val bCryptPasswordEncoder: BCryptPasswordEncoder
|
||||
private val bCryptPasswordEncoder: BCryptPasswordEncoder,
|
||||
private val tokenRepository: MongoPersistentTokenRepository
|
||||
) {
|
||||
@Autowired
|
||||
lateinit var logService: LogService
|
||||
@ -42,6 +49,22 @@ class SecurityConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// RememberMeServices를 Bean으로 생성하고 필드에 할당하거나, 생성자 주입을 할 수 있음
|
||||
|
||||
|
||||
@Bean
|
||||
fun rememberMeServices(): RememberMeServices {
|
||||
val key = "your-remember-me-key"
|
||||
return PersistentTokenBasedRememberMeServices(key, userManager,
|
||||
tokenRepository as PersistentTokenRepository?
|
||||
).apply {
|
||||
setParameter("remember-me") // 기본 파라미터명
|
||||
setTokenValiditySeconds(86400) // 토큰 유효시간 설정
|
||||
// 필요시 setAlwaysRemember(true) 등 추가 설정 가능
|
||||
println("CALLED rememberMeServices")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http.csrf { csrf ->
|
||||
@ -68,8 +91,9 @@ class SecurityConfig(
|
||||
.defaultSuccessUrl("/", true)
|
||||
.permitAll()
|
||||
}.rememberMe { rememberMe ->
|
||||
rememberMe
|
||||
.key("BsTs*!12@") // 보통 안전한 키 지정
|
||||
rememberMe.rememberMeServices(rememberMeServices())
|
||||
.key("remember-BsTs*!12@") // 보통 안전한 키 지정
|
||||
.tokenRepository(tokenRepository)
|
||||
.tokenValiditySeconds(60 * 60 * 24 * 7) // 7일간 유효
|
||||
.userDetailsService(userManager) // 사용자 정보 서비스 지정
|
||||
}.logout { logout ->
|
||||
@ -86,43 +110,6 @@ class SecurityConfig(
|
||||
.passwordEncoder(bCryptPasswordEncoder)
|
||||
return authenticationManagerBuilder.build() // .and() 없이 직접 build() 호출
|
||||
}
|
||||
// @Bean
|
||||
// fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
//
|
||||
// http.csrf {
|
||||
// it.ignoringRequestMatchers("/user/joinUser.bjx").disable()
|
||||
// }
|
||||
// http.cors { it.disable() }
|
||||
// http.headers {
|
||||
// it.frameOptions { frameOptionsConfig ->
|
||||
// frameOptionsConfig.disable()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// http.authorizeHttpRequests {
|
||||
// logService.log(it.toString())
|
||||
// it.requestMatchers(HttpMethod.POST,"/user/**").permitAll()
|
||||
// it.requestMatchers("/blog/viewer/**").permitAll()
|
||||
// it.anyRequest().permitAll()
|
||||
// }
|
||||
// http.sessionManagement {
|
||||
// it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
// }
|
||||
// .exceptionHandling { it ->
|
||||
// it.authenticationEntryPoint(unauthorizedEntryPoint)
|
||||
// .accessDeniedHandler(accessDeniedHandler)
|
||||
// }
|
||||
//// .formLogin { formLogin ->
|
||||
//// formLogin
|
||||
//// .loginPage("/user/join")
|
||||
//////// .usernameParameter("username")
|
||||
//////// .passwordParameter("password")
|
||||
//// .loginProcessingUrl("/user/joinUser.api")
|
||||
//// .defaultSuccessUrl("/", true)
|
||||
//// }
|
||||
//
|
||||
// return http.build()
|
||||
// }
|
||||
|
||||
private val unauthorizedEntryPoint =
|
||||
AuthenticationEntryPoint { request: HttpServletRequest?, response: HttpServletResponse, authException: AuthenticationException? ->
|
||||
|
||||
@ -176,7 +176,13 @@ class BlogController() {
|
||||
val vm = ResultMV("content/blog/viewer")
|
||||
postManager.getPost(postId).block().apply {
|
||||
this?.title = URLDecoder.decode(this?.title)
|
||||
this?.content = URLDecoder.decode(this?.content)
|
||||
println("this?.content >>> ${this?.content}")
|
||||
if (this?.content is String){
|
||||
this?.content = URLDecoder.decode(this?.content)
|
||||
} else {
|
||||
this?.content = Gson().toJson(this?.content)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
package kr.lunaticbum.back.lun.controllers
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kr.lunaticbum.back.lun.model.PostManager
|
||||
import kr.lunaticbum.back.lun.model.ResultMV
|
||||
import kr.lunaticbum.back.lun.utils.LogService
|
||||
@ -31,24 +35,68 @@ class Home {
|
||||
@Autowired
|
||||
private lateinit var postManager: PostManager
|
||||
|
||||
data class PostView(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val thumb: String?,
|
||||
val writeTime: Long,
|
||||
val textOnly: String,
|
||||
val firstImage: String?
|
||||
)
|
||||
|
||||
data class DeltaOp(val insert: Any)
|
||||
data class Delta(val ops: List<DeltaOp>)
|
||||
|
||||
fun extractFromDelta(deltaJson: String): Pair<String, String?> {
|
||||
|
||||
val delta: Delta = Gson().fromJson(deltaJson, Delta::class.java)
|
||||
|
||||
var textOnly = StringBuilder()
|
||||
var firstImage: String? = null
|
||||
|
||||
delta.ops.forEach { op ->
|
||||
if (op.insert is String) {
|
||||
textOnly.append(op.insert)
|
||||
} else if (op.insert is Map<*, *>) {
|
||||
val obj = op.insert as Map<*, *>
|
||||
if (obj["image"] != null && firstImage == null) {
|
||||
firstImage = obj["image"].toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
return textOnly.toString() to firstImage
|
||||
}
|
||||
|
||||
@GetMapping("/","/home.bs")
|
||||
fun home() : ResultMV {
|
||||
suspend fun home() : ResultMV {
|
||||
val vm = ResultMV("content/home")
|
||||
try {
|
||||
vm.modelMap.put("Posts", postManager.find4().apply {
|
||||
vm.modelMap.put("Posts", postManager.find8().apply {
|
||||
this.forEach {
|
||||
it.title = URLDecoder.decode(it.title)
|
||||
it.content = URLDecoder.decode(it.content)
|
||||
val parser: Parser = Parser.builder().build()
|
||||
val document: Node = parser.parse(it.content)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
Jsoup.parse(renderer.render(document))?.let { doc ->
|
||||
val firstImg: Element? = doc.select("img")?.first()
|
||||
val imgSrc: String = firstImg?.attr("src") ?: ""
|
||||
it.image = imgSrc
|
||||
it.thumb = imgSrc.replace(imgSrc.split("/").last(), imgSrc.split("/").last().replace(".","_thumbnail."))
|
||||
generateThumbnail(imgSrc.split("/").last(), 200)
|
||||
it.html = doc.text()
|
||||
println("content >>> ${it.content}")
|
||||
try {
|
||||
JsonParser.parseString(it.content)
|
||||
it.content?.let { content ->
|
||||
var delta = extractFromDelta(content)
|
||||
val firstImg = delta.second
|
||||
it.image = firstImg ?: "images/pic01.jpg"
|
||||
it.thumb = firstImg ?: "images/pic01.jpg"
|
||||
it.html = delta.first
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Jsoup.parse(renderer.render(document))?.let { doc ->
|
||||
val firstImg: Element? = doc.select("img")?.first()
|
||||
val imgSrc: String = firstImg?.attr("src") ?: ""
|
||||
it.image = imgSrc
|
||||
it.thumb = imgSrc.replace(imgSrc.split("/").last(), imgSrc.split("/").last().replace(".","_thumbnail."))
|
||||
generateThumbnail(imgSrc.split("/").last(), 200)
|
||||
it.html = doc.text()
|
||||
}
|
||||
}
|
||||
it.title = if ((it.title?.length ?: 0) >= 1) it.title else ""
|
||||
}
|
||||
|
||||
@ -77,7 +77,11 @@ class Telegram {
|
||||
jsonString.extractModelData { exception, originDataString ->
|
||||
if (exception == null) {
|
||||
Gson().fromJson<ReportModel>(originDataString, ReportModel::class.java)?.let { msg ->
|
||||
sendMsg("${msg.name}님이 전송\n${msg.message}\n회신가능 메일${msg.email}")
|
||||
WebClient.create().get()
|
||||
.uri("https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage?chat_id=${globalEvv.telegramMyId}&text=${msg.name}님이 전송\n${msg.message}\n회신가능 메일${msg.email}")
|
||||
.retrieve()
|
||||
.bodyToMono(String::class.java).block() ?: "FAIL"
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,9 @@ import javax.naming.AuthenticationException
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
class UserController {
|
||||
class UserController(
|
||||
private val rememberMeServices: RememberMeServices
|
||||
) {
|
||||
|
||||
|
||||
@Autowired
|
||||
@ -84,17 +86,17 @@ class UserController {
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("login.bjx")
|
||||
fun login(httpServletRequest: HttpServletRequest, @RequestBody jsonString: String) : ResponseEntity<LoginResult> {
|
||||
fun login(httpServletRequest: HttpServletRequest,
|
||||
httpRes: HttpServletResponse,
|
||||
@RequestBody jsonString: String) : ResponseEntity<LoginResult> {
|
||||
var loginResult = LoginResult().apply {
|
||||
this.isOk = false
|
||||
this.resultCode = 9999
|
||||
this.resultMsg = "JUST INITIALIZED"
|
||||
}
|
||||
try {
|
||||
|
||||
logService.log(httpServletRequest.requestURI)
|
||||
logService.log(jsonString)
|
||||
var lResultCode = 0
|
||||
var lResultMsg = "Suscces"
|
||||
var u : UserDetails? = null
|
||||
var user : User? = null
|
||||
var tokenData : TokenData? = null
|
||||
var remeberMe = false
|
||||
var authResult : Authentication? = null
|
||||
jsonString.extractModelData { exception, originDataString ->
|
||||
if (exception == null) {
|
||||
@ -107,6 +109,7 @@ class UserController {
|
||||
// 인증 성공 시 SecurityContextHolder에 인증 정보 저장
|
||||
SecurityContextHolder.getContext().authentication = authResult
|
||||
|
||||
|
||||
// 인증 정보가 담긴 SecurityContext를 세션에 저장
|
||||
val session = httpServletRequest.getSession(true)
|
||||
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext())
|
||||
@ -114,11 +117,11 @@ class UserController {
|
||||
|
||||
val principal = authResult?.principal
|
||||
if (principal is UserDetails) {
|
||||
val username = principal.username
|
||||
|
||||
val session = httpServletRequest.getSession(true)
|
||||
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext())
|
||||
remeberMe = target.remeberMe ?: false
|
||||
println("target.remeberMe >>> ${target.rememberMe}")
|
||||
loginResult.rememberMe = target.rememberMe
|
||||
if (target.rememberMe == true) {
|
||||
rememberMeServices.loginSuccess(httpServletRequest, httpRes, authResult)
|
||||
}
|
||||
} else {
|
||||
lResultMsg = "is wrong infomation id or passord"
|
||||
lResultCode = 7100
|
||||
@ -134,11 +137,10 @@ class UserController {
|
||||
}
|
||||
val responce = ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).headers {
|
||||
|
||||
}.body(LoginResult().apply {
|
||||
}.body(loginResult.apply {
|
||||
this.isOk = lResultCode == 0
|
||||
this.resultCode = lResultCode
|
||||
this.resultMsg = lResultMsg
|
||||
this.token = if (remeberMe) "OK" else ""
|
||||
}).apply {
|
||||
|
||||
}
|
||||
@ -151,8 +153,6 @@ class UserController {
|
||||
this.isOk = false
|
||||
this.resultCode = -999
|
||||
this.resultMsg = e.message ?: "unknown exception"
|
||||
this.token = ""
|
||||
this.refresh = ""
|
||||
}).apply {
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import kr.lunaticbum.back.lun.utils.LogService
|
||||
import lombok.AllArgsConstructor
|
||||
import lombok.Data
|
||||
import lombok.NoArgsConstructor
|
||||
import okio.Timeout
|
||||
import org.bson.BsonType
|
||||
import org.bson.codecs.pojo.annotations.BsonId
|
||||
import org.bson.codecs.pojo.annotations.BsonRepresentation
|
||||
@ -15,6 +16,7 @@ import org.springframework.data.mongodb.core.mapping.Document
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.data.mongodb.core.query.Query
|
||||
import org.springframework.data.mongodb.core.query.Update
|
||||
import org.springframework.data.mongodb.repository.Aggregation
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Repository
|
||||
@ -101,6 +103,14 @@ interface PostRepository : ReactiveMongoRepository<Post, String> {
|
||||
fun countByOrderByModifyTimeDesc(): Mono<Long>
|
||||
fun findTop5ByOrderByReadCountDesc(): Flux<Post>
|
||||
fun findTop5ByOrderByModifyTimeDesc(): Flux<Post>
|
||||
@Aggregation(pipeline = [
|
||||
"{ \$sort: { modifyTime: -1 } }",
|
||||
"{ \$group: { _id: \"\$originId\", post: { \$first: \"\$\$ROOT\" } } }",
|
||||
"{ \$sort: { \"post.modifyTime\": -1 } }",
|
||||
"{ \$limit: 8 }",
|
||||
"{ \$replaceRoot: { newRoot: \"\$post\" } }"
|
||||
])
|
||||
fun findLatestUniqueOrigin(): Flux<Post>
|
||||
}
|
||||
|
||||
|
||||
@ -159,16 +169,9 @@ class PostManager(
|
||||
|
||||
|
||||
|
||||
fun find4() : List<Post> {
|
||||
val originalList = postRepository.findAllByModifyTime(0)
|
||||
.takeLast(4)
|
||||
.buffer(4)
|
||||
.blockLast(Duration.ofSeconds(30)) ?: listOf()
|
||||
|
||||
return originalList + List((4 - originalList.size).coerceAtLeast(0)) {
|
||||
Post() // 기본값 생성 (필드 초기화 필요)
|
||||
}
|
||||
// return postRepository.findAllByModifyTime(0).takeLast(4).buffer(4).blockLast(Duration.ofSeconds(30)) ?: listOf()
|
||||
fun find8() : List<Post> {
|
||||
return postRepository.findLatestUniqueOrigin().collectList() // Mono<List<Post>>로 변환 // Flux<Post> → Mono<List<Post>>
|
||||
.block(Duration.ofSeconds(30)) ?: emptyList()
|
||||
}
|
||||
|
||||
fun find20() : List<Post> {
|
||||
|
||||
@ -16,8 +16,7 @@ open class PostsResult : BaseResult() {
|
||||
|
||||
@Getter
|
||||
open class LoginResult : ResponceResult() {
|
||||
var token: String? = null
|
||||
var refresh: String? = null
|
||||
var rememberMe: Boolean? = null
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@ -7,7 +7,11 @@ import org.springframework.data.annotation.Id
|
||||
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.security.web.authentication.rememberme.PersistentRememberMeToken
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
@ -38,4 +42,68 @@ class TokenData {
|
||||
interface TokenDataRepository : ReactiveMongoRepository<TokenData, String> {
|
||||
fun findBytokenKey(tokenKey: String): Mono<TokenData>
|
||||
fun deleteBytokenKey(tokenKey: String)
|
||||
}
|
||||
}
|
||||
interface PersistentTokenRepository {
|
||||
fun createNewToken(token: PersistentRememberMeToken)
|
||||
fun updateToken(series: String, tokenValue: String, lastUsed: Date)
|
||||
fun getTokenForSeries(seriesId: String): PersistentRememberMeToken?
|
||||
fun removeUserTokens(username: String)
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Document(collection = "persistent_logins")
|
||||
data class PersistentLogin(
|
||||
@Id val series: String,
|
||||
val username: String,
|
||||
val tokenValue: String,
|
||||
val lastUsed: Date
|
||||
)
|
||||
|
||||
interface PersistentLoginRepository : ReactiveMongoRepository<PersistentLogin, String> {
|
||||
fun findByUsername(username: String): Flux<PersistentLogin>
|
||||
}
|
||||
|
||||
@Component
|
||||
class MongoPersistentTokenRepository (
|
||||
private val repository: PersistentLoginRepository
|
||||
) : PersistentTokenRepository {
|
||||
|
||||
override fun createNewToken(token: PersistentRememberMeToken) {
|
||||
val persistentLogin = PersistentLogin(
|
||||
series = token.series,
|
||||
username = token.username,
|
||||
tokenValue = token.tokenValue,
|
||||
lastUsed = token.date
|
||||
)
|
||||
repository.save(persistentLogin).block() // 블로킹 여부는 환경에 따라 조절
|
||||
println("CALLED rememberMeServices")
|
||||
}
|
||||
|
||||
override fun updateToken(series: String, tokenValue: String, lastUsed: Date) {
|
||||
val login = repository.findById(series).block()
|
||||
if (login != null) {
|
||||
val updated = login.copy(tokenValue = tokenValue, lastUsed = lastUsed)
|
||||
repository.save(updated).block()
|
||||
println("CALLED rememberMeServices")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTokenForSeries(seriesId: String): PersistentRememberMeToken? {
|
||||
val login = repository.findById(seriesId).block()
|
||||
return login?.let {
|
||||
println("CALLED rememberMeServices")
|
||||
PersistentRememberMeToken(it.username, it.series, it.tokenValue, it.lastUsed)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeUserTokens(username: String) {
|
||||
val tokens = repository.findByUsername(username).collectList().block()
|
||||
tokens?.let {
|
||||
println("CALLED rememberMeServices")
|
||||
repository.deleteAll(it).block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ class User {
|
||||
var isAccept : String? = null
|
||||
var isAdmin : String? = null
|
||||
|
||||
var remeberMe : Boolean? = false
|
||||
var rememberMe : Boolean? = false
|
||||
|
||||
fun checkValid() : Boolean {
|
||||
if (
|
||||
|
||||
@ -3439,4 +3439,5 @@ button.small,
|
||||
right: 10px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,90 +14,150 @@ function initEditor(useEditor) {
|
||||
baseData.firstPostLat = serverData.firstPostLat;
|
||||
baseData.firstPostLon = serverData.firstPostLon;
|
||||
baseData.writeTime = serverData.writeTime;
|
||||
baseData.originId = serverData.originId;
|
||||
|
||||
getLocation();
|
||||
|
||||
setEditorHeight();
|
||||
window.addEventListener('resize', setEditorHeight);
|
||||
var Font = Quill.import('formats/font');
|
||||
Font.whitelist = ['sans-serif', 'serif', 'monospace', 'arial', 'georgia', 'comic-sans-ms', 'courier-new', 'roboto', 'playfair-display'];
|
||||
Quill.register(Font, true);
|
||||
Quill.register({ 'modules/table-better': QuillTableBetter }, true);
|
||||
|
||||
quill = new Quill(editorContainer, {
|
||||
theme: 'snow',
|
||||
modules: useEditor ? {
|
||||
toolbar: {
|
||||
container: [
|
||||
[{ font: Font.whitelist }],
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'header': 1 }, { 'header': 2 }, 'blockquote', 'code-block'],
|
||||
[{ 'script': 'sub'}, { 'script': 'super' }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
['link', 'image', 'video'],
|
||||
['table-better'],
|
||||
[{ 'direction': 'rtl' }],
|
||||
[{ 'align': [] }],
|
||||
['clean']
|
||||
],
|
||||
handlers: {
|
||||
image: function () {
|
||||
selectLocalImage();
|
||||
},
|
||||
video: function () {
|
||||
selectLocalVideo()
|
||||
}
|
||||
}
|
||||
},
|
||||
'table-better': {
|
||||
language: 'en_US',
|
||||
toolbarTable: true
|
||||
},
|
||||
keyboard: {
|
||||
bindings: QuillTableBetter.keyboardBindings
|
||||
}
|
||||
} : {toolbar : false, toolbarTable: false},
|
||||
|
||||
readOnly: !useEditor
|
||||
});
|
||||
|
||||
loadContent(serverData.content);
|
||||
if (!useEditor) {
|
||||
editorContainer.classList.add('readonly-mode');
|
||||
} else {
|
||||
editorContainer.classList.remove('readonly-mode');
|
||||
}
|
||||
console.log("quill", quill);
|
||||
}
|
||||
|
||||
function isDelta(content) {
|
||||
try {
|
||||
// Delta는 JSON이면서 'ops'라는 키를 포함
|
||||
if (typeof content === "string") {
|
||||
content = JSON.parse(content);
|
||||
var Font = Quill.import('formats/font');
|
||||
Font.whitelist = ['sans-serif', 'serif', 'monospace', 'arial', 'georgia', 'comic-sans-ms', 'courier-new', 'roboto', 'playfair-display'];
|
||||
Quill.register(Font, true);
|
||||
Quill.register({ 'modules/table-better': QuillTableBetter }, true);
|
||||
|
||||
quill = new Quill(editorContainer, {
|
||||
theme: 'snow',
|
||||
modules: useEditor ? {
|
||||
toolbar: {
|
||||
container: [
|
||||
[{ font: Font.whitelist }],
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'header': 1 }, { 'header': 2 }, 'blockquote', 'code-block'],
|
||||
[{ 'script': 'sub'}, { 'script': 'super' }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
['link', 'image', 'video'],
|
||||
['table-better'],
|
||||
[{ 'direction': 'rtl' }],
|
||||
[{ 'align': [] }],
|
||||
['clean']
|
||||
],
|
||||
handlers: {
|
||||
image: function () {
|
||||
selectLocalImage();
|
||||
},
|
||||
video: function () {
|
||||
selectLocalVideo()
|
||||
}
|
||||
}
|
||||
},
|
||||
'table-better': {
|
||||
language: 'en_US',
|
||||
toolbarTable: true
|
||||
},
|
||||
keyboard: {
|
||||
bindings: QuillTableBetter.keyboardBindings
|
||||
}
|
||||
} : {toolbar : false, toolbarTable: false},
|
||||
|
||||
readOnly: !useEditor
|
||||
});
|
||||
|
||||
loadContent(serverData.content);
|
||||
if (!useEditor) {
|
||||
editorContainer.classList.add('readonly-mode');
|
||||
} else {
|
||||
editorContainer.classList.remove('readonly-mode');
|
||||
}
|
||||
console.log("quill", quill);
|
||||
}catch (e) { }
|
||||
try {
|
||||
document.querySelector("#title_field").textContent = baseData.title
|
||||
}catch (e) { }
|
||||
try {
|
||||
document.getElementById('location_field').textContent = "Lat: " + baseData.firstPostLat + ", Lon: " + baseData.firstPostLon;
|
||||
var requestOptions = {
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
fetch("https://api.geoapify.com/v1/geocode/reverse?lat="+baseData.firstPostLat+"&lon="+baseData.firstPostLon+"&apiKey=2b37a75bb0754086b5a1c4a7c3173ee8", requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(function(result) {
|
||||
try {
|
||||
document.getElementById('location_field').textContent = result.features[0].properties.formatted
|
||||
} catch (e) {
|
||||
document.getElementById('location_field').innerHTML = "Lat: " + baseData.firstPostLat + "<br> Lon: " + baseData.firstPostLon;
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('error', error));
|
||||
}catch (e) { }
|
||||
}
|
||||
|
||||
function parseDelta(content) {
|
||||
try {
|
||||
if (typeof content === "string") {
|
||||
const obj = JSON.parse(content);
|
||||
if (obj && typeof obj === "object" && Array.isArray(obj.ops)) {
|
||||
return obj; // 유효한 Delta 객체
|
||||
}
|
||||
} else if (content && typeof content === "object" && Array.isArray(content.ops)) {
|
||||
return content; // 이미 Delta 객체 형태
|
||||
}
|
||||
return typeof content === "object" && content.ops !== undefined;
|
||||
} catch (e) {
|
||||
return false; // JSON 파싱 실패하면 마크업(HTML)으로 간주
|
||||
// JSON 파싱 실패 → HTML로 간주
|
||||
}
|
||||
return null; // Delta 아님
|
||||
}
|
||||
function loadEditor() {
|
||||
location.href = getMainPath() + '/blog/editor/' + serverData.id;
|
||||
}
|
||||
|
||||
|
||||
function loadContent(content) {
|
||||
if (isDelta(content)) {
|
||||
quill.setContents(content); // Delta 데이터라면 setContents
|
||||
console.log("content >>> ", content);
|
||||
const delta = parseDelta(content);
|
||||
if (delta) {
|
||||
quill.setContents(delta);
|
||||
} else {
|
||||
quill.clipboard.dangerouslyPasteHTML(content); // HTML 데이터라면 dangerouslyPasteHTML
|
||||
quill.clipboard.dangerouslyPasteHTML(content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function save() {
|
||||
onclickWrite(serverData.enc ,serverData.keyword,quill.getContents())
|
||||
onclickWrite(serverData.enc ,serverData.keyword,JSON.stringify(quill.getContents()))
|
||||
}
|
||||
|
||||
function parseDeltaContent(deltaString) {
|
||||
let textContent = "";
|
||||
let mediaLinks = [];
|
||||
|
||||
try {
|
||||
const delta = JSON.parse(deltaString); // Delta 문자열 → 객체
|
||||
if (delta && Array.isArray(delta.ops)) {
|
||||
delta.ops.forEach(op => {
|
||||
if (op.insert) {
|
||||
if (typeof op.insert === "string") {
|
||||
textContent += op.insert;
|
||||
} else if (typeof op.insert === "object") {
|
||||
if (op.insert.image) {
|
||||
mediaLinks.push(op.insert.image);
|
||||
}
|
||||
if (op.insert.video) {
|
||||
mediaLinks.push(op.insert.video);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Delta JSON parse error:", e);
|
||||
}
|
||||
return { text: textContent.trim(), media: mediaLinks };
|
||||
}
|
||||
|
||||
|
||||
function selectLocalImage() {
|
||||
// 이미지 URL 입력 받기
|
||||
const url = prompt("이미지 URL을 입력하거나 빈칸으로 두시면 파일 업로드를 합니다.");
|
||||
@ -237,8 +297,8 @@ function onclickWrite(type, keyword, html) {
|
||||
if (hasValues) {
|
||||
baseData.title = encodeURIComponent(title_field.value)
|
||||
baseData.content = encodeURIComponent(html)
|
||||
baseData.firstPostLat = encodeURIComponent(currentLat)
|
||||
baseData.firstPostLon = encodeURIComponent(currentLon)
|
||||
baseData.modifyLat = encodeURIComponent(currentLat)
|
||||
baseData.modifyLon = encodeURIComponent(currentLon)
|
||||
}
|
||||
let uploadUrl = getMainPath() + "/blog/post.bjx";
|
||||
if(confirm(JSON.stringify(baseData) + "\n해당 내용으로\n유저 등록 하실??")) {
|
||||
|
||||
@ -419,16 +419,16 @@ function submitLoginForm() {
|
||||
let user_id = document.getElementById('loginId')
|
||||
let user_pw = document.getElementById('loginPassword')
|
||||
let rememberMe = document.getElementById('rememberMe')
|
||||
console.log(rememberMe.value)
|
||||
let data = {
|
||||
'user_id': user_id.value,
|
||||
'user_pw': user_pw.value,
|
||||
'rememberMe' : rememberMe.value,
|
||||
'rememberMe' : rememberMe.value === "on",
|
||||
}
|
||||
postLogin(getMainPath()+"/user/login.bjx",user_pw.data,JSON.stringify(data),user_pw.data, function (data) {
|
||||
closePopup()
|
||||
alert("data >> " + data)
|
||||
if (data.isOk) {
|
||||
document.cookie = "access=" + data.token.split(";")[0]+";"
|
||||
window.sessionStorage.setItem("REFRESH",data.refresh.split(";")[0])
|
||||
document.location.replace(location.href)
|
||||
} else {
|
||||
if (data.resultCode === 7100) {
|
||||
|
||||
@ -27,10 +27,11 @@
|
||||
<th:block layout:fragment="content" id="content">
|
||||
|
||||
<section class="wrapper style2" >
|
||||
<th:block th:if="${PERMISSION == 'OK'}">
|
||||
<th:block sec:authorize="isAuthenticated()">
|
||||
<div class="container" >
|
||||
<header class="major">
|
||||
<h3><label for="title_field"><input id="title_field" th:value="${srcPost.title}"></label></h3>
|
||||
<h3><label for="title_field"><input id="title_field" th:value="${srcPost.title}" style="width:100%; max-width:100%; box-sizing:border-box;
|
||||
border:none; outline:none; text-align:center;"></label></h3>
|
||||
<p th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')}"></p>
|
||||
</header>
|
||||
</div>
|
||||
@ -42,6 +43,7 @@
|
||||
<h1>권한이 없는 뎁쇼?!</h1>
|
||||
</th:block>
|
||||
<th:block sec:authorize="isAuthenticated()">
|
||||
|
||||
<div id="editor" ></div>
|
||||
<div class="write_controllbox">
|
||||
<div class="write_option btn-example" to="#popLayer1" onclick="openPopup(this)" ></div>
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
<script th:inline="javascript">
|
||||
var serverData = {
|
||||
id: [[${srcPost != null and srcPost.id != null} ? ${srcPost.id} : 0]],
|
||||
originId: [[${srcPost != null and srcPost.originId != null} ? ${srcPost.originId} : 0]],
|
||||
title: /*[[${srcPost != null and srcPost.title != null} ? ${srcPost.title} : '']]*/,
|
||||
content: /*[[${srcPost != null and srcPost.content != null} ? ${srcPost.content} : '']]*/,
|
||||
firstPostLat: [[${srcPost != null and srcPost.firstPostLat != null} ? ${srcPost.firstPostLat} : 0]],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user