This commit is contained in:
lunaticbum 2025-03-21 17:15:55 +09:00
parent 97236619f0
commit 2175abbc80
18 changed files with 341 additions and 135 deletions

View File

@ -56,6 +56,8 @@ dependencies {
implementation ("org.seleniumhq.selenium:selenium-java:4.10.0") implementation ("org.seleniumhq.selenium:selenium-java:4.10.0")
implementation ("org.commonmark:commonmark:0.18.0") implementation ("org.commonmark:commonmark:0.18.0")
implementation ("net.coobird:thumbnailator:0.4.14")
implementation ("com.drewnoakes:metadata-extractor:2.19.0") implementation ("com.drewnoakes:metadata-extractor:2.19.0")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")

View File

@ -1,12 +1,12 @@
package kr.lunaticbum.back.lun.configs package kr.lunaticbum.back.lun.configs
import io.qdrant.client.QdrantClient
import org.springframework.ai.ollama.api.OllamaApi
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.http.CacheControl
import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import java.time.Duration
@Configuration @Configuration
@ -17,12 +17,15 @@ class AppConfig : WebMvcConfigurer {
@Value("\${resource.location}") @Value("\${resource.location}")
private val resourceLocation: String? = null private val resourceLocation: String? = null
val cacheControl: CacheControl = CacheControl.maxAge(Duration.ofDays(365))
@Bean @Bean
fun authInterceptor(): BumsInterceptor { fun authInterceptor(): BumsInterceptor {
return BumsInterceptor() return BumsInterceptor()
} }
override fun addResourceHandlers(registry: org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry) { override fun addResourceHandlers(registry: org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry) {
registry.addResourceHandler(resourceHandler).addResourceLocations(resourceLocation)
registry.addResourceHandler(resourceHandler).addResourceLocations(resourceLocation).setCacheControl(cacheControl)
} }
override fun addInterceptors(registry: InterceptorRegistry) { override fun addInterceptors(registry: InterceptorRegistry) {

View File

@ -4,6 +4,9 @@ import com.google.gson.Gson
import jakarta.servlet.http.Cookie import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import kr.lunaticbum.back.lun.configs.GlobalEnvironment.Companion.ApiKeyWordKey
import kr.lunaticbum.back.lun.configs.GlobalEnvironment.Companion.EncType11
import kr.lunaticbum.back.lun.configs.GlobalEnvironment.Companion.EncTypeKey
import kr.lunaticbum.back.lun.model.UserManager import kr.lunaticbum.back.lun.model.UserManager
import kr.lunaticbum.back.lun.service.JwtService import kr.lunaticbum.back.lun.service.JwtService
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@ -98,6 +101,8 @@ class BumsInterceptor : HandlerInterceptor {
(it.getAttribute(WRITE_PERMISSION_KEY) as? Boolean)?.let { permission -> (it.getAttribute(WRITE_PERMISSION_KEY) as? Boolean)?.let { permission ->
if (permission) { if (permission) {
modelAndView?.modelMap?.put(WRITE_PERMISSION_KEY,"OK") modelAndView?.modelMap?.put(WRITE_PERMISSION_KEY,"OK")
modelAndView?.modelMap?.put(EncTypeKey, EncType11)
modelAndView?.modelMap?.put(ApiKeyWordKey,"Def")
} }
} }
} }

View File

@ -16,6 +16,7 @@ import kr.lunaticbum.back.lun.model.*
import kr.lunaticbum.back.lun.service.JwtService import kr.lunaticbum.back.lun.service.JwtService
import kr.lunaticbum.back.lun.utils.LogService import kr.lunaticbum.back.lun.utils.LogService
import kr.lunaticbum.back.lun.utils.getFileExtension import kr.lunaticbum.back.lun.utils.getFileExtension
import net.coobird.thumbnailator.Thumbnails
import org.commonmark.node.Node import org.commonmark.node.Node
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
@ -25,13 +26,11 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.core.io.Resource import org.springframework.core.io.Resource
import org.springframework.core.io.UrlResource import org.springframework.core.io.UrlResource
import org.springframework.data.domain.Pageable
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import reactor.kotlin.core.publisher.toMono
import java.io.* import java.io.*
import java.net.URLDecoder import java.net.URLDecoder
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -219,16 +218,54 @@ class BlogController() {
val firstImg: Element? = doc.select("img")?.first() val firstImg: Element? = doc.select("img")?.first()
val imgSrc: String = firstImg?.attr("src") ?: "" val imgSrc: String = firstImg?.attr("src") ?: ""
it.image = imgSrc it.image = imgSrc
it.thumb = imgSrc.replaceBeforeLast(".", "_thumbnail.")
generateThumbnail(imgSrc.split("/").last(), 200)
it.html = doc.text() it.html = doc.text()
} }
it.title = if ((it.title?.length ?: 0) >= 1) it.title else "" it.title = if ((it.title?.length ?: 0) >= 1) it.title else ""
} }
}.chunked(2)) })
}catch (ex: Exception){ex.printStackTrace()} }catch (ex: Exception){ex.printStackTrace()}
return vm return vm
} }
fun generateThumbnail(originalPath: String, targetWidth: Int) {
try {
val originalFile = File("$uploadPath${File.separator}$originalPath")
// 썸네일 경로 생성 (예: /upload/uuid.jpg → /upload/uuid_thumbnail.jpg)
val thumbnailPath = originalFile.path
.replaceBeforeLast(".", "_thumbnail.")
val thumbnailFile = File(thumbnailPath)
// 썸네일 이미 존재하면 종료
if (thumbnailFile.exists()) {
println("썸네일 이미 존재: $thumbnailPath")
return
}
// 원본 파일 존재 확인
if (!originalFile.exists()) {
println("원본 파일 없음: $originalPath")
return
}
// 썸네일 생성 (가로 기준 비율 유지)
Thumbnails.of(originalFile)
.width(targetWidth)
.keepAspectRatio(true)
.toFile(thumbnailPath)
println("썸네일 생성 완료: $thumbnailPath")
} catch (e: IOException) {
println("썸네일 생성 실패: ${e.message}")
}
}
@GetMapping("recent") @GetMapping("recent")
fun recent() : ResultMV{ fun recent() : ResultMV{
val vm = ResultMV("content/blog/viewer") val vm = ResultMV("content/blog/viewer")
@ -271,80 +308,75 @@ class BlogController() {
} }
@PostMapping("post/imageUpload") @PostMapping("post/imageUpload")
fun postImage(@RequestPart("file") upload: MultipartFile, res: HttpServletResponse, req: HttpServletRequest) : ResponseEntity<FileSaveResult> { fun postImage(@RequestPart("file") upload: MultipartFile, res: HttpServletResponse, req: HttpServletRequest): ResponseEntity<FileSaveResult> {
var lResultCode = 0 var lResultCode = 0
var lResultMsg = "Suscces" var lResultMsg = "Success"
var out: OutputStream? = null var out: FileOutputStream? = null
var printWriter: PrintWriter? = null var targetFile: File? = null
var targetFile : File? = null
logService.log("imgUploadPath ${upload.originalFilename}") val uuid = UUID.randomUUID()
res.characterEncoding = "utf-8"
res.contentType = "text/html;charset=utf-8"
var uuid = UUID.randomUUID()
val extension: String = getFileExtension(upload.originalFilename) ?: "" val extension: String = getFileExtension(upload.originalFilename) ?: ""
try { try {
logService.log("imgUploadPath ${uuid.toString()}")
val bytes = upload.bytes val bytes = upload.bytes
var f = File(uploadPath) val f = File(uploadPath)
// logService.log("imgUploadPath ${f.parentFile.parentFile.parentFile.parentFile.absoluteFile}") if (!f.exists()) f.mkdirs()
// logService.log("imgUploadPath ${f.parentFile.parentFile.parentFile.absoluteFile}")
// logService.log("imgUploadPath ${f.parentFile.parentFile.absoluteFile}")
// logService.log("imgUploadPath ${f.parentFile.absoluteFile}")
logService.log("imgUploadPath ${f.exists()}")
logService.log("imgUploadPath ${f.absolutePath}")
if (f.exists() == false) f.mkdirs()
// 실제 이미지 저장 경로
val imgUploadPath = (uploadPath + File.separator + uuid).toString() + "." + extension
logService.log("imgUploadPath $imgUploadPath")
targetFile = File(imgUploadPath)
if(targetFile.parentFile.exists() == false)targetFile.parentFile.mkdirs()
// 이미지 저장 // 원본 이미지 저장 경로
out = FileOutputStream(imgUploadPath) val originalImagePath = "$uploadPath${File.separator}$uuid.$extension"
logService.log("Original image path: $originalImagePath")
// 썸네일 저장 경로
val thumbnailPath = "$uploadPath${File.separator}${uuid}_thumbnail.$extension"
logService.log("Thumbnail path: $thumbnailPath")
targetFile = File(originalImagePath)
if (!targetFile.parentFile.exists()) targetFile.parentFile.mkdirs()
// 원본 이미지 저장
out = FileOutputStream(originalImagePath)
out.write(bytes) out.write(bytes)
out.flush() out.flush()
// ckEditor 로 전송 // 썸네일 생성 및 저장
// printWriter = res.writer Thumbnails.of(originalImagePath)
// val callback = req.getParameter("CKEditorFuncNum") .width(200) // 가로 크기를 설정
// val fileUrl = "/blog/post/image/$uuid.$extension" .keepAspectRatio(true)
.toFile(thumbnailPath)
// printWriter.println( logService.log("Original image saved: ${File(originalImagePath).exists()}")
// ("<script type='text/javascript'>" logService.log("Thumbnail saved: ${File(thumbnailPath).exists()}")
// + "window.parent.CKEDITOR.tools.callFunction("
// + callback + ",'" + fileUrl + "','이미지를 업로드하였습니다.')" // 메타데이터 읽기 (원본 이미지에서)
// + "</script>") val metadata: Metadata? = ImageMetadataReader.readMetadata(File(originalImagePath))
// )
//
// printWriter.flush()
logService.log("imgUploadPath $imgUploadPath")
logService.log("imgUploadPath ${File(imgUploadPath).exists()}")
val metadata: Metadata? = ImageMetadataReader.readMetadata(File(imgUploadPath))
metadata?.let { metadata?.let {
it.directories?.forEach { directory -> it.directories?.forEach { directory ->
logService.log(directory.name) logService.log(directory.name)
logService.log(directory.tags.map { tag -> logService.log("tag.tagName >>> ${tag.tagName} || tag.description ${tag.description}")}.joinToString(" \n")) logService.log(directory.tags.map { tag ->
logService.log("tag.tagName >>> ${tag.tagName} || tag.description ${tag.description}")
}.joinToString(" \n"))
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
lResultCode = 1
lResultMsg = "Error: ${e.message}"
} finally { } finally {
try { try {
out?.close() out?.close()
printWriter?.close()
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} }
} }
val responce = ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(FileSaveResult().apply {
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(FileSaveResult().apply {
this.resultCode = lResultCode this.resultCode = lResultCode
this.resultMsg = lResultMsg this.resultMsg = lResultMsg
this.fileName = "$uuid.$extension" this.fileName = "$uuid.$extension"
this.thumbnailName = "${uuid}_thumbnail.$extension"
}) })
return responce
} }
} }

View File

@ -36,6 +36,7 @@ class Post {
var html : String? = null var html : String? = null
var image : String? = null var image : String? = null
var thumb : String? = null
var writer : String? = null var writer : String? = null
var writeTime : Long = 0 var writeTime : Long = 0

View File

@ -18,4 +18,5 @@ open class LoginResult : ResponceResult() {
@Getter @Getter
class FileSaveResult : ResponceResult() { class FileSaveResult : ResponceResult() {
var fileName : String? = null var fileName : String? = null
var thumbnailName : String? = null
} }

View File

@ -3385,3 +3385,58 @@ button.small,
} }
} }
/*Login Popup*/
.login_overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.login_popup {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.login_form {
display: none;
}
.login_form h2 {
margin-top: 0;
}
.login_form input {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
/*.login_form button {*/
/* width: 100%;*/
/* padding: 10px;*/
/* background-color: #4CAF50;*/
/* color: white;*/
/* border: none;*/
/* cursor: pointer;*/
/*}*/
.login_close {
position: absolute;
top: 10px;
right: 10px;
font-size: 20px;
cursor: pointer;
}

View File

@ -48,9 +48,9 @@
border-radius: 10px; border-radius: 10px;
background: #00000044; background: #00000044;
} }
#title_field { /*#title_field {*/
font-size: 20px; /*font-size: 20px;*/
} /*}*/
.pop_layer .pop_container { .pop_layer .pop_container {
padding: 20px 25px; padding: 20px 25px;

View File

@ -16,8 +16,8 @@ let baseData = {
'writeTime' : 0, 'writeTime' : 0,
} }
function goToEditor(path,id,sk) { function goToEditor(id) {
location.href = path + id+"?token="+sk; location.href = getMainPath() + '/blog/editor/' + id;
} }

View File

@ -1,4 +1,10 @@
document.addEventListener('DOMContentLoaded', function() {
const loginForm = document.getElementById('loginFormElement');
loginForm.addEventListener('submit', function(e) {
e.preventDefault(); // 기본 폼 제출 동작 방지
submitLoginForm();
});
});
onload = function() { onload = function() {
history.replaceState({}, null, location.pathname); history.replaceState({}, null, location.pathname);
// var accToken = get_cookie("access") // var accToken = get_cookie("access")
@ -8,6 +14,31 @@ onload = function() {
document.cookie = "access=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" document.cookie = "access=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"
document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" document.cookie = "refresh=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"
document.cookie = "CLEAR=; expires=Thu, 01 Jan 1970 00:00:01 GMT;" document.cookie = "CLEAR=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"
var currentList = [{"page":["posts"],"id":"menu_posts"},{"page":["licenses"],"id":"menu_drop"},{"page":["licenses"],"id":"menu_drop"}]
if(location.pathname.length > 1) {
// 1. 모든 'current' 클래스를 가진 요소를 선택하고 제거
// const currentElements = document.querySelectorAll('.current');
// currentElements.forEach(element => {
// element.classList.remove('current');
// });
currentList.forEach(element => {
element.page.forEach((page, index) => {
console.log(location.pathname);
if (location.pathname.includes(page)) {
const targetElement = document.getElementById(element.id);
if (targetElement) {
targetElement.classList.add('current');
}
}
})
})
} else {
const targetElement = document.getElementById('menu_home');
if (targetElement) {
targetElement.classList.add('current');
}
}
} }
// onbeforeunload = function () { // onbeforeunload = function () {
// var accToken = get_cookie("access") // var accToken = get_cookie("access")
@ -261,3 +292,74 @@ function layer_popup(el){
function urldecode(t){ function urldecode(t){
return decodeURI(t) return decodeURI(t)
} }
function openLoginPopup(formType) {
document.getElementById('overlay').style.display = 'block';
document.getElementById('loginForm').style.display = formType === 'login' ? 'block' : 'none';
document.getElementById('signupForm').style.display = formType === 'signup' ? 'block' : 'none';
if(formType === 'login') {
const loginIdInput = document.getElementById('loginId');
loginIdInput.focus();
}
}
function closePopup() {
document.getElementById('overlay').style.display = 'none';
}
//
// function submitForm(formType) {
// alert(formType === 'login' ? '로그인 시도' : '회원가입 시도');
// closePopup();
// }
function submitLoginForm() {
// const id = document.getElementById('loginId').value;
// const password = document.getElementById('loginPassword').value;
let user_id = document.getElementById('loginId')
let user_pw = document.getElementById('loginPassword')
let data = {
'user_id': user_id.value,
'user_pw': user_pw.value,
}
postLogin(getMainPath()+"/user/login.ajax",user_pw.data,JSON.stringify(data),user_pw.data, function (data) {
closePopup()
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) {
if(confirm(`너 누구임 정보 없는데?!\n${data.resultMsg}[${data.resultCode}]\n가입 할래!?`)){
document.location.replace(getMainPath() + "/user/join")
}
} else {
alert(`너 누구임?!\n${data.resultMsg}[${data.resultCode}]`)
}
}
})
// AJAX 요청
// fetch('/login', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ id, password }),
// })
// .then(response => response.json())
// .then(data => {
// if (data.success) {
// alert('로그인 성공!');
// // 로그인 성공 후 처리 (예: 페이지 리다이렉트)
// } else {
// alert('로그인 실패: ' + data.message);
// }
// })
// .catch(error => {
// console.error('Error:', error);
// alert('로그인 중 오류가 발생했습니다.');
// });
}

View File

@ -23,30 +23,19 @@
baseData.firstPostLon = [[${srcPost.firstPostLon}]]; baseData.firstPostLon = [[${srcPost.firstPostLon}]];
baseData.writeTime = [[${srcPost.writeTime}]]; baseData.writeTime = [[${srcPost.writeTime}]];
getLocation() getLocation()
var style = getComputedStyle(document.body) // var style = getComputedStyle(document.body)
console.log(style.getPropertyValue('--ContentVerticalMargin')) // console.log(style.getPropertyValue('--ContentVerticalMargin'))
console.log(window.c) // console.log(window.c)
console.log(style.getPropertyValue('--FooterHeight')) // console.log(style.getPropertyValue('--FooterHeight'))
console.log(style.getPropertyValue('--TopHeight')) // console.log(style.getPropertyValue('--TopHeight'))
document.querySelector("#title_field").value = baseData.title
var editorHeght = (
document.querySelector('#main_layer').getBoundingClientRect().height -
(
Number(style.getPropertyValue('--ButtonHegit').replace("px","") * 3)
+ Number(style.getPropertyValue('--TopHeight').replace("px",""))
)
)
editor = new toastui.Editor({ editor = new toastui.Editor({
el: document.querySelector('#editor'), el: document.querySelector('#editor'),
previewStyle: 'tab', previewStyle: 'tab',
height: editorHeght + 'px', height: '900px',
width:'95%', width:'95%',
theme:'dark',
usageStatistics : false, usageStatistics : false,
toolbar:null, toolbar:null,
initialValue:baseData.content, initialValue:baseData.content,
theme:"dark",
initialEditType:"wysiwyg", initialEditType:"wysiwyg",
hooks: { hooks: {
addImageBlobHook: (blob, callback) => { addImageBlobHook: (blob, callback) => {
@ -84,24 +73,32 @@
</script> </script>
</th:block> </th:block>
<th:block layout:fragment="content" id="content"> <th:block layout:fragment="content" id="content">
<div id="main_layer">
<section class="wrapper style2" >
<th:block th:if="${PERMISSION == 'OK'}">
<div class="container" >
<header class="major">
<h3><label for="title_field"><input id="title_field" th:value="${srcPost.title}"></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>
</th:block>
</section>
<section class="wrapper style2">
<th:block th:if="${PERMISSION != 'OK'}"> <th:block th:if="${PERMISSION != 'OK'}">
<h1>권한이 없는 뎁쇼?!</h1> <h1>권한이 없는 뎁쇼?!</h1>
</th:block> </th:block>
<th:block th:if="${PERMISSION == 'OK'}"> <th:block th:if="${PERMISSION == 'OK'}">
<label for="title_field"></label><input id="title_field" class="write_option">
<div id="editor" ></div> <div id="editor" ></div>
<div class="write_controllbox"> <div class="write_controllbox">
<div class="write_option btn-example" to="#popLayer1" onclick="openPopup(this)" ></div> <div class="write_option btn-example" to="#popLayer1" onclick="openPopup(this)" ></div>
<div class="write_option btn-example" to="#popLayer2" onclick="openPopup(this)" id="hashtag_field"></div> <div class="write_option btn-example" to="#popLayer2" onclick="openPopup(this)" id="hashtag_field"></div>
<label for="location_field"></label><input class="write_option" readonly id="location_field"/> <label for="location_field"></label><input class="write_option" readonly id="location_field"/>
</div> </div>
<h1><button id="save" class="write_option" style="width: 100%; position: relative" onclick="save()">저장하셈</button></h1> <h1><button id="save" class="write_option" style="width: 100%; position: relative" onclick="save()">저장하셈</button></h1>
</th:block> </th:block>
</div> </section>
</th:block> </th:block>
<th:block layout:fragment="popup_layer"> <th:block layout:fragment="popup_layer">
<div id="popLayer1" class="pop_layer"> <div id="popLayer1" class="pop_layer">

View File

@ -14,17 +14,6 @@
let onChange = () => {console.log(editor.getMarkdown())} let onChange = () => {console.log(editor.getMarkdown())}
document.addEventListener("DOMContentLoaded", onLoaded); document.addEventListener("DOMContentLoaded", onLoaded);
function onLoaded() { function onLoaded() {
// 1. 모든 'current' 클래스를 가진 요소를 선택하고 제거
const currentElements = document.querySelectorAll('.current');
currentElements.forEach(element => {
element.classList.remove('current');
});
// 2. 특정 조건에 맞는 요소에 'current' 클래스 추가 (예: ID가 'targetElement'인 요소)
const targetElement = document.getElementById('menu_posts');
if (targetElement) {
targetElement.classList.add('current');
}
} }
</script> </script>
@ -65,30 +54,16 @@
<div class="col-6 col-12-narrower imp-narrower"> <div class="col-6 col-12-narrower imp-narrower">
<div id="content"> <div id="content">
<article> <article>
<header> <section class="col-6 col-12-narrower imp-narrower" th:each="post : ${Posts}">
<h2>Two Sidebar</h2> <!-- <span th:text="${cell}"></span>-->
<p>Yup. Two sidebars at the same time.</p> <div class="box post" onclick="goToViewer(this)" th:data="${post.id}">
</header> <a href="#" class="image left"><img th:src="${#strings.length(post.image) > 0} ? ${post.image} : 'images/pic01.jpg'" alt="" /></a>
<div class="inner">
<span class="image featured"><img src="images/banner.jpg" alt="" /></span> <h3 th:text="${#strings.length(post.title) > 0} ? ${post.title} : ('untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')} + ']')"></h3>
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
<p>Phasellus quam turpis, feugiat sit amet ornare in, hendrerit in lectus. </div>
Praesent semper mod quis eget mi. Etiam eu ante risus. Aliquam erat volutpat. </div>
Aliquam luctus et mattis lectus sit amet pulvinar. Nam turpis nisi </section>
consequat etiam lorem ipsum dolor sit amet nullam.</p>
<h3>And Yet Another Subheading</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ac quam risus, at tempus
justo. Sed dictum rutrum massa eu volutpat. Quisque vitae hendrerit sem. Pellentesque lorem felis,
ultricies a bibendum id, bibendum sit amet nisl. Mauris et lorem quam. Maecenas rutrum imperdiet
rhoncus dui quis euismod. Maecenas lorem tellus, congue et condimentum ac, ullamcorper non sapien.
Donec sagittis massa et leo semper a scelerisque metus faucibus. Morbi congue mattis mi.
Phasellus sed nisl vitae risus tristique volutpat. Cras rutrum commodo luctus.</p>
<p>Phasellus odio risus, faucibus et viverra vitae, eleifend ac purus. Praesent mattis, enim
quis hendrerit porttitor, sapien tortor viverra magna, sit amet rhoncus nisl lacus nec arcu.
Maecenas tortor mauris, consectetur pellentesque dapibus eget, tincidunt vitae arcu.
Vestibulum purus augue, tincidunt sit amet iaculis id, porta eu purus.</p>
</article> </article>
</div> </div>

View File

@ -30,12 +30,22 @@
initialValue:baseData.content, initialValue:baseData.content,
}); });
} }
function loadEditor() {
goToEditor([[${srcPost.id}]]);
}
</script> </script>
</th:block> </th:block>
<th:block layout:fragment="content" id="content"> <th:block layout:fragment="content" id="content">
<section class="wrapper style2"> <section class="wrapper style2">
<div class="container">
<header class="major" > <div class="container" th:if="${PERMISSION == 'OK'}" onclick="loadEditor()">
<header class="major">
<h2 id="title_layer" th:text="${srcPost.title}">A gigantic heading you can use for whatever</h2>
<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>
<div class="container" th:if="${PERMISSION != 'OK'}" onclick="openLoginPopup('login')">
<header class="major">
<h2 id="title_layer" th:text="${srcPost.title}">A gigantic heading you can use for whatever</h2> <h2 id="title_layer" th:text="${srcPost.title}">A gigantic heading you can use for whatever</h2>
<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> <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> </header>

View File

@ -51,7 +51,7 @@
<section class="col-6 col-12-narrower" th:each="post : ${row}"> <section class="col-6 col-12-narrower" th:each="post : ${row}">
<!-- <span th:text="${cell}"></span>--> <!-- <span th:text="${cell}"></span>-->
<div class="box post" onclick="goToViewer(this)" th:data="${post.id}"> <div class="box post" onclick="goToViewer(this)" th:data="${post.id}">
<a href="#" class="image left"><img th:src="${#strings.length(post.image) > 0} ? ${post.image} : 'images/pic01.jpg'" alt="" /></a> <a href="#" class="image left"><img th:src="${#strings.length(post.image) > 0} ? ${post.image} : 'images/pic01.jpg'" alt="" /></a>
<div class="inner"> <div class="inner">
<h3 th:text="${#strings.length(post.title) > 0} ? ${post.title} : ('untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')} + ']')"></h3> <h3 th:text="${#strings.length(post.title) > 0} ? ${post.title} : ('untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')} + ']')"></h3>
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p> <p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
@ -73,7 +73,12 @@
</div> </div>
</section> </section>
<section class="col-4 col-12-narrower"> <section class="col-4 col-12-narrower">
<div class="box highlight"> <div class="box highlight" th:if="${PERMISSION == 'OK'}" onclick=gotoWrite()>
<i class="icon solid major fa-pencil-alt"></i>
<h3>글쓰기[Writing]</h3>
<p>오직 주인장 만의 권한 임요. 그냥 내가 쓰기 편하게 여기 놔둔 메뉴임. 님들은 못씀요.<br>[Only the owner has the authority. This is just a menu that I put here for my convenience. You can't use it.]</p>
</div>
<div class="box highlight" th:if="${PERMISSION != 'OK'}" onclick="openLoginPopup('login')">
<i class="icon solid major fa-pencil-alt"></i> <i class="icon solid major fa-pencil-alt"></i>
<h3>글쓰기[Writing]</h3> <h3>글쓰기[Writing]</h3>
<p>오직 주인장 만의 권한 임요. 그냥 내가 쓰기 편하게 여기 놔둔 메뉴임. 님들은 못씀요.<br>[Only the owner has the authority. This is just a menu that I put here for my convenience. You can't use it.]</p> <p>오직 주인장 만의 권한 임요. 그냥 내가 쓰기 편하게 여기 놔둔 메뉴임. 님들은 못씀요.<br>[Only the owner has the authority. This is just a menu that I put here for my convenience. You can't use it.]</p>

View File

@ -14,10 +14,8 @@
let onChange = () => {console.log(editor.getMarkdown())} let onChange = () => {console.log(editor.getMarkdown())}
document.addEventListener("DOMContentLoaded", onLoaded); document.addEventListener("DOMContentLoaded", onLoaded);
function onLoaded() { function onLoaded() {
var h = document.querySelector('#main_layer').getBoundingClientRect().height + 'px'
editor = new toastui.Editor({ editor = new toastui.Editor({
el: document.querySelector('#editor'), el: document.querySelector('#editor'),
height: '500px',
width:'100%', width:'100%',
viewer: true, viewer: true,
usageStatistics : false, usageStatistics : false,
@ -137,15 +135,15 @@
"> - **Embedded license files**: [jcl-over-slf4j-2.0.16.jar/META-INF/LICENSE.txt](jcl-over-slf4j-2.0.16.jar/META-INF/LICENSE.txt)\n" + "> - **Embedded license files**: [jcl-over-slf4j-2.0.16.jar/META-INF/LICENSE.txt](jcl-over-slf4j-2.0.16.jar/META-INF/LICENSE.txt)\n" +
"\n" + "\n" +
"\n" "\n"
,
theme:"dark",
}); });
} }
</script> </script>
</th:block> </th:block>
<th:block layout:fragment="content" id="content"> <th:block layout:fragment="content" id="content">
<div id="main_layer" style="background:#30303594;"> <section class="wrapper style1">
<div id="editor" ></div> <div class="container">
</div> <div id="editor" ></div>
</div>
</section>
</th:block> </th:block>
</html> </html>

View File

@ -9,10 +9,14 @@
<!-- Nav --> <!-- Nav -->
<nav id="nav"> <nav id="nav">
<ul> <ul>
<li id="menu_home" class="current"><a th:href="@{/}">Home</a></li> <li id="menu_home" ><a th:href="@{/}">Home</a></li>
<li id="menu_posts"><a href="blog/posts">Posts</a></li> <li id="menu_posts"><a href="blog/posts">Posts</a></li>
<li id="menu_sec"><a href="left-sidebar">Left Sidebar</a></li>
<li id="menu_thr"><a href="right-sidebar">Right Sidebar</a></li>
<li id="menu_four"><a href="two-sidebar">Two Sidebar</a></li>
<li id="menu_drop"> <li id="menu_drop">
<a href="#">Dropdown</a> <a href="#">About</a>
<ul> <ul>
<li><a href="#">Lorem dolor</a></li> <li><a href="#">Lorem dolor</a></li>
<li><a href="#">Magna phasellus</a></li> <li><a href="#">Magna phasellus</a></li>
@ -27,13 +31,9 @@
<li><a href="#">Veroeros feugiat</a></li> <li><a href="#">Veroeros feugiat</a></li>
</ul> </ul>
</li> </li>
<li><a href="#">Veroeros feugiat</a></li> <li><a th:href="@{/licenses}">Licenses</a></li>
</ul> </ul>
</li> </li>
<li id="menu_sec"><a href="left-sidebar">Left Sidebar</a></li>
<li id="menu_thr"><a href="right-sidebar">Right Sidebar</a></li>
<li id="menu_four"><a href="two-sidebar">Two Sidebar</a></li>
</ul> </ul>
</nav> </nav>

View File

@ -19,6 +19,26 @@
</div> </div>
<th:block th:replace="~{fragments/footer :: footer}"></th:block> <th:block th:replace="~{fragments/footer :: footer}"></th:block>
</div> </div>
<div id="overlay" class="login_overlay">
<div id="popup" class="login_popup">
<div id="loginForm" class="login_form">
<h2>로그인</h2>
<form id="loginFormElement" onsubmit="return false;">
<input type="text" th:data="${enc}" id="loginId" placeholder="아이디" required>
<input type="password" th:data="${type}" id="loginPassword" placeholder="비밀번호" required>
<button type="submit" class="button">로그인</button>
</form>
</div>
<div id="signupForm" class="login_form">
<h2>회원가입</h2>
<input type="text" placeholder="아이디" required>
<input type="password" placeholder="비밀번호" required>
<input type="email" placeholder="이메일" required>
<button onclick="submitForm('signup')">가입하기</button>
</div>
<span class="login_close" onclick="closePopup()">&times;</span>
</div>
</div>
<!-- Scripts --> <!-- Scripts -->
<script th:src="@{/assets/js/jquery.min.js}"></script> <script th:src="@{/assets/js/jquery.min.js}"></script>
<script th:src="@{/assets/js/jquery.dropotron.min.js}"></script> <script th:src="@{/assets/js/jquery.dropotron.min.js}"></script>