...
This commit is contained in:
@@ -90,7 +90,7 @@ resource.handler=.
|
||||
resource.location=.
|
||||
server.forward-headers-strategy=framework
|
||||
#>>>>>>> ab915d0a416c69708f1df1ad76d7a14c779c1f59
|
||||
|
||||
logging.level.org.thymeleaf=DEBUG
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
logging.level.org.hibernate.SQL=DEBUG
|
||||
|
||||
@@ -140,24 +140,28 @@ a.btn_layerClose:hover {
|
||||
margin-top: 2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row; /* [수정] 아이템을 가로(row)로 정렬합니다. */
|
||||
align-items: stretch; /* [수정] 모든 박스가 동일한 높이를 갖도록 stretch로 변경 */
|
||||
list-style: none;
|
||||
gap: 15px; /* Use gap for spacing */
|
||||
}
|
||||
|
||||
.write_option {
|
||||
display: flex; /* Use flexbox for centering */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column; /* [수정] 자식(제목, 내용)을 세로로 쌓습니다. */
|
||||
justify-content: flex-start; /* 내용을 상단부터 표시합니다. */
|
||||
align-items: flex-start; /* 모든 내용을 왼쪽 정렬합니다. */
|
||||
|
||||
flex: 1 1 0%; /* [핵심] 1:1:1 비율로 동일한 너비를 갖도록 설정 */
|
||||
min-width: 0; /* [추가] flex item이 내용 래핑을 위해 축소될 수 있도록 허용 */
|
||||
|
||||
padding: 0.75em;
|
||||
margin: 0;
|
||||
background: var(--pure-white, #fff);
|
||||
border: solid 1px #e0e0e0;
|
||||
border-radius: 5px;
|
||||
color: inherit;
|
||||
min-height: 48px; /* Set a minimum height */
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,11 +182,35 @@ a.btn_layerClose:hover {
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
min-width: unset;
|
||||
background: var(--pure-white, #fff);
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
color: var(--font-color_default, #474747);
|
||||
}
|
||||
/* * --- QUILL TOOLBAR SEPARATOR (구분선) ---
|
||||
* 기본 'margin-right: 15px' 대신 패딩과 보더로 구분선을 추가합니다.
|
||||
*/
|
||||
|
||||
|
||||
.ql-toolbar.ql-snow .ql-formats {
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
.ql-toolbar.ql-snow .ql-formats {
|
||||
min-width: unset;
|
||||
margin-right: 8px; /* 기본 마진 감소 */
|
||||
padding-right: 8px; /* 보더 오른쪽에 패딩 추가 */
|
||||
border-right: 1px solid #e0e0e0; /* 테마의 보더 색상으로 구분선 추가 */
|
||||
}
|
||||
|
||||
/* 툴바의 마지막 그룹에는 구분선과 불필요한 여백을 제거합니다. */
|
||||
.ql-toolbar.ql-snow .ql-formats:last-child {
|
||||
min-width: unset;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Ensure editor content uses the theme's default font and size */
|
||||
.ql-editor {
|
||||
@@ -191,6 +219,27 @@ a.btn_layerClose:hover {
|
||||
line-height: 1.65em;
|
||||
}
|
||||
|
||||
|
||||
.ql-snow.ql-toolbar button, .ql-snow .ql-toolbar button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
height: 24px;
|
||||
padding: 3px 5px;
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
padding-left: unset;
|
||||
padding-right: unset;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
/*
|
||||
* --- COMMENT SECTION ---
|
||||
* Styled to fit the main theme.
|
||||
@@ -239,13 +288,20 @@ a.btn_layerClose:hover {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 읽기 모드 컨트롤 박스 내부의 태그 스타일 */
|
||||
/* 컨트롤 박스 내부 공통 제목 스타일 */
|
||||
.write_option .tag-title {
|
||||
font-weight: 600;
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0.35em; /* [수정] 제목과 내용 사이에 하단 여백을 줍니다. */
|
||||
color: #555;
|
||||
font-size: 0.85em; /* [추가] 제목 라벨 폰트를 살짝 작게 */
|
||||
text-transform: uppercase; /* [추가] 라벨처럼 보이도록 영문 대문자 처리 */
|
||||
|
||||
/* 태그 제목(HASHTAGS)이 태그 아이템과 동일한 스타일을 공유할 경우 대비 (선택 사항) */
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 컨트롤 박스 내부 공통 태그 아이템 스타일 (뷰어 모드용) */
|
||||
.write_option .tag-item {
|
||||
display: inline-block;
|
||||
background-color: var(--almost-white, #f7f7f7);
|
||||
@@ -261,4 +317,149 @@ a.btn_layerClose:hover {
|
||||
.write_option.controlbox-category:not(.btn-example),
|
||||
.write_option.controlbox-hashtag:not(.btn-example) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* [추가] 태그/내용을 감싸는 래퍼 (줄바꿈 담당) */
|
||||
.tag-content-wrapper {
|
||||
display: flex; /* 내부 아이템(태그 등)을 가로로 배치 */
|
||||
flex-wrap: wrap; /* 태그가 많으면 자동 줄바꿈 (기본값) */
|
||||
gap: 4px 6px; /* 태그 사이의 간격 (세로 4px, 가로 6px) */
|
||||
line-height: 1.4; /* 내용 줄간격 */
|
||||
width: 100%; /* 부모 박스(write_option) 너비를 채움 */
|
||||
}
|
||||
|
||||
|
||||
/* --- 컨트롤 박스 공통 스타일 (카테고리, 해시태그, 위치) --- */
|
||||
/* [수정] 3개 박스에 공통 스타일을 적용합니다. */
|
||||
.write_option.controlbox-category,
|
||||
.write_option.controlbox-hashtag,
|
||||
.write_option.controlbox-location {
|
||||
/* 모든 박스는 .write_option의 flex: 1 1 0% 규칙을 따릅니다. */
|
||||
box-sizing: border-box;
|
||||
min-height: 50px; /* 모든 박스의 최소 높이 통일 */
|
||||
}
|
||||
|
||||
/* --- 컨트롤 박스 내부 태그 아이템 공통 스타일 (팝업 편집용) --- */
|
||||
/* (카테고리와 해시태그 박스 내부의 태그 아이템만 이 스타일을 공유) */
|
||||
.write_option.controlbox-location .tag-item,
|
||||
.write_option.controlbox-category .tag-item,
|
||||
.write_option.controlbox-hashtag .tag-item {
|
||||
background-color: var(--highlight-bg-color, #e9e9e9);
|
||||
color: var(--highlight-text-color, #333);
|
||||
border-radius: 5px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.9em;
|
||||
white-space: normal;
|
||||
margin-right: 0; /* .tag-content-wrapper의 gap이 간격을 제어하므로 마진 제거 */
|
||||
}
|
||||
|
||||
/* --- 컨트롤 박스: 카테고리 (개별 내부 스타일) --- */
|
||||
.write_option.controlbox-category .tag-title {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
/* margin-right: 8px; <-- 세로 레이아웃으로 변경되어 불필요 */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- 컨트롤 박스: 해시태그 (개별 내부 스타일) --- */
|
||||
.write_option.controlbox-hashtag {
|
||||
/* 태그 사이의 내부 간격 (래퍼가 대신 처리) */
|
||||
/* gap: 8px 10px; */
|
||||
}
|
||||
|
||||
.write_option.controlbox-hashtag .tag-title {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
margin-right: 0px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- 컨트롤 박스: 콘텐츠 래퍼 동작 정의 --- */
|
||||
|
||||
/* 해시태그는 .tag-content-wrapper의 기본값(flex-wrap: wrap)을 그대로 사용합니다. */
|
||||
|
||||
/* [추가/그룹화] 카테고리와 위치 박스는 내용이 줄바꿈되지 않도록 공통 처리합니다. */
|
||||
/*.write_option.controlbox-category .tag-content-wrapper,*/
|
||||
/*.write_option.controlbox-location .tag-content-wrapper {*/
|
||||
/* white-space: nowrap; !* 내용이 길어도 한 줄로 표시 (줄바꿈 방지) *!*/
|
||||
/* overflow: hidden;*/
|
||||
/* text-overflow: ellipsis;*/
|
||||
/*}*/
|
||||
|
||||
|
||||
/* 추가: 컨트롤 박스 자체의 레이아웃 (이전 CSS에서 남음. 현재 구조에서는 영향 없음) */
|
||||
#main_container .write_option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* === POPUP TAG LIST STYLES (신규 추가) === */
|
||||
.pop_conts .tag-list {
|
||||
display: flex; /* 태그들을 가로로 나열 */
|
||||
flex-wrap: wrap; /* 공간이 부족하면 다음 줄로 자동 줄바꿈 */
|
||||
gap: 8px; /* 태그 아이템 사이의 간격을 8px로 지정 */
|
||||
margin-bottom: 1.5em; /* 태그 목록과 하단 입력창 사이의 간격 */
|
||||
padding-bottom: 1.5em; /* 태그 목록 하단 패딩 */
|
||||
border-bottom: 1px solid #e0e0e0; /* 입력창과 목록을 구분하는 선 */
|
||||
}
|
||||
|
||||
.pop_conts .tag-item {
|
||||
display: inline-block;
|
||||
background-color: var(--almost-white, #f7f7f7);
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 15px; /* 둥근 알약 모양 */
|
||||
padding: 0.3em 0.9em; /* 내부 여백 */
|
||||
font-size: 0.9em;
|
||||
cursor: pointer; /* 클릭 가능하도록 커서 변경 */
|
||||
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.pop_conts .tag-item:hover {
|
||||
background-color: #e0e0e0; /* 호버 시 배경색 변경 */
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
/* === (신규 추가) POPUP STAGING AREA STYLES === */
|
||||
.pop_conts .tag-list-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
color: #777;
|
||||
margin-top: 1.2em; /* 각 섹션 상단 여백 */
|
||||
margin-bottom: 0.5em;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 선택된 항목을 담는 스테이징 영역 스타일 */
|
||||
.pop_conts .staging-area {
|
||||
min-height: 40px;
|
||||
padding: 0.5em;
|
||||
background: #fdfdfd;
|
||||
border: 1px dashed #ccc; /* 점선 테두리로 구분 */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 스테이징 영역 내부의 태그에서 'X' (삭제) 버튼 스타일 */
|
||||
.staging-area .tag-item .remove-tag {
|
||||
font-family: "Courier New", monospace; /* 'X' 버튼 글꼴 */
|
||||
font-weight: bold;
|
||||
color: #cc0000; /* 어두운 빨간색 */
|
||||
margin-left: 8px;
|
||||
cursor: pointer; /* 클릭 가능하도록 */
|
||||
display: inline-block;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.staging-area .tag-item .remove-tag:hover {
|
||||
color: #ff0000; /* 밝은 빨간색 */
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
/* 카테고리 스테이징 영역은 flex가 아니므로, 내부 태그 마진을 별도 지정 */
|
||||
#selected-category-area .tag-item {
|
||||
margin: 0;
|
||||
}
|
||||
#selected-category-area i, #selected-hashtags-area i {
|
||||
color: #999;
|
||||
}
|
||||
@@ -1,18 +1,128 @@
|
||||
/*.layer {*/
|
||||
/* height: 100%;*/
|
||||
/* place-content: space-between;*/
|
||||
/* place-items: stretch;*/
|
||||
/* display: grid;*/
|
||||
/* gap: 10px;*/
|
||||
/* grid-auto-rows: minmax(200px, auto);*/
|
||||
/* grid-template-columns: repeat(auto-fill, minmax(200px, auto));*/
|
||||
/* width: 100%;*/
|
||||
/*}*/
|
||||
/*!*.where_item {*!*/
|
||||
/*!* justify-content: space-between;*!*/
|
||||
/*!* flex-wrap: wrap;*!*/
|
||||
/*!* flex-direction: row;*!*/
|
||||
/*!* width: 100%;*!*/
|
||||
/*!* border-radius: 10px;*!*/
|
||||
/*!* background: #F0F0F524;*!*/
|
||||
/*!*}*!*/
|
||||
/* 기본 레이아웃 및 폰트 */
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f4f7f6;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#main_layer {
|
||||
max-width: 900px;
|
||||
margin: 30px auto;
|
||||
background-color: #ffffff;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.layer {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* 개별 위치 로그 아이템 스타일 */
|
||||
.location-item {
|
||||
background-color: #fcfcfc;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.location-item:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 헤더 (날짜 및 경과 시간) */
|
||||
.location-header {
|
||||
display: flex; /* 가로로 배치 */
|
||||
justify-content: space-between; /* 양 끝 정렬 */
|
||||
align-items: center;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.location-time {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.location-between {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
background-color: #eef;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 본문 (주소 및 좌표) */
|
||||
.location-body {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.location-address {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
color: #444;
|
||||
word-break: break-all; /* 긴 주소 자동 줄바꿈 */
|
||||
}
|
||||
|
||||
.location-coords {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
display: flex; /* 위도, 경도, 국가명 가로 배치 */
|
||||
gap: 15px; /* 각 요소 사이 간격 */
|
||||
flex-wrap: wrap; /* 화면 좁아지면 줄바꿈 */
|
||||
}
|
||||
|
||||
.location-coords b {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.location-country {
|
||||
color: #777;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 페이지네이션 스타일 */
|
||||
.pagination {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.pagination a, .pagination span {
|
||||
display: inline-block;
|
||||
padding: 8px 15px;
|
||||
margin: 0 4px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #007bff;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.pagination span { /* 현재 페이지 또는 비활성화된 버튼 */
|
||||
color: #6c757d;
|
||||
background-color: #e2e6ea;
|
||||
border-color: #e2e6ea;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 기존 CSS에서 불필요한 부분 제거 또는 통합 */
|
||||
.where_item { /* 이전 코드의 클래스. 새 클래스로 대체되었으므로 필요 없으면 제거 */
|
||||
/* display: none; */
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
* =================================================================================
|
||||
*/
|
||||
|
||||
var stagedCategory = 'none';
|
||||
var stagedHashtags = []; // 해시태그는 배열로 관리
|
||||
|
||||
// 전역 변수: Quill 에디터 인스턴스와 게시물 기본 데이터를 저장합니다.
|
||||
var quill = null;
|
||||
var currentLat = 0.0;
|
||||
@@ -55,6 +58,83 @@ $(document).ready(function() {
|
||||
$('.open-login-popup').on('click', function() {
|
||||
openPopup(this);
|
||||
});
|
||||
/* === (대규모 수정) 팝업 입력/적용/취소 버튼 로직 === */
|
||||
|
||||
// --- 1. Category Popup Logic ---
|
||||
const categoryInput = document.getElementById('category-input');
|
||||
const addCategoryBtn = document.getElementById('add-category-btn');
|
||||
const applyCategoryBtn = document.getElementById('apply-category-btn'); // (신규) 적용 버튼 선택
|
||||
|
||||
// "Add" 버튼 (입력창에서 추가) 로직 (수정됨)
|
||||
if (addCategoryBtn) {
|
||||
addCategoryBtn.addEventListener('click', function() {
|
||||
const newCategory = categoryInput.value.trim();
|
||||
if (newCategory) {
|
||||
stagedCategory = newCategory; // 1. 임시 변수(stagedCategory) 업데이트
|
||||
renderStagedCategory(); // 2. 스테이징 UI 새로고침
|
||||
categoryInput.value = ''; // 3. 입력창 비우기 (팝업 유지)
|
||||
}
|
||||
});
|
||||
}
|
||||
// "Enter" 키 지원 (기존과 동일)
|
||||
if (categoryInput) {
|
||||
categoryInput.addEventListener('keyup', function(e) {
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
addCategoryBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
// (신규) "Apply" 버튼 로직
|
||||
if (applyCategoryBtn) {
|
||||
applyCategoryBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault(); // A태그 기본 동작(새로고침/이동) 방지
|
||||
|
||||
// "적용" 시점에만 실제 baseData를 임시 변수 값으로 덮어쓰기
|
||||
baseData.category = stagedCategory;
|
||||
|
||||
updateControlBoxDisplay(); // 본문 에디터 UI 갱신
|
||||
closePopup(); // 팝업 닫기
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// --- 2. Hashtag Popup Logic ---
|
||||
const hashtagInput = document.getElementById('hashtag-input');
|
||||
const addHashtagBtn = document.getElementById('add-hashtag-btn');
|
||||
const applyHashtagBtn = document.getElementById('apply-hashtag-btn'); // (신규) 적용 버튼 선택
|
||||
|
||||
// "Add" 버튼 (입력창에서 추가) 로직 (수정됨)
|
||||
if (addHashtagBtn) {
|
||||
addHashtagBtn.addEventListener('click', function() {
|
||||
// 1. 임시 배열(stagedHashtags)에 추가 (신규 헬퍼 함수 사용)
|
||||
if (addTagToStaged(hashtagInput.value)) {
|
||||
renderStagedHashtags(); // 2. 추가 성공 시 스테이징 UI 새로고침
|
||||
}
|
||||
hashtagInput.value = ''; // 3. 입력창은 항상 비움
|
||||
});
|
||||
}
|
||||
// "Enter" 키 지원 (기존과 동일)
|
||||
if (hashtagInput) {
|
||||
hashtagInput.addEventListener('keyup', function(e) {
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
addHashtagBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
// (신규) "Apply" 버튼 로직
|
||||
if (applyHashtagBtn) {
|
||||
applyHashtagBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault(); // A태그 기본 동작 방지
|
||||
|
||||
// "적용" 시점에 임시 배열을 쉼표(,)로 구분된 문자열로 변환하여 실제 baseData에 저장
|
||||
baseData.tags = stagedHashtags.join(',');
|
||||
|
||||
updateControlBoxDisplay(); // 본문 에디터 UI 갱신
|
||||
closePopup(); // 팝업 닫기
|
||||
});
|
||||
}
|
||||
/* =============================================================== */
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -98,8 +178,8 @@ function initEditor(useEditor = false) {
|
||||
['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']
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }], [{ 'align': [] }],
|
||||
['table-better'], [{ 'direction': 'rtl' }], ['clean'], ['link', 'image', 'video'],
|
||||
],
|
||||
handlers: { image: function() { selectLocalImage(); }, video: function() { selectLocalVideo(); } }
|
||||
},
|
||||
@@ -248,56 +328,88 @@ function setupControlBox(mode) {
|
||||
if (mode === 'edit') {
|
||||
categoryBox.setAttribute('onclick', 'openPopup(this)');
|
||||
hashtagBox.setAttribute('onclick', 'openPopup(this)');
|
||||
categoryBox.innerText = '카테고리 설정';
|
||||
hashtagBox.innerText = '해시태그 편집';
|
||||
fetchCategoriesAndHashtags();
|
||||
|
||||
// === 수정된 부분 ===
|
||||
// 기존 "innerText" 설정 줄을 삭제하고,
|
||||
// 페이지 로드 시 현재 데이터로 UI를 업데이트하는 함수를 호출합니다.
|
||||
updateControlBoxDisplay();
|
||||
// ==================
|
||||
|
||||
fetchCategoriesAndHashtags(); // 팝업 목록 채우는 로직은 그대로 실행
|
||||
} else {
|
||||
// (읽기 모드 'else' 블록 수정)
|
||||
categoryBox.removeAttribute('onclick');
|
||||
hashtagBox.removeAttribute('onclick');
|
||||
categoryBox.classList.remove('btn-example');
|
||||
hashtagBox.classList.remove('btn-example');
|
||||
|
||||
categoryBox.innerHTML = `<span class="tag-title">카테고리: </span><span class="tag-item">${baseData.category || '지정되지 않음'}</span>`;
|
||||
// [수정] 카테고리를 새 구조(제목 + 래퍼)로 변경
|
||||
const categoryContent = `<span class="tag-item">${baseData.category || '지정되지 않음'}</span>`;
|
||||
categoryBox.innerHTML = `<span class="tag-title">CATEGORY</span><div class="tag-content-wrapper">${categoryContent}</div>`;
|
||||
|
||||
hashtagBox.innerHTML = '<span class="tag-title">태그: </span>';
|
||||
// [수정] 해시태그를 새 구조(제목 + 래퍼)로 변경
|
||||
let hashtagContent = '';
|
||||
if (baseData.tags && baseData.tags.length > 0) {
|
||||
baseData.tags.split(',').forEach(tag => {
|
||||
hashtagBox.innerHTML += `<span class="tag-item">#${tag.trim()}</span>`;
|
||||
});
|
||||
hashtagContent = baseData.tags.split(',').map(tag => {
|
||||
return `<span class="tag-item">#${tag.trim()}</span>`;
|
||||
}).join(' '); // join으로 하나의 문자열로 만듭니다.
|
||||
} else {
|
||||
hashtagBox.innerHTML += '<span>없음</span>';
|
||||
hashtagContent = '<span>없음</span>';
|
||||
}
|
||||
hashtagBox.innerHTML = `<span class="tag-title">HASHTAGS</span><div class="tag-content-wrapper">${hashtagContent}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 백엔드 API를 호출하여 카테고리와 해시태그 목록을 가져와 팝업을 채웁니다.
|
||||
*/
|
||||
/**
|
||||
* 백엔드 API를 호출하여 카테고리와 해시태그 목록을 가져와 팝업을 채웁니다.
|
||||
* (수정됨: 클릭 시 baseData 대신 Staging 변수를 업데이트하도록 변경)
|
||||
*/
|
||||
function fetchCategoriesAndHashtags() {
|
||||
// Fetch Categories
|
||||
fetch(`${getMainPath()}/blog/categories.bjx`).then(res => res.json()).then(data => {
|
||||
if (data.resultCode === 0 && data.tags) {
|
||||
const list = document.querySelector('#category-list');
|
||||
if(list) {
|
||||
if (list) {
|
||||
list.innerHTML = '';
|
||||
data.tags.forEach(tag => {
|
||||
const el = document.createElement('span');
|
||||
el.className = 'tag-item';
|
||||
el.innerText = tag;
|
||||
|
||||
// (로직 변경) 클릭 시
|
||||
el.onclick = function() {
|
||||
stagedCategory = tag; // 1. 임시 변수(stagedCategory) 업데이트
|
||||
renderStagedCategory(); // 2. 스테이징 UI만 새로고침 (팝업 안 닫음)
|
||||
};
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch(err => console.error('Error fetching categories:', err));
|
||||
|
||||
// Fetch Hashtags
|
||||
fetch(`${getMainPath()}/blog/hashtags.bjx`).then(res => res.json()).then(data => {
|
||||
if (data.resultCode === 0 && data.tags) {
|
||||
const list = document.querySelector('#hashtag-list');
|
||||
if(list) {
|
||||
if (list) {
|
||||
list.innerHTML = '';
|
||||
data.tags.forEach(tag => {
|
||||
const rawTag = tag;
|
||||
const el = document.createElement('span');
|
||||
el.className = 'tag-item';
|
||||
el.innerText = `#${tag}`;
|
||||
el.innerText = `#${rawTag}`;
|
||||
|
||||
// (로직 변경) 클릭 시
|
||||
el.onclick = function() {
|
||||
// 1. 임시 배열(stagedHashtags)에 추가 (중복 방지 헬퍼 사용)
|
||||
if (addTagToStaged(rawTag)) {
|
||||
// 2. 추가 성공 시에만 스테이징 UI 새로고침
|
||||
renderStagedHashtags();
|
||||
}
|
||||
};
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
@@ -333,18 +445,66 @@ function loadEditor() {
|
||||
*/
|
||||
function save() {
|
||||
const titleField = document.getElementById('title_field');
|
||||
|
||||
// --- (수정/신규 로직) ---
|
||||
// 1. baseData의 복사본을 만들어 전송용 임시 객체(dataToSend)를 생성합니다.
|
||||
// (원본 baseData를 직접 수정하면 팝업 UI가 인코딩된 문자로 깨집니다)
|
||||
let dataToSend = JSON.parse(JSON.stringify(baseData));
|
||||
|
||||
// 2. dataToSend 객체의 모든 텍스트 필드를 encodeURIComponent로 인코딩합니다.
|
||||
if (titleField) {
|
||||
baseData.title = encodeURIComponent(titleField.value);
|
||||
dataToSend.title = encodeURIComponent(titleField.value);
|
||||
} else {
|
||||
dataToSend.title = encodeURIComponent(dataToSend.title || '');
|
||||
}
|
||||
|
||||
baseData.content = encodeURIComponent(JSON.stringify(quill.getContents()));
|
||||
baseData.modifyLat = currentLat;
|
||||
baseData.modifyLon = currentLon;
|
||||
dataToSend.content = encodeURIComponent(JSON.stringify(quill.getContents()));
|
||||
// (누락되었던 필드 추가)
|
||||
dataToSend.category = encodeURIComponent(dataToSend.category || 'none');
|
||||
dataToSend.tags = encodeURIComponent(dataToSend.tags || '');
|
||||
|
||||
// 3. 좌표 데이터를 임시 객체에 업데이트합니다.
|
||||
dataToSend.modifyLat = currentLat;
|
||||
dataToSend.modifyLon = currentLon;
|
||||
|
||||
// (신규 게시물일 경우 원본 위치 좌표 설정)
|
||||
if (dataToSend.firstPostLat === 0.0 || dataToSend.firstPostLat === null) {
|
||||
dataToSend.firstPostLat = currentLat;
|
||||
}
|
||||
if (dataToSend.firstPostLon === 0.0 || dataToSend.firstPostLon === null) {
|
||||
dataToSend.firstPostLon = currentLon;
|
||||
}
|
||||
|
||||
const uploadUrl = `${getMainPath()}/blog/post.bjx`;
|
||||
if (confirm("해당 내용으로 저장하시겠습니까?")) {
|
||||
post(uploadUrl, serverData.enc, JSON.stringify(baseData), serverData.keyword, function(resultData) {
|
||||
alert("저장되었습니다.");
|
||||
console.log("Data being sent to server:", dataToSend);
|
||||
console.log("JSON string being sent:", JSON.stringify(dataToSend));
|
||||
post(uploadUrl, serverData.enc, JSON.stringify(dataToSend), serverData.keyword, function(resultData) {
|
||||
// --- (수정된 콜백 로직) ---
|
||||
try {
|
||||
// 1. 서버로부터 받은 JSON 문자열을 객체로 파싱합니다.
|
||||
const response = JSON.parse(resultData);
|
||||
|
||||
// 2. 서버 응답이 성공(resultCode === 0)이고,
|
||||
// 서버가 postId를 (예: response.data.postId) 보내줬는지 확인합니다.
|
||||
// (참고: 'response.data.postId'는 서버 응답 구조에 따라 변경해야 할 수 있습니다.)
|
||||
if (response.resultCode === 0 && response.data && response.data.postId) {
|
||||
|
||||
// 3. 알림 후, 응답받은 ID를 사용해 뷰어 페이지로 리디렉션합니다.
|
||||
alert("저장되었습니다. 게시물 보기 페이지로 이동합니다.");
|
||||
location.href = getMainPath() + "/blog/viewer/" + response.data.postId;
|
||||
|
||||
} else {
|
||||
// 저장은 성공했으나 ID를 받지 못한 경우 (또는 서버가 다른 에러 코드를 보낸 경우)
|
||||
alert("저장되었으나 페이지 이동에 실패했습니다: " + (response.resultMsg || "Unknown error"));
|
||||
}
|
||||
} catch (e) {
|
||||
// JSON 파싱 실패 등 예외 처리
|
||||
console.error("Failed to parse save response:", e, resultData);
|
||||
alert("저장에 성공했으나 서버 응답을 처리할 수 없습니다.");
|
||||
}
|
||||
// --- (여기까지 수정된 로직) ---
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -353,7 +513,32 @@ function save() {
|
||||
* 사용자의 현재 위치(위도, 경도)를 가져옵니다.
|
||||
*/
|
||||
function getLocation() {
|
||||
if (navigator.geolocation) {
|
||||
if (baseData.firstPostLat !== 0.0 || baseData.firstPostLon !== 0.0) {
|
||||
try {
|
||||
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) {
|
||||
const locationField = document.getElementById('location_field');
|
||||
try {
|
||||
var inh = `<span class="tag-title">LOCATION</span>`;
|
||||
inh = inh + `<div class="tag-content-wrapper">`;
|
||||
try{
|
||||
inh = inh + `<div class="tag-item">${result.features[0].properties.formatted}</div>`;
|
||||
}catch(err) {}
|
||||
inh = inh + `<div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div>`;
|
||||
inh = inh + `<div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
||||
locationField.innerHTML = inh;
|
||||
} catch (e) {
|
||||
locationField.innerHTML = `<span class="tag-title">LOCATION</span>` +
|
||||
`<div class="tag-content-wrapper"><div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div><div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
||||
}
|
||||
})
|
||||
.catch(error => console.log('error', error));
|
||||
}catch (e) { }
|
||||
} else if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(pos => {
|
||||
currentLat = pos.coords.latitude;
|
||||
currentLon = pos.coords.longitude;
|
||||
@@ -363,9 +548,18 @@ function getLocation() {
|
||||
baseData.modifyLon = currentLon;
|
||||
const locationField = document.getElementById('location_field');
|
||||
if (locationField) {
|
||||
locationField.textContent = `Lat: ${currentLat.toFixed(4)}, Lon: ${currentLon.toFixed(4)}`;
|
||||
// [수정] 제목과 내용 래퍼 구조로 변경
|
||||
locationField.innerHTML = `<span class="tag-title">LOCATION</span>` +
|
||||
`<div class="tag-content-wrapper"><div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div><div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const locationField = document.getElementById('location_field');
|
||||
if (locationField) {
|
||||
// [수정] 제목과 내용 래퍼 구조로 변경
|
||||
locationField.innerHTML = `<span class="tag-title">LOCATION</span>` +
|
||||
`<div class="tag-content-wrapper"><div class="tag-item">Lat: ${baseData.firstPostLat.toFixed(2)}</div><div class="tag-item">Lon: ${baseData.firstPostLon.toFixed(2)}</div></div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,10 +567,29 @@ function getLocation() {
|
||||
* 팝업 레이어를 엽니다.
|
||||
*/
|
||||
function openPopup(element) {
|
||||
|
||||
const targetId = element.getAttribute('to');
|
||||
const popup = document.querySelector(targetId);
|
||||
const overlay = document.querySelector('.dim_layer');
|
||||
|
||||
if (popup && overlay) {
|
||||
|
||||
// === (신규) Staging 변수 초기화 로직 ===
|
||||
if (targetId === '#popLayer1') { // 카테고리 팝업
|
||||
// 1. 실제 데이터(baseData)에서 임시 변수(stagedCategory)로 값을 복사
|
||||
stagedCategory = baseData.category || 'none';
|
||||
// 2. 임시 변수 기준으로 스테이징 UI 렌더링
|
||||
renderStagedCategory();
|
||||
}
|
||||
else if (targetId === '#popLayer2') { // 해시태그 팝업
|
||||
// 1. 실제 데이터(baseData)에서 임시 배열(stagedHashtags)로 값을 복사
|
||||
// (문자열을 배열로 변환하고, 빈 문자열 필터링)
|
||||
stagedHashtags = baseData.tags ? baseData.tags.split(',').filter(t => t.trim() !== '') : [];
|
||||
// 2. 임시 배열 기준으로 스테이징 UI 렌더링
|
||||
renderStagedHashtags();
|
||||
}
|
||||
// ===================================
|
||||
|
||||
overlay.style.display = 'block';
|
||||
popup.style.display = 'block';
|
||||
}
|
||||
@@ -461,7 +674,7 @@ function submitLoginForm() {
|
||||
function gotoHome() { document.location.replace(`${getMainPath()}/home.bs`); }
|
||||
function gotoWrite() { document.location.replace(`${getMainPath()}/blog/edit`); } // 수정된 URL
|
||||
function gotoModify() { document.location.replace(`${getMainPath()}/blog/posts`); } // 수정된 URL
|
||||
function gotoLogin() { document.location.replace(`${getMainPath()}/login.bs`); }
|
||||
function gotoWhere() { document.location.replace(`${getMainPath()}/bums/where.bs`); }
|
||||
function gotoJoin() { document.location.replace(`${getMainPath()}/user/join.bs`); }
|
||||
|
||||
/**
|
||||
@@ -543,4 +756,172 @@ function unformat(type, data, key) {
|
||||
default:
|
||||
return [odd.reverse().join(""), dividerStr, even.reverse().join("")].join("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (신규 추가)
|
||||
* 중복을 방지하며 baseData.tags (문자열)에 새 태그를 안전하게 추가합니다.
|
||||
*/
|
||||
function addTagToData(newTag) {
|
||||
if (!newTag || newTag.trim() === '') return; // 빈 태그 방지
|
||||
|
||||
// 현재 태그 문자열을 배열로 변환 (태그가 없으면 빈 배열)
|
||||
let tags = baseData.tags ? baseData.tags.split(',') : [];
|
||||
|
||||
// 새 태그가 이미 존재하는지 확인 (공백 제거 및 대소문자 무시)
|
||||
const tagExists = tags.some(t => t.trim().toLowerCase() === newTag.trim().toLowerCase());
|
||||
|
||||
if (!tagExists) {
|
||||
tags.push(newTag.trim()); // 새 태그 추가
|
||||
baseData.tags = tags.join(','); // 다시 쉼표로 구분된 문자열로 저장
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (신규 추가)
|
||||
* 편집기 컨트롤 박스의 텍스트를 현재 baseData 기준으로 새로 고칩니다.
|
||||
*/
|
||||
function updateControlBoxDisplay() {
|
||||
const categoryBox = document.querySelector('.controlbox-category');
|
||||
const hashtagBox = document.querySelector('.controlbox-hashtag');
|
||||
|
||||
if (categoryBox) {
|
||||
// [수정] 편집 모드도 읽기 모드와 동일한 HTML 구조(제목 + 래퍼)로 생성
|
||||
const categoryContent = (baseData.category && baseData.category !== 'none')
|
||||
? `<span class="tag-item">${baseData.category}</span>`
|
||||
: '<i>카테고리 설정</i>';
|
||||
categoryBox.innerHTML = `<span class="tag-title">CATEGORY</span><div class="tag-content-wrapper">${categoryContent}</div>`;
|
||||
}
|
||||
|
||||
if (hashtagBox) {
|
||||
// [수정] 편집 모드도 읽기 모드와 동일한 HTML 구조(제목 + 래퍼)로 생성
|
||||
let hashtagContent = '';
|
||||
if (baseData.tags && baseData.tags.length > 0) {
|
||||
hashtagContent = baseData.tags.split(',')
|
||||
.map(t => `<span class="tag-item">#${t.trim()}</span>`)
|
||||
.join(' '); // 각 태그를 span으로 감싸고 공백으로 연결
|
||||
} else {
|
||||
hashtagContent = '<i>해시태그 편집</i>';
|
||||
}
|
||||
hashtagBox.innerHTML = `<span class="tag-title">HASHTAGS</span><div class="tag-content-wrapper">${hashtagContent}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/* === (신규 추가) POPUP STAGING 헬퍼 함수들 === */
|
||||
|
||||
/** 1. Staging Category 렌더링: 선택된 카테고리(임시 변수)를 팝업 UI에 표시 */
|
||||
function renderStagedCategory() {
|
||||
const area = document.getElementById('selected-category-area');
|
||||
if (area) {
|
||||
if (stagedCategory && stagedCategory !== 'none') {
|
||||
// 선택된 아이템에 삭제(X) 버튼을 포함하여 렌더링
|
||||
area.innerHTML = `<span class="tag-item">${stagedCategory}
|
||||
<span class="remove-tag" onclick="removeStagedCategory()">X</span>
|
||||
</span>`;
|
||||
} else {
|
||||
area.innerHTML = '<i>No category selected.</i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 2. Staging Hashtags 렌더링: 선택된 해시태그 목록(임시 배열)을 팝업 UI에 표시 */
|
||||
function renderStagedHashtags() {
|
||||
const area = document.getElementById('selected-hashtags-area');
|
||||
if (area) {
|
||||
area.innerHTML = ''; // 영역 초기화
|
||||
if (stagedHashtags.length > 0) {
|
||||
stagedHashtags.forEach((tag, index) => {
|
||||
// 각 아이템에 삭제(X) 버튼과 올바른 index를 전달하는 onclick 이벤트 추가
|
||||
area.innerHTML += `<span class="tag-item">#${tag}
|
||||
<span class="remove-tag" onclick="removeStagedHashtag(${index})">X</span>
|
||||
</span>`;
|
||||
});
|
||||
} else {
|
||||
area.innerHTML = '<i>No tags selected.</i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 3. Staged Category 삭제: (X) 버튼 클릭 시 호출 */
|
||||
function removeStagedCategory() {
|
||||
stagedCategory = 'none';
|
||||
renderStagedCategory(); // UI 새로고침
|
||||
}
|
||||
|
||||
/** 4. Staged Hashtag 삭제: (X) 버튼 클릭 시 호출 */
|
||||
function removeStagedHashtag(index) {
|
||||
stagedHashtags.splice(index, 1); // 배열에서 해당 인덱스의 아이템 1개 제거
|
||||
renderStagedHashtags(); // UI 새로고침
|
||||
}
|
||||
|
||||
/** 5. Staged Hashtag 추가 (중복 방지 헬퍼): 임시 배열에 태그 추가 */
|
||||
function addTagToStaged(newTag) {
|
||||
if (!newTag || newTag.trim() === '') return false; // 빈 값 방지
|
||||
|
||||
const tagToAdd = newTag.trim().replace(/#/g, ''); // # 제거 및 공백 제거
|
||||
// 임시 배열에 이미 존재하는지 확인 (대소문자 무시)
|
||||
const tagExists = stagedHashtags.some(t => t.toLowerCase() === tagToAdd.toLowerCase());
|
||||
|
||||
if (!tagExists) {
|
||||
stagedHashtags.push(tagToAdd); // 임시 배열에 추가
|
||||
return true;
|
||||
}
|
||||
return false; // 중복이면 false 반환
|
||||
}
|
||||
|
||||
function handleVote(buttonElement, voteType) {
|
||||
// 1. 가장 가까운 .vote-controls 컨테이너를 찾음
|
||||
const controls = buttonElement.closest('.vote-controls');
|
||||
|
||||
// 2. 컨테이너의 data-post-id 속성에서 postId를 가져옴
|
||||
const postId = controls.dataset.postId; // (data-post-id="...") 값을 읽음
|
||||
|
||||
// 3. 모든 버튼 비활성화 (중복 클릭 방지)
|
||||
controls.querySelectorAll('button').forEach(btn => btn.disabled = true);
|
||||
|
||||
// 4. 요청할 URL 생성
|
||||
let url = `${getMainPath()}/blog/post/${postId}/${voteType === 'like' ? 'like' : 'unlike'}.bjx`;
|
||||
|
||||
// 5. CSRF 토큰 및 헤더 준비 (기본 헤더)
|
||||
let headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// [수정] CSRF 메타 태그가 존재하는지 (즉, 사용자가 로그인했는지) 확인
|
||||
const csrfMeta = document.querySelector('meta[name="_csrf"]');
|
||||
if (csrfMeta) {
|
||||
const csrfToken = csrfMeta.getAttribute('content');
|
||||
if (csrfToken) {
|
||||
// 토큰이 존재할 경우에만 헤더에 추가
|
||||
headers['X-CSRF-TOKEN'] = csrfToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Fetch API를 사용하여 POST 요청 전송
|
||||
// (익명 사용자는 CSRF 헤더 없이 요청하고, 인증 사용자는 헤더와 함께 요청)
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// 7. 성공 시: UI 업데이트
|
||||
const likeSpan = controls.querySelector('.like-count');
|
||||
const unlikeSpan = controls.querySelector('.unlike-count');
|
||||
|
||||
if (likeSpan) likeSpan.innerText = data.voteCount;
|
||||
if (unlikeSpan) unlikeSpan.innerText = data.unlikeCount;
|
||||
})
|
||||
.catch(error => {
|
||||
// 8. 실패 시: 버튼 다시 활성화
|
||||
console.error('Error handling vote:', error);
|
||||
alert('투표 중 오류가 발생했습니다. 나중에 다시 시도해주세요.');
|
||||
controls.querySelectorAll('button').forEach(btn => btn.disabled = false);
|
||||
});
|
||||
}
|
||||
/* ============================================= */
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
|
||||
<th:block layout:fragment="head">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(true)});</script>
|
||||
</th:block>
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<head>
|
||||
<th:block id="head">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(true)});</script>
|
||||
</th:block>
|
||||
</head>
|
||||
|
||||
|
||||
<section class="wrapper style2" >
|
||||
<th:block sec:authorize="isAuthenticated()">
|
||||
<div class="container" >
|
||||
@@ -33,6 +33,14 @@
|
||||
<p th:if="${srcPost.writeTime != null and srcPost.writeTime > 0}"
|
||||
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 class="write_controllbox" style="margin-top: -1em; margin-bottom: 2em;">
|
||||
<div class="write_option btn-example controlbox-category" to="#popLayer1">
|
||||
</div>
|
||||
<div class="write_option btn-example controlbox-hashtag" to="#popLayer2" id="hashtag_field">
|
||||
</div>
|
||||
<div class="write_option btn-example controlbox-location" id="location_field"></div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</section>
|
||||
@@ -46,44 +54,52 @@
|
||||
<div class="container">
|
||||
<div id="editor"></div>
|
||||
|
||||
<div class="write_controllbox">
|
||||
<div class="write_option btn-example controlbox-category" to="#popLayer1">
|
||||
</div>
|
||||
<div style="width: 15px"></div>
|
||||
<div class="write_option btn-example controlbox-hashtag" to="#popLayer2" id="hashtag_field">
|
||||
</div>
|
||||
<div style="width: 15px"></div>
|
||||
<div class="write_option" id="location_field"></div>
|
||||
</div>
|
||||
|
||||
<button id="save" class="button fit" style="margin-top: 1em;" onclick="save()">저장하기</button>
|
||||
</div>
|
||||
</th:block>
|
||||
</section>
|
||||
</th:block>
|
||||
|
||||
<th:block layout:fragment="popup_layer">
|
||||
<div id="popLayer1" class="pop_layer category-popup">
|
||||
<div class="pop_container">
|
||||
<div class="pop_conts">
|
||||
<h2>Categories</h2>
|
||||
|
||||
<div class="tag-list-label">Selected:</div>
|
||||
<div id="selected-category-area" class="staging-area">
|
||||
</div>
|
||||
|
||||
<div class="tag-list-label">Pre-loaded Categories:</div>
|
||||
<div id="category-list" class="tag-list"></div>
|
||||
|
||||
<input type="text" id="category-input" placeholder="Add a new category" class="tag-input">
|
||||
<button id="add-category-btn" class="button">Add</button>
|
||||
|
||||
<div class="btn_r">
|
||||
<a href="#" id="apply-category-btn" class="button" style="margin-right: 0.5em;">Apply</a>
|
||||
<a href="#" class="btn_layerClose">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="popLayer2" class="pop_layer hashtag-popup">
|
||||
<div class="pop_container">
|
||||
<div class="pop_conts">
|
||||
<h2>Hashtags</h2>
|
||||
|
||||
<div class="tag-list-label">Selected:</div>
|
||||
<div id="selected-hashtags-area" class="staging-area tag-list">
|
||||
</div>
|
||||
|
||||
<div class="tag-list-label">Suggested Tags:</div>
|
||||
<div id="hashtag-list" class="tag-list"></div>
|
||||
|
||||
<input type="text" id="hashtag-input" placeholder="Add a new hashtag" class="tag-input">
|
||||
<button id="add-hashtag-btn" class="button">Add</button>
|
||||
|
||||
<div class="btn_r">
|
||||
<a href="#" id="apply-hashtag-btn" class="button" style="margin-right: 0.5em;">Apply</a>
|
||||
<a href="#" class="btn_layerClose">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/default_layout}">
|
||||
<th:block layout:fragment="head">
|
||||
|
||||
</th:block>
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<div id="main_layer">
|
||||
<th:block sec:authorize="isAnonymous()">
|
||||
<h1>권한이 없는 뎁쇼?!</h1>
|
||||
</th:block>
|
||||
<th:block sec:authorize="isAuthenticated()">
|
||||
<div class="post_layer" id="posts" th:each="posts, postsStat : ${chunkedPosts}">
|
||||
<div class="post_layer" th:class="${#strings.append(rowKey, postsStat.index)}" th:each="post, postStat : ${posts}">
|
||||
<div class="post_item"
|
||||
th:if="${post.id != null and post.id != ''}"
|
||||
onclick="goToEditor(this)"
|
||||
th:attr="data-post-id=${post.id}">
|
||||
<span id="postTitle" class="post_attr" th:text="${post.title}"></span>
|
||||
<div id="content" class="post_attr content" th:attr="data=${post.content}"></div>
|
||||
<span id="writeDate" class="post_attr" th:text="${#dates.format(post.writeTime, 'yyyy.MM.dd HH:mm:ss')}"></span>
|
||||
<span id="postId" class="post_attr" th:text="${post.id}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -9,22 +9,29 @@
|
||||
<th:block layout:fragment="head">
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-8 col-12-narrower">
|
||||
<div id="content">
|
||||
<div id="content_inner">
|
||||
<article>
|
||||
<section th:each="post : ${Posts}">
|
||||
<section th:each="post : ${postsPage.content}">
|
||||
<div class="box post" th:id="${post.id}">
|
||||
<a href="javascript:void(0);" th:onclick="'goToViewer(this.parentNode)'" class="image left">
|
||||
<img th:src="${post.thumb != null and not #strings.isEmpty(post.thumb)} ? @{${post.thumb}} : @{/images/pic01.jpg}" alt="Post Thumbnail" />
|
||||
</a>
|
||||
<div class="inner">
|
||||
<h3 th:text="${post.title != null and not #strings.isEmpty(post.title)} ? ${post.title} : 'untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm')} + ']'"></h3>
|
||||
<h3 style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span th:text="${post.title != null and not #strings.isEmpty(post.title)} ? ${post.title} : 'untitled[' + ${#temporals.format(T(java.time.Instant).ofEpochMilli(post.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm')} + ']'"></span>
|
||||
<span style="font-size: 0.75em; color: #888; font-weight: normal; white-space: nowrap; margin-left: 1em;">
|
||||
(읽음: <span th:text="${post.readCount}">0</span>)
|
||||
</span>
|
||||
</h3>
|
||||
<p style="font-size: 0.9em; color: #555; margin-bottom: 0.5em;" th:text="${'by ' + post.writer}"></p>
|
||||
|
||||
<p th:text="${#strings.abbreviate(post.html, 80)}" class="ellipsis"></p>
|
||||
|
||||
</div>
|
||||
|
||||
<footer sec:authorize="isAuthenticated()" style="text-align: right; margin-top: 1em;">
|
||||
@@ -33,11 +40,34 @@
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Page navigation" th:if="${postsPage.totalPages > 1}" style="text-align: center; margin-top: 2.5em; font-size: 0.9em;">
|
||||
<ul class="pagination" style="display: inline-block; padding-left: 0; list-style: none; border-radius: 5px; border: 1px solid #e0e0e0; overflow: hidden;">
|
||||
<li th:styleappend="${postsPage.isFirst()} ? 'opacity: 0.5; pointer-events: none;' : ''" style="display: inline; float: left;">
|
||||
<a th:href="${postsPage.isFirst()} ? '#' : @{/blog/posts(page=${postsPage.number - 1})}"
|
||||
class="button alt small" style="border-radius:0; margin:0; border-right: 1px solid #e0e0e0;">
|
||||
« Prev
|
||||
</a>
|
||||
</li>
|
||||
<li th:each="pageNum : ${#numbers.sequence(0, postsPage.totalPages - 1)}"
|
||||
style="display: inline; float: left; border-right: 1px solid #e0e0e0;">
|
||||
<a th:href="@{/blog/posts(page=${pageNum})}"
|
||||
th:text="${pageNum + 1}"
|
||||
th:class="${pageNum == postsPage.number} ? 'button small' : 'button alt small'"
|
||||
style="border-radius:0; margin:0;">
|
||||
</a>
|
||||
</li>
|
||||
<li th:styleappend="${postsPage.isLast()} ? 'opacity: 0.5; pointer-events: none;'" style="display: inline; float: left;">
|
||||
<a th:href="${postsPage.isLast()} ? '#' : @{/blog/posts(page=${postsPage.number + 1})}"
|
||||
class="button alt small" style="border-radius:0; margin:0;">
|
||||
Next »
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div> </div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,53 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
<head>
|
||||
<th:block layout:fragment="head">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(false)});</script>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
||||
layout:decorate="~{layout/default_layout}">
|
||||
|
||||
</th:block>
|
||||
</head>
|
||||
<th:block layout:fragment="head">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill-table-better@1/dist/quill-table-better.js"></script>
|
||||
<script>document.addEventListener('DOMContentLoaded', function() {initEditor(false)});</script>
|
||||
</th:block>
|
||||
|
||||
<body>
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<section class="wrapper style2">
|
||||
<div class="container" sec:authorize="isAuthenticated()" onclick="loadEditor()" style="cursor: pointer;" title="클릭하여 수정하기">
|
||||
<header class="major">
|
||||
<h2 id="title_layer" th:text="${srcPost.title}">게시물 제목이 여기에 표시됩니다</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>
|
||||
<h2 id="title_layer">
|
||||
<span th:text="${srcPost.title}">게시물 제목이 여기에 표시됩니다</span>
|
||||
<span style="font-size: 0.8em; color: #888; font-weight: normal; margin-left: 0.5em;">
|
||||
(읽음: <span th:text="${srcPost.readCount}">0</span>)
|
||||
</span>
|
||||
</h2>
|
||||
<p>
|
||||
<span th:if="${srcPost.writer != null}" th:text="${'by ' + srcPost.writer + ' | '}" style="font-weight: 600;"></span>
|
||||
<span th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="write_controllbox" style="margin-top: -1em; margin-bottom: 2em;">
|
||||
<div class="write_option controlbox-category">
|
||||
</div>
|
||||
<div class="write_option controlbox-hashtag" id="hashtag_field">
|
||||
</div>
|
||||
<div class="write_option controlbox-location" id="location_field">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container open-login-popup" sec:authorize="isAnonymous()" to="#loginPopup" style="cursor: pointer;">
|
||||
<header class="major">
|
||||
<h2 id="title_layer" th:text="${srcPost.title}">게시물 제목이 여기에 표시됩니다</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>
|
||||
<h2 id="title_layer_anon">
|
||||
<span th:text="${srcPost.title}">게시물 제목이 여기에 표시됩니다</span>
|
||||
<span style="font-size: 0.8em; color: #888; font-weight: normal; margin-left: 0.5em;">
|
||||
(읽음: <span th:text="${srcPost.readCount}">0</span>)
|
||||
</span>
|
||||
</h2>
|
||||
<p>
|
||||
<span th:if="${srcPost.writer != null}" th:text="${'by ' + srcPost.writer + ' | '}" style="font-weight: 600;"></span>
|
||||
<span th:text="${#temporals.format(T(java.time.Instant).ofEpochMilli(srcPost.writeTime).atZone(T(java.time.ZoneId).systemDefault()).toLocalDateTime(), 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="write_controllbox" style="margin-top: -1em; margin-bottom: 2em;">
|
||||
<div class="write_option controlbox-category">
|
||||
</div>
|
||||
<div class="write_option controlbox-hashtag" id="hashtag_field_anon">
|
||||
</div>
|
||||
<div class="write_option controlbox-location" id="location_field_anon">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wrapper style1">
|
||||
<div class="container">
|
||||
<div id="content">
|
||||
<div id="content_inner">
|
||||
<article>
|
||||
<div id="editor"></div>
|
||||
|
||||
<div class="write_controllbox">
|
||||
<div class="write_option controlbox-category">
|
||||
</div>
|
||||
<div style="width: 15px"></div>
|
||||
<div class="write_option controlbox-hashtag" id="hashtag_field">
|
||||
</div>
|
||||
<div style="width: 15px"></div>
|
||||
<div class="write_option" id="location_field">
|
||||
</div>
|
||||
<div class="vote-controls" style="margin-top: 2em; text-align: center; border-top: 1px solid #e0e0e0; padding-top: 2em;" th:data-post-id="${srcPost.id}">
|
||||
<button class="button" onclick="handleVote(this, 'like')">
|
||||
👍 Like (<span class="like-count" th:text="${srcPost.voteCount}">0</span>)
|
||||
</button>
|
||||
<button class="button" onclick="handleVote(this, 'unlike')" style="margin-left: 1em;">
|
||||
👎 Unlike (<span class="unlike-count" th:text="${srcPost.unlikeCount}">0</span>)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 id="write" th:text="${srcPost.firstAddress}"></h3>
|
||||
@@ -67,5 +95,4 @@
|
||||
</div>
|
||||
</section>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,17 +10,57 @@
|
||||
<th:block layout:fragment="content" id="content">
|
||||
<div id="main_layer">
|
||||
<div class="layer">
|
||||
<th:block id="where" th:each="location : ${locations}">
|
||||
<div class="where_item" style="font-family: monospace; white-space: nowrap;">
|
||||
<span th:text="${location.timeString}"></span> |
|
||||
<span th:text="${location.mAddressLines}"></span> |
|
||||
<span th:text="${location.bettween}"></span> |
|
||||
<span th:text="${#numbers.formatDecimal(location.mLatitude, 1, 3)}"></span> |
|
||||
<span th:text="${#numbers.formatDecimal(location.mLongitude, 1, 3)}"></span> |
|
||||
<span th:text="${location.mCountryName}"></span>
|
||||
<th:block th:each="location : ${locationPage.content}">
|
||||
<div class="location-item"> <div class="location-header">
|
||||
<span class="location-time" th:text="${location.displayTime}"></span>
|
||||
<span class="location-between" th:text="${location.bettween}"></span>
|
||||
</div>
|
||||
<div class="location-body">
|
||||
<p class="location-address" th:text="${location.mAddressLines}"></p>
|
||||
<p class="location-coords">
|
||||
<span>위도: <b th:text="${#numbers.formatDecimal(location.mLatitude, 1, 3)}"></b></span>
|
||||
<span>경도: <b th:text="${#numbers.formatDecimal(location.mLongitude, 1, 3)}"></b></span>
|
||||
<span class="location-country" th:text="${'(' + location.mCountryName + ')'}"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<th:block th:with="currentPage=${locationPage.number},
|
||||
totalPages=${locationPage.totalPages}">
|
||||
|
||||
<a class="nav-link" th:if="${currentPage > 0}" th:href="@{/bums/where.bs(page=0)}"><< 처음</a>
|
||||
<span class="nav-link disabled" th:unless="${currentPage > 0}"><< 처음</span>
|
||||
|
||||
<a class="nav-link" th:if="${locationPage.hasPrevious()}" th:href="@{/bums/where.bs(page=${currentPage - 1})}">< 이전</a>
|
||||
<span class="nav-link disabled" th:unless="${locationPage.hasPrevious()}">< 이전</span>
|
||||
|
||||
<th:block th:with="startPage=${T(java.lang.Math).max(0, currentPage - 3)},
|
||||
endPage=${T(java.lang.Math).min(totalPages - 1, currentPage + 3)}">
|
||||
|
||||
<th:block th:each="pageNum : ${#numbers.sequence(startPage, endPage)}">
|
||||
<a class="page-num"
|
||||
th:if="${pageNum != currentPage}"
|
||||
th:href="@{/bums/where.bs(page=${pageNum})}"
|
||||
th:text="${pageNum + 1}">
|
||||
</a>
|
||||
<span class="page-num current-page"
|
||||
th:if="${pageNum == currentPage}"
|
||||
th:text="${pageNum + 1}">
|
||||
</span>
|
||||
</th:block>
|
||||
</th:block>
|
||||
<a class="nav-link" th:if="${locationPage.hasNext()}" th:href="@{/bums/where.bs(page=${currentPage + 1})}">다음 ></a>
|
||||
<span class="nav-link disabled" th:unless="${locationPage.hasNext()}">다음 ></span>
|
||||
|
||||
<a class="nav-link" th:if="${currentPage < totalPages - 1}" th:href="@{/bums/where.bs(page=${totalPages - 1})}">끝 >></a>
|
||||
<span class="nav-link disabled" th:unless="${currentPage < totalPages - 1}">끝 >></span>
|
||||
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
||||
@@ -17,16 +17,45 @@
|
||||
<meta name="_csrf" th:content="${_csrf.token}"/>
|
||||
<meta name="_csrf_parameter" th:content="${_csrf.parameterName}"/>
|
||||
<script th:inline="javascript">
|
||||
/*
|
||||
* [수정됨] 이 객체는 Post.kt 모델의 모든 필드를 포함해야 합니다.
|
||||
* 여기서 누락된 필드는 편집 후 저장 시 서버에서 null 또는 0으로 초기화됩니다.
|
||||
*/
|
||||
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]],
|
||||
firstPostLon: [[${srcPost != null and srcPost.firstPostLon != null} ? ${srcPost.firstPostLon} : 0]],
|
||||
writeTime: [[${srcPost != null and srcPost.writeTime != null} ? ${srcPost.writeTime} : 0]],
|
||||
enc: /*[[${enc != null} ? ${enc} : '']]*/,
|
||||
keyword: /*[[${keyword != null} ? ${keyword} : '']]*/
|
||||
// --- Key IDs ---
|
||||
id: [[${srcPost?.id}]], // (Thymeleaf 3.x의 안전 탐색 연산자 '?' 사용)
|
||||
originId: [[${srcPost?.originId}]],
|
||||
|
||||
// --- Core Content (컨트롤러에서 이미 디코딩됨) ---
|
||||
title: /*[[${srcPost?.title ?: ''}]]*/,
|
||||
content: /*[[${srcPost?.content ?: ''}]]*/,
|
||||
|
||||
// === (수정) 치명적으로 누락되었던 필드들 ===
|
||||
category: /*[[${srcPost?.category ?: 'none'}]]*/,
|
||||
tags: /*[[${srcPost?.tags ?: ''}]]*/,
|
||||
|
||||
// --- Timestamps (데이터 보존을 위해 필수) ---
|
||||
writeTime: [[${srcPost?.writeTime ?: 0}]],
|
||||
modifyTime: [[${srcPost?.modifyTime ?: 0}]],
|
||||
|
||||
// --- Location Data (데이터 보존을 위해 필수) ---
|
||||
firstPostLat: [[${srcPost?.firstPostLat ?: 0.0}]],
|
||||
firstPostLon: [[${srcPost?.firstPostLon ?: 0.0}]],
|
||||
firstAddress: /*[[${srcPost?.firstAddress ?: ''}]]*/,
|
||||
modifyLat: [[${srcPost?.modifyLat ?: 0.0}]],
|
||||
modifyLon: [[${srcPost?.modifyLon ?: 0.0}]],
|
||||
modifyAddress: /*[[${srcPost?.modifyAddress ?: ''}]]*/,
|
||||
|
||||
// --- Metadata (데이터 보존을 위해 필수) ---
|
||||
writer: /*[[${srcPost?.writer ?: ''}]]*/,
|
||||
posting: [[${srcPost?.posting ?: false}]],
|
||||
readCount: [[${srcPost?.readCount ?: 0}]],
|
||||
voteCount: [[${srcPost?.voteCount ?: 0}]],
|
||||
unlikeCount: [[${srcPost?.unlikeCount ?: 0}]],
|
||||
|
||||
// --- Page-specific (모델 데이터 아님) ---
|
||||
enc: /*[[${enc ?: ''}]]*/,
|
||||
keyword: /*[[${keyword ?: ''}]]*/
|
||||
};
|
||||
</script>
|
||||
</th:block>
|
||||
|
||||
Reference in New Issue
Block a user