This commit is contained in:
lunaticbum 2024-10-11 17:17:57 +09:00
parent a810db5d25
commit 23ed2e43cc
6 changed files with 428 additions and 94 deletions

View File

@ -43,6 +43,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect") implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
implementation ("org.jsoup:jsoup:1.18.1")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
compileOnly("org.projectlombok:lombok") compileOnly("org.projectlombok:lombok")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client") runtimeOnly("org.mariadb.jdbc:mariadb-java-client")

View File

@ -2,21 +2,22 @@ package kr.lunaticbum.back.lun.controllers
import com.google.gson.Gson import com.google.gson.Gson
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kr.lunaticbum.back.lun.configs.GlobalEnvironment import kr.lunaticbum.back.lun.configs.GlobalEnvironment
import kr.lunaticbum.back.lun.model.CurrentWeather import kr.lunaticbum.back.lun.model.*
import kr.lunaticbum.back.lun.model.Message
import kr.lunaticbum.back.lun.model.TelegramMsgService
import kr.lunaticbum.back.lun.model.TelegramUpdate
import kr.lunaticbum.back.lun.utils.LogService import kr.lunaticbum.back.lun.utils.LogService
import org.jsoup.Jsoup
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -32,18 +33,56 @@ class Telegram {
@Autowired @Autowired
lateinit var logService: LogService lateinit var logService: LogService
@Autowired
lateinit var locationLogService: LocationLogService
@ResponseBody @ResponseBody
@GetMapping("hello") @GetMapping("hello")
fun hello(): String { fun hello(): String {
return "hello1212" return "hello1212"
} }
@Autowired
lateinit var rssDataService: RssDataService
@ResponseBody @ResponseBody
@PostMapping("webhook") @PostMapping("webhook")
fun test(httpServletRequest: HttpServletRequest, @RequestBody update : TelegramUpdate?) { fun test(httpServletRequest: HttpServletRequest, @RequestBody update : kr.lunaticbum.back.lun.model.Result) : String {
try { try {
logService.log("test strat ${update}") logService.log("test strat ${Gson().toJson(update)}")
// logService.log("test strat ${Gson().toJson(update)}")
logService.log("test strat ${httpServletRequest.requestURI}") logService.log("test strat ${httpServletRequest.requestURI}")
update?.message?.let {
if(it.text?.startsWith("/") == true) {
it.text?.split(" ")?.let { cmds ->
cmds[0].let { cmd ->
when(cmd.trim()) {
"/get" ->{}
"/jf" ->{
CoroutineScope(Dispatchers.IO).launch {
logService.log("${cmd[0]} Start ${cmd[1]}")
String.format(String(Base64.getMimeDecoder().decode("aHR0cHM6Ly9qYXZtb3N0LnRvL3NlYXJjaC9tb3ZpZS8lcw==".toByteArray())),cmd[1]).getJ().let { doc -> FeedParseManager.parse(doc,rssDataService) }
logService.log("${cmd[0]} END ${cmd[1]}")
}
CoroutineScope(Dispatchers.IO).launch {
logService.log("on Cmd JF with SO")
logService.log("${cmd[0]} Start ${cmd[1]}")
String.format(String(Base64.getMimeDecoder().decode("aHR0cHM6Ly9rcjcwLnNvZ2lybC5zby8/cz0lcw==".toByteArray())),cmd[1]).getJ().let { doc -> FeedParseManager.parse(doc,rssDataService)}
logService.log("${cmd[0]} END ${cmd[1]}")
}
}
else -> {}
}
}
}
if (it.text?.contains(" ") == true) {
} else {
}
}
if (it.text?.contains("어디") == true) { it.from?.id?.let { sendMsg(it.toString()) } }
}
// val client0 = WebClient.create() // val client0 = WebClient.create()
// client0.get() // client0.get()
// .uri("https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/getUpdates") // .uri("https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/getUpdates")
@ -71,8 +110,8 @@ class Telegram {
// fun test(@RequestBody str : String) { // fun test(@RequestBody str : String) {
// println("path >>> $str") // println("path >>> $str")
//>>>>>>> ab915d0a416c69708f1df1ad76d7a14c779c1f59 //>>>>>>> ab915d0a416c69708f1df1ad76d7a14c779c1f59
} }
return "Success"
} }
@ -110,78 +149,81 @@ class Telegram {
} }
@Bean // @Bean
@Scheduled(cron = "0 0/2 * * * *") // // @Scheduled(cron = "0 0/2 * * * *") //
fun pollingTelegramUpdate() { // fun pollingTelegramUpdate() {
try { // try {
logService.log("pollingTelegramUpdate telegramBotKey >>>> ${globalEvv.telegramBotKey}") // logService.log("pollingTelegramUpdate telegramBotKey >>>> ${globalEvv.telegramBotKey}")
logService.log("pollingTelegramUpdate telegramMyId >>>> ${globalEvv.telegramMyId}") // logService.log("pollingTelegramUpdate telegramMyId >>>> ${globalEvv.telegramMyId}")
logService.log("pollingTelegramUpdate weatherApiKey >>>> ${globalEvv.weatherApiKey}") // logService.log("pollingTelegramUpdate weatherApiKey >>>> ${globalEvv.weatherApiKey}")
if ( // if (
((globalEvv.weatherApiKey?.length ?: 0) > 3 )&& // ((globalEvv.weatherApiKey?.length ?: 0) > 3 )&&
((globalEvv.telegramBotKey?.length ?: 0) > 3 )&& // ((globalEvv.telegramBotKey?.length ?: 0) > 3 )&&
((globalEvv.telegramMyId?.length ?: 0) > 3) // ((globalEvv.telegramMyId?.length ?: 0) > 3)
) { // ) {
val client0 = WebClient.create() // val client0 = WebClient.create()
val result = client0.get() // val result = client0.get()
.uri("https://api.telegram.org/${globalEvv.telegramBotKey}/getUpdates") // .uri("https://api.telegram.org/${globalEvv.telegramBotKey}/getUpdates")
.retrieve() // .retrieve()
.bodyToMono(String::class.java).block() ?: "FAIL" // .bodyToMono(String::class.java).block() ?: "FAIL"
logService.log("pollingTelegramUpdate result >>>> $result") // logService.log("pollingTelegramUpdate result >>>> $result")
Gson().fromJson(result, TelegramUpdate::class.java)?.let { sss -> // Gson().fromJson(result, TelegramUpdate::class.java)?.let { sss ->
logService.log("pollingTelegramUpdate sss >>>> $sss") // logService.log("pollingTelegramUpdate sss >>>> $sss")
if (sss.isSucces()) { // if (sss.isSucces()) {
sss.result?.filter { // sss.result?.filter {
((it.message?.date ?: 0L) * 1000L) > before5Min() // ((it.message?.date ?: 0L) * 1000L) > before5Min()
}?.forEach { // }?.forEach {
logService.log("pollingTelegramUpdate before Query doOnSuccess m >>>> ${it}") // logService.log("pollingTelegramUpdate before Query doOnSuccess m >>>> ${it}")
it.message?.let { msg -> // it.message?.let { msg ->
logService.log("pollingTelegramUpdate before Query doOnSuccess m >>>> ${msg.message_id}") // logService.log("pollingTelegramUpdate before Query doOnSuccess m >>>> ${msg.message_id}")
qns(msg.message_id,msg) // qns(msg.message_id,msg)
} // }
} // }
} // }
} // }
} // }
}catch (e : Exception) { // }catch (e : Exception) {
e.printStackTrace() // e.printStackTrace()
} // }
} // }
fun qns(it : String, msg : Message) { // fun qns(it : String, msg : Message) {
var doSave = true // var doSave = true
telegramService.findById(it)?.subscribe( { m -> // telegramService.findById(it)?.subscribe( { m ->
logService.log("pollingTelegramUpdate doOnSuccess m >>>> $m") // logService.log("pollingTelegramUpdate doOnSuccess m >>>> $m")
if (m != null) { // if (m != null) {
if (msg.text?.contains("어디") == true) { // if (msg.text?.contains("어디") == true) {
//
// } else {
// logService.log(msg.text ?: "NONE")
// }
// } else {
// doSave = false
// }
// },{ e ->
// e.printStackTrace()
// },{
// if (doSave) {
// telegramService.save(msg)
// if (msg.text?.contains("어디") == true || msg.text?.startsWith("\"") == true) {
// sendMsg()
// }
// }
// logService.log("pollingTelegramUpdate doOnSuccess comp")
// })
// }
} else { fun sendMsg(target : String) {
logService.log(msg.text ?: "NONE")
}
} else {
doSave = false
}
},{ e ->
e.printStackTrace()
},{
if (doSave) {
telegramService.save(msg)
if (msg.text?.contains("어디") == true || msg.text?.startsWith("\"") == true) {
sendMsg()
}
}
logService.log("pollingTelegramUpdate doOnSuccess comp")
})
}
fun sendMsg() {
val client = WebClient.create() val client = WebClient.create()
locationLogService.getLocationLog()?.let {
client.get() client.get()
.uri("https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage?chat_id=${globalEvv.telegramMyId}&text=/g_mustShareLocation") .uri("https://api.telegram.org/${globalEvv.telegramBotKey}/sendMessage?chat_id=${target}&text=${SimpleDateFormat("yyyy/MM/dd-HH:mm:ss").format(Date(it.time))}\n${it.mAddressLines.first()}")
.retrieve() .retrieve()
.bodyToMono(String::class.java).block() ?: "FAIL" .bodyToMono(String::class.java).block() ?: "FAIL"
} }
}
} }

View File

@ -1,15 +1,23 @@
package kr.lunaticbum.back.lun.model package kr.lunaticbum.back.lun.model
import com.google.gson.Gson
import kr.lunaticbum.back.lun.utils.LogService import kr.lunaticbum.back.lun.utils.LogService
import lombok.AllArgsConstructor import lombok.AllArgsConstructor
import lombok.Data import lombok.Data
import lombok.NoArgsConstructor import lombok.NoArgsConstructor
import org.jsoup.Jsoup
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.annotation.Id
import org.springframework.data.domain.Page
import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.List
class BumsPrivate { class BumsPrivate {
} }
@ -64,7 +72,7 @@ class LocationLog {
@Repository @Repository
interface LocationLogRepository : ReactiveMongoRepository<LocationLog, String> { interface LocationLogRepository : ReactiveMongoRepository<LocationLog, String> {
fun findFirstByOrderByTimeDesc() : Mono<LocationLog>
fun save(log: LocationLog): Mono<LocationLog> fun save(log: LocationLog): Mono<LocationLog>
} }
interface LocationService { interface LocationService {
@ -79,6 +87,9 @@ class LocationLogService : LocationService {
@Autowired @Autowired
private lateinit var logRepository: LocationLogRepository private lateinit var logRepository: LocationLogRepository
fun getLocationLog() : LocationLog? {
return logRepository.findFirstByOrderByTimeDesc().block()
}
fun save(log: LocationLog) { fun save(log: LocationLog) {
@ -89,3 +100,220 @@ class LocationLogService : LocationService {
} }
} }
interface RssDataInterface {
fun title() : String
fun thumbnailUrl() : String
fun originPage() : String
fun description() : String
fun pubDate() : Long
fun category() : RssDataType
fun getCho() : String?
}
enum class RssDataType {
NO_DATA,
YOUTUBE,
NewsFeed,
GURU,
Most,
TAGS,
REDDIT,
REDDIT_nsfw,
Dotax,
FmKorae,
DcInside,
RuliWeb,
Clien,
TheQoo,
Arca;
// fun getResId() = when (this) {
// YOUTUBE -> R.drawable.youtube
// REDDIT, REDDIT_nsfw -> R.drawable.reddit
// Dotax -> R.drawable.daum
// FmKorae -> R.drawable.fmk
// DcInside -> R.drawable.dcinside
// Arca -> R.drawable.arca
// else -> {
// 0
// }
// }
fun defaultImgSize() = when (this) {
YOUTUBE -> 200
REDDIT_nsfw,GURU,Most -> 360
else -> { 120 }
}
// fun getDefaultVisibiliy() = when (this) {
// REDDIT_nsfw,GURU,Most,NewsFeed -> View.GONE
// else -> { View.VISIBLE }
// }
}
class RssData : RssDataInterface {
@Id
var originPage : String? = null
var title : String? = null
var description : String? = null
var thumbnail : String? = null
var pubDate : Long = 0L
var category : String? = null
var chosung : String? = null
var mRssDataType : RssDataType? = null
override fun title(): String {
return when(category()){
RssDataType.NewsFeed -> {
if(title?.length ?: 0 > 30) title?.substring(0,30).plus("...") else title ?: ""
}
else -> title ?: ""
}.apply {
// chosung = JamoUtils.split(this).joinToString("")
}
}
override fun thumbnailUrl(): String {
return thumbnail ?: ""
}
override fun originPage(): String {
return originPage ?: ""
}
override fun description(): String {
return when(category()){
RssDataType.YOUTUBE -> {
if(description?.contains("게시자") == true) description!!.split("게시자")[0] else description ?: ""
}
RssDataType.NewsFeed -> {
category().name
}
else -> description.plus(" / ").plus(category().name)
}
}
override fun pubDate(): Long {
return pubDate
}
override fun category(): RssDataType {
if (mRssDataType == null)
mRssDataType = RssDataType.valueOf(category!!)
return mRssDataType!!
}
override fun getCho(): String? {
return chosung
}
}
val USAGT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"
fun String.getJ() = Jsoup.connect(this).userAgent(USAGT).get()
object FeedParseManager {
val parsers = listOf<SoInterface>(QVZTb2dpcmw,SkFWTW9zdA)
fun parse(doc : org.jsoup.nodes.Document, service: RssDataService) {
try {
parsers.filter { doc.title().contains(it.getName()) }.first()?.let {
it.parse(doc,service)
}
} catch (e : Exception) {
e.printStackTrace()
}
}
}
interface SoInterface{
fun getName() : String
fun parse(doc : org.jsoup.nodes.Document,service: RssDataService)
}
object QVZTb2dpcmw : SoInterface {
override fun getName(): String {
return String(Base64.getMimeDecoder().decode(this.javaClass.simpleName.plus("==").toByteArray()))
}
override fun parse(doc : org.jsoup.nodes.Document, service : RssDataService) {
doc.getElementsByTag("article").forEach { article ->
val title = article.getElementsByTag("a").get(0).attr("title")
val href = article.getElementsByTag("a").get(0).attr("href")
val img = article.getElementsByTag("img").get(0).attr("data-src")
service.save(RssData().apply {
this.originPage = href
this.title = title
this.description = "Sogirl"
this.thumbnail = img
this.pubDate = Date().time
this.category = RssDataType.GURU.name
})
}
}
}
object SkFWTW9zdA : SoInterface {
var dmy = SimpleDateFormat("dd-MM-yyyy")
override fun getName(): String {
return String(Base64.getMimeDecoder().decode(this.javaClass.simpleName.plus("==").toByteArray()))
}
override fun parse(doc: org.jsoup.nodes.Document, service: RssDataService) {
doc.getElementsByClass("card").forEach { card ->
var thumb = if(card.getElementsByTag("img").size > 0) card.getElementsByTag("img").get(0).attr("src") else ""
if (thumb.contains("No+Poster")) thumb = if(card.getElementsByTag("img").size > 0) card.getElementsByTag("img").get(0).attr("data-src") else thumb
var model = if(card.getElementsByTag("img").size > 0) card.getElementsByTag("img").get(0).attr("alt") else ""
if(card.getElementsByClass("card-block").size > 0) if(card.getElementsByClass("card-block").size > 0) {
val link = card.getElementsByClass("card-block").get(0).getElementsByTag("a").get(0).attr("href")
val title = card.getElementsByClass("card-block").get(0).getElementsByTag("a").get(0).attr("title")
val date = card.getElementsByTag("span").get(0).text()
service.save(RssData().apply {
description = model
thumbnail = thumb
originPage = link
this.title = title
try {
pubDate = dmy.parse(date).time
}catch (e : Exception) {e.printStackTrace()}
})
}
}
}
}
@Repository
interface RssDataRepository : ReactiveMongoRepository<RssData, String> {
fun findFirstByOriginPageEquals(originPage : String): Mono<RssData>
fun findAllByOrderByPubDate() : Mono<List<RssData>>
fun save(log: RssData): Mono<RssData>
}
@Service
class RssDataService {
@Autowired
private lateinit var logService: LogService
@Autowired
private lateinit var rssDataRepository: RssDataRepository
fun hasItem(originPage : String) {
}
fun getLocationLog() : List<RssData>? {
return rssDataRepository.findAllByOrderByPubDate().block()
}
fun save(log: RssData) {
println("saved msg before ${Gson().toJson(log)}")
log.originPage?.let {
if(rssDataRepository.findFirstByOriginPageEquals(it).block() == null) {
rssDataRepository.save(log)
.subscribe({ println("saved msg after ${it}") }, { e -> e.printStackTrace() }, {
println("saved msg comp")
})
} else {
println("있어???")
}
}
}
}

View File

@ -0,0 +1,23 @@
:root{
--ButtonWidth:45%;
--ButtonHeight:45px;
}
#save {
right: 0;
position: absolute;
width: var(--ButtonWidth);
height: var(--ButtonHeight);
}
.layer > * {
height: var(--ButtonHeight);
}
input {
width: 100%;
}
select , #hashtag{
margin-left: 1%;
width: fit-content;
margin-right: 1%;
}

View File

@ -1,38 +1,63 @@
:root { :root {
--WindowFull : 100svh; --WindowFull : 100%;
--TopHeight: 50px; --TopHeight: 160px;
--FooterHeight: 120px; --FooterHeight: 160px;
--ContentVerticalMargin: 5px;
}
input, select ,button{
color: white;
background: #2d2f34;
border: black;
border-width: 1px;
padding: 0;
margin: 0;
position: relative;
} }
body, html { body, html {
background-color: black; background-color: black;
margin: 0px; margin: 0px;
margin-left: 2.5%;
margin-right: 2.5%;
height: 100lvh; height: 100lvh;
width: 95%;
} }
header { header {
width: 100%;
align-content: center; align-content: center;
position: relative; position: relative;
background-color: Gray; background-color: Gray;
height: var(--TopHeight); height: var(--TopHeight);
background-image: url("../blog/post/images/42cc3207-42a4-4ceb-8a2f-f5f7a89496fc.jpg");
background-repeat: revert;
background-size: contain;
background-origin: revert;
} }
#content { #content {
margin-left: 2.5%;
margin-right: 2.5%;
position: relative; position: relative;
overflow-y: auto;
overflow-x: clip;
background: black;
min-height: calc((var(--TopHeight) + var(--FooterHeight)) * 2);
height: calc(var(--WindowFull) - calc(var(--FooterHeight) + var(--TopHeight))); height: calc(var(--WindowFull) - calc(var(--FooterHeight) + var(--TopHeight)));
background-image: url("../blog/post/images/bb109b5a-f907-4da1-9c4f-55533395ed6e.jpg");
background-repeat: revert;
background-size: contain;
background-origin: revert;
} }
#content > * { #content > * {
margin-top: 5px; margin-top: var(--ContentVerticalMargin);
margin-bottom: 5px; margin-bottom: var(--ContentVerticalMargin);
} }
footer { footer {
width: 100%;
align-content: center; align-content: center;
position: relative; position: relative;
height: var(--FooterHeight); height: var(--FooterHeight);
background-color: aquamarine; background-image: url("../blog/post/images/42cc3207-42a4-4ceb-8a2f-f5f7a89496fc.jpg");
background-repeat: revert;
background-size: contain;
transform: scaleY(-1);
} }

View File

@ -7,20 +7,33 @@
<head> <head>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script> <script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script>
<script type="text/javascript" th:src="@{/js/toast-ui.js}"></script> <script type="text/javascript" th:src="@{/js/toast-ui.js}"></script>
<link th:href="@{/css/blog.css}" rel="stylesheet" />
<link th:href="@{/css/toast-ui.css}" rel="stylesheet" /> <link th:href="@{/css/toast-ui.css}" rel="stylesheet" />
<link th:href="@{/css/toast-ui-dark.css}" rel="stylesheet" /> <link th:href="@{/css/toast-ui-dark.css}" rel="stylesheet" />
<!-- <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor-dark.css" />--> <!-- <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor-dark.css" />-->
<script th:inline="javascript"> <script th:inline="javascript">
let editor var editor
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('#content').getBoundingClientRect().height + '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'))
var editorHeght = (
document.querySelector('#content').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: '500px', height: editorHeght+ 'px',
width:'100%', width:'95%',
theme:'dark', theme:'dark',
usageStatistics : false, usageStatistics : false,
toolbar:null, toolbar:null,
@ -80,14 +93,16 @@
</script> </script>
</head> </head>
<div layout:fragment="content" id="content"> <div layout:fragment="content" id="content">
<div class="layer">
<input id="title_layer" /> <input id="title_layer" />
</div>
<div id="editor" ></div> <div id="editor" ></div>
<div id="hashtag_layer"> <div class="layer">
<select > </select> <select > </select>
<input id="hashtag" /> <input id="hashtag" />
</div> </div>
<div id="controll_layer" > <div id="layer" >
<button onclick="save()">asdsad</button> <button id="save" onclick="save()">asdsad</button>
</div> </div>
</div> </div>
</html> </html>