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.commonmark:commonmark:0.18.0")
implementation ("net.coobird:thumbnailator:0.4.14")
implementation ("com.drewnoakes:metadata-extractor:2.19.0")
implementation("org.springframework.boot:spring-boot-starter-security")

View File

@ -1,12 +1,12 @@
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.context.annotation.Bean
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.WebMvcConfigurer
import java.time.Duration
@Configuration
@ -17,12 +17,15 @@ class AppConfig : WebMvcConfigurer {
@Value("\${resource.location}")
private val resourceLocation: String? = null
val cacheControl: CacheControl = CacheControl.maxAge(Duration.ofDays(365))
@Bean
fun authInterceptor(): BumsInterceptor {
return BumsInterceptor()
}
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) {

View File

@ -4,6 +4,9 @@ import com.google.gson.Gson
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
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.service.JwtService
import org.springframework.beans.factory.annotation.Autowired
@ -98,6 +101,8 @@ class BumsInterceptor : HandlerInterceptor {
(it.getAttribute(WRITE_PERMISSION_KEY) as? Boolean)?.let { permission ->
if (permission) {
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.utils.LogService
import kr.lunaticbum.back.lun.utils.getFileExtension
import net.coobird.thumbnailator.Thumbnails
import org.commonmark.node.Node
import org.commonmark.parser.Parser
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.core.io.Resource
import org.springframework.core.io.UrlResource
import org.springframework.data.domain.Pageable
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.reactive.function.client.WebClient
import reactor.kotlin.core.publisher.toMono
import java.io.*
import java.net.URLDecoder
import java.text.SimpleDateFormat
@ -219,16 +218,54 @@ class BlogController() {
val firstImg: Element? = doc.select("img")?.first()
val imgSrc: String = firstImg?.attr("src") ?: ""
it.image = imgSrc
it.thumb = imgSrc.replaceBeforeLast(".", "_thumbnail.")
generateThumbnail(imgSrc.split("/").last(), 200)
it.html = doc.text()
}
it.title = if ((it.title?.length ?: 0) >= 1) it.title else ""
}
}.chunked(2))
})
}catch (ex: Exception){ex.printStackTrace()}
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")
fun recent() : ResultMV{
val vm = ResultMV("content/blog/viewer")
@ -271,80 +308,75 @@ class BlogController() {
}
@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 lResultMsg = "Suscces"
var out: OutputStream? = null
var printWriter: PrintWriter? = null
var targetFile : File? = null
logService.log("imgUploadPath ${upload.originalFilename}")
res.characterEncoding = "utf-8"
res.contentType = "text/html;charset=utf-8"
var uuid = UUID.randomUUID()
var lResultMsg = "Success"
var out: FileOutputStream? = null
var targetFile: File? = null
val uuid = UUID.randomUUID()
val extension: String = getFileExtension(upload.originalFilename) ?: ""
try {
logService.log("imgUploadPath ${uuid.toString()}")
val bytes = upload.bytes
var f = File(uploadPath)
// logService.log("imgUploadPath ${f.parentFile.parentFile.parentFile.parentFile.absoluteFile}")
// 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()
val f = File(uploadPath)
if (!f.exists()) f.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.flush()
// ckEditor 로 전송
// printWriter = res.writer
// val callback = req.getParameter("CKEditorFuncNum")
// val fileUrl = "/blog/post/image/$uuid.$extension"
// 썸네일 생성 및 저장
Thumbnails.of(originalImagePath)
.width(200) // 가로 크기를 설정
.keepAspectRatio(true)
.toFile(thumbnailPath)
// printWriter.println(
// ("<script type='text/javascript'>"
// + "window.parent.CKEDITOR.tools.callFunction("
// + callback + ",'" + fileUrl + "','이미지를 업로드하였습니다.')"
// + "</script>")
// )
//
// printWriter.flush()
logService.log("imgUploadPath $imgUploadPath")
logService.log("imgUploadPath ${File(imgUploadPath).exists()}")
val metadata: Metadata? = ImageMetadataReader.readMetadata(File(imgUploadPath))
logService.log("Original image saved: ${File(originalImagePath).exists()}")
logService.log("Thumbnail saved: ${File(thumbnailPath).exists()}")
// 메타데이터 읽기 (원본 이미지에서)
val metadata: Metadata? = ImageMetadataReader.readMetadata(File(originalImagePath))
metadata?.let {
it.directories?.forEach { directory ->
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) {
e.printStackTrace()
lResultCode = 1
lResultMsg = "Error: ${e.message}"
} finally {
try {
out?.close()
printWriter?.close()
} catch (e: IOException) {
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.resultMsg = lResultMsg
this.fileName = "$uuid.$extension"
this.thumbnailName = "${uuid}_thumbnail.$extension"
})
return responce
}
}

View File

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

View File

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

View File

@ -3384,4 +3384,59 @@ button.small,
text-align: left;
}
}
/*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;
background: #00000044;
}
#title_field {
font-size: 20px;
}
/*#title_field {*/
/*font-size: 20px;*/
/*}*/
.pop_layer .pop_container {
padding: 20px 25px;

View File

@ -167,4 +167,4 @@ footer {
height: 5vh;
min-height: 5vh;
position: relative;
}
}

View File

@ -16,8 +16,8 @@ let baseData = {
'writeTime' : 0,
}
function goToEditor(path,id,sk) {
location.href = path + id+"?token="+sk;
function goToEditor(id) {
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() {
history.replaceState({}, null, location.pathname);
// 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 = "refresh=; 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 () {
// var accToken = get_cookie("access")
@ -260,4 +291,75 @@ function layer_popup(el){
function urldecode(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.writeTime = [[${srcPost.writeTime}]];
getLocation()
var style = getComputedStyle(document.body)
console.log(style.getPropertyValue('--ContentVerticalMargin'))
console.log(window.c)
console.log(style.getPropertyValue('--FooterHeight'))
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",""))
)
)
// var style = getComputedStyle(document.body)
// console.log(style.getPropertyValue('--ContentVerticalMargin'))
// console.log(window.c)
// console.log(style.getPropertyValue('--FooterHeight'))
// console.log(style.getPropertyValue('--TopHeight'))
editor = new toastui.Editor({
el: document.querySelector('#editor'),
previewStyle: 'tab',
height: editorHeght + 'px',
height: '900px',
width:'95%',
theme:'dark',
usageStatistics : false,
toolbar:null,
initialValue:baseData.content,
theme:"dark",
initialEditType:"wysiwyg",
hooks: {
addImageBlobHook: (blob, callback) => {
@ -84,24 +73,32 @@
</script>
</th:block>
<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'}">
<h1>권한이 없는 뎁쇼?!</h1>
</th:block>
<th:block th:if="${PERMISSION == 'OK'}">
<label for="title_field"></label><input id="title_field" class="write_option">
<div id="editor" ></div>
<div class="write_controllbox">
<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>
<label for="location_field"></label><input class="write_option" readonly id="location_field"/>
</div>
<h1><button id="save" class="write_option" style="width: 100%; position: relative" onclick="save()">저장하셈</button></h1>
</th:block>
</div>
</section>
</th:block>
<th:block layout:fragment="popup_layer">
<div id="popLayer1" class="pop_layer">

View File

@ -14,17 +14,6 @@
let onChange = () => {console.log(editor.getMarkdown())}
document.addEventListener("DOMContentLoaded", 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>
@ -65,30 +54,16 @@
<div class="col-6 col-12-narrower imp-narrower">
<div id="content">
<article>
<header>
<h2>Two Sidebar</h2>
<p>Yup. Two sidebars at the same time.</p>
</header>
<span class="image featured"><img src="images/banner.jpg" alt="" /></span>
<p>Phasellus quam turpis, feugiat sit amet ornare in, hendrerit in lectus.
Praesent semper mod quis eget mi. Etiam eu ante risus. Aliquam erat volutpat.
Aliquam luctus et mattis lectus sit amet pulvinar. Nam turpis nisi
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>
<section class="col-6 col-12-narrower imp-narrower" th:each="post : ${Posts}">
<!-- <span th:text="${cell}"></span>-->
<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>
<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>
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
</div>
</div>
</section>
</article>
</div>

View File

@ -30,12 +30,22 @@
initialValue:baseData.content,
});
}
function loadEditor() {
goToEditor([[${srcPost.id}]]);
}
</script>
</th:block>
<th:block layout:fragment="content" id="content">
<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>
<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>

View File

@ -51,7 +51,7 @@
<section class="col-6 col-12-narrower" th:each="post : ${row}">
<!-- <span th:text="${cell}"></span>-->
<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">
<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>
@ -73,7 +73,12 @@
</div>
</section>
<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>
<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>

View File

@ -14,10 +14,8 @@
let onChange = () => {console.log(editor.getMarkdown())}
document.addEventListener("DOMContentLoaded", onLoaded);
function onLoaded() {
var h = document.querySelector('#main_layer').getBoundingClientRect().height + 'px'
editor = new toastui.Editor({
el: document.querySelector('#editor'),
height: '500px',
width:'100%',
viewer: true,
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" +
"\n" +
"\n"
,
theme:"dark",
});
}
</script>
</th:block>
<th:block layout:fragment="content" id="content">
<div id="main_layer" style="background:#30303594;">
<div id="editor" ></div>
</div>
<section class="wrapper style1">
<div class="container">
<div id="editor" ></div>
</div>
</section>
</th:block>
</html>

View File

@ -9,10 +9,14 @@
<!-- Nav -->
<nav id="nav">
<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_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">
<a href="#">Dropdown</a>
<a href="#">About</a>
<ul>
<li><a href="#">Lorem dolor</a></li>
<li><a href="#">Magna phasellus</a></li>
@ -27,13 +31,9 @@
<li><a href="#">Veroeros feugiat</a></li>
</ul>
</li>
<li><a href="#">Veroeros feugiat</a></li>
<li><a th:href="@{/licenses}">Licenses</a></li>
</ul>
</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>
</nav>

View File

@ -19,6 +19,26 @@
</div>
<th:block th:replace="~{fragments/footer :: footer}"></th:block>
</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 -->
<script th:src="@{/assets/js/jquery.min.js}"></script>
<script th:src="@{/assets/js/jquery.dropotron.min.js}"></script>