This commit is contained in:
2025-09-15 17:18:44 +09:00
parent cc43ea8e0a
commit 1ab12cb6d9
29 changed files with 4951 additions and 2912 deletions
-551
View File
@@ -1,551 +0,0 @@
/*
* MODIFIED COMMON.CSS
* This file is refactored to inherit styles and variables from main.css.
* It styles custom components (popups, editor, etc.) to match the Arcana theme.
*/
/* * Removed conflicting global 'html, body' styles.
* The site will now correctly use the font and background from main.css.
*/
/*
* --- POPUP AND DIM LAYER STYLES ---
* Reworked to use variables and styles from main.css for a consistent look.
*/
.pop_layer {
display: none;
position: fixed; /* Use fixed for proper viewport centering */
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Modern centering method */
width: 450px;
max-width: 90%; /* Ensure it's responsive on small screens */
height: auto;
background-color: var(--pure-white, #fff); /* Use theme's white variable */
border: 1px solid #e0e0e0; /* Use theme's standard border color */
box-shadow: 0 0 15px rgba(0, 0, 0, 0.15); /* Add a subtle shadow */
z-index: 1001; /* Ensure it's above the dim layer */
border-radius: 5px; /* Use theme's standard border-radius */
}
.pop_layer .pop_container {
padding: 2em;
}
.pop_layer .pop_conts h2 {
font-size: 1.75em; /* Match theme's h2 style */
margin-bottom: 1em;
text-align: center;
}
.pop_layer p.ctxt {
color: var(--font-color_default, #474747); /* Use theme's text color */
line-height: 1.65em;
}
.pop_layer .btn_r {
width: 100%;
margin-top: 1.5em;
padding-top: 1em;
border-top: 1px solid #e0e0e0; /* Use theme's divider color */
text-align: right;
}
/* Style the close button to match the theme's alternate button style */
a.btn_layerClose {
display: inline-block;
padding: 0 1.5em;
line-height: 2.75em;
text-decoration: none;
font-weight: 600;
border-radius: 5px;
cursor: pointer;
background-color: var(--button-alt-default, #555);
color: var(--pure-white, #fff);
transition: background-color 0.2s ease-in-out;
}
a.btn_layerClose:hover {
background-color: var(--button-alt-hover, #626262);
color: var(--pure-white, #fff) !important; /* Ensure hover color override */
}
.dim_layer {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.4); /* Slightly softer dim */
}
/* * --- LOGIN FORM STYLES ---
* Adapted to use the default form input styles from main.css.
*/
#loginFormElement input[type="text"],
#loginFormElement input[type="password"] {
margin-bottom: 1em; /* Add spacing between fields */
}
#loginFormElement button {
margin-top: 1em;
width: 100%;
}
/* Custom Checkbox Styling */
#loginFormElement span {
vertical-align: middle;
margin-left: 0.5em;
}
#rememberMe {
vertical-align: middle;
width: 22px;
height: 22px;
appearance: none;
-webkit-appearance: none;
border: 1px solid #e0e0e0;
border-radius: 5px;
background-color: var(--pure-white, #fff);
cursor: pointer;
position: relative;
top: -2px;
}
#rememberMe:checked {
background-color: var(--point-color, #FFA500);
border-color: var(--point-color, #FFA500);
}
#rememberMe:checked::after {
content: '';
position: absolute;
top: 2px;
left: 7px;
width: 6px;
height: 12px;
border: solid var(--pure-white, #fff);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* * --- BLOG POST & EDITOR STYLES ---
*/
/* Control box below the editor in viewer.html */
.write_controllbox {
margin-top: 2em;
padding: 0;
display: flex;
flex-direction: row; /* [수정] 아이템을 가로(row)로 정렬합니다. */
align-items: stretch; /* [수정] 모든 박스가 동일한 높이를 갖도록 stretch로 변경 */
list-style: none;
gap: 15px; /* Use gap for spacing */
}
.write_option {
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;
}
/* * --- QUILL EDITOR THEME OVERRIDE ---
* Modified to blend with the Arcana theme's light background.
*/
/* Define custom fonts for Quill to match the site */
.ql-font-source-sans-pro { font-family: 'Source Sans Pro', sans-serif; }
/* Add any other fonts you've whitelisted in common.js */
.ql-toolbar.ql-snow {
background: var(--almost-white, #f7f7f7); /* Use theme's light gray */
border: 1px solid #e0e0e0;
border-bottom: none; /* Connect toolbar to editor visually */
border-radius: 5px 5px 0 0;
padding: 12px 8px;
}
.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 {
font-family: 'Source Sans Pro', sans-serif;
font-size: 1rem; /* Base size */
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.
*/
.comment-section {
margin-top: 3em;
padding-top: 2em;
border-top: 1px solid #e0e0e0;
}
#comment-form-container {
display: flex;
flex-direction: column;
margin-bottom: 2em;
}
/* Inherits styles from main.css 'textarea' selector */
#comment-input {
min-height: 100px;
margin-bottom: 1em;
}
#comments-list .comment {
background: var(--almost-white, #f7f7f7);
border-radius: 5px;
padding: 1em 1.5em;
margin-bottom: 1em;
border: 1px solid #e0e0e0;
}
.comment-header {
font-weight: 600;
color: var(--font-color_default, #474747);
margin-bottom: 0.5em;
}
.comment-time {
font-size: 0.9em;
color: #999;
margin-left: 0.5em;
}
.comment-content {
line-height: 1.65em;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 컨트롤 박스 내부 공통 제목 스타일 */
.write_option .tag-title {
font-weight: 600;
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);
border: 1px solid #e0e0e0;
border-radius: 15px; /* 둥근 태그 모양 */
padding: 0.2em 0.8em;
margin-right: 0.5em;
font-size: 0.9em;
color: var(--font-color_default, #474747);
}
/* 읽기 모드에서는 커서 모양을 기본으로 변경 */
.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;
}
/* === 대댓글 기능 CSS (신규 추가) === */
/* 1. 대댓글 목록 컨테이너 (들여쓰기) */
.reply-list {
margin-left: 40px;
padding-left: 15px;
border-left: 2px solid #eee;
}
/* 2. 댓글 헤더 (작성자/날짜/답글 버튼) */
.comment-header {
border-bottom: 1px dotted #ddd;
padding-bottom: 5px;
margin-bottom: 10px;
/* Flexbox를 사용해 요소를 양쪽으로 정렬 */
display: flex;
justify-content: space-between;
align-items: center;
}
.comment-author-info {
font-size: 0.9em;
}
.comment-author-info strong {
margin-right: 8px;
}
.comment-author-info .comment-date {
font-size: 0.9em;
color: #888;
}
/* 3. 답글 달기 버튼 */
.btn-reply {
font-size: 0.8em;
padding: 3px 8px;
cursor: pointer;
border: 1px solid #ccc;
background: #f9f9f9;
border-radius: 4px;
color: #555;
}
.btn-reply:hover {
background: #eee;
}
/* 4. 답글 상태 표시줄 (숨겨진 UI) */
#reply-status-bar {
display: none; /* JS로 제어 */
background: #f0f8ff;
border: 1px solid #bde0ff;
padding: 8px 12px;
margin-bottom: 10px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
#reply-status-text {
font-size: 0.9em;
color: #333;
font-weight: 500;
}
#btn-cancel-reply {
background: none;
border: none;
color: #E63946; /* 빨간색 계열 */
cursor: pointer;
font-size: 0.85em;
font-weight: bold;
}
@media screen and (max-width: 480px) {
.vote-controls {
display: flex; /* Flexbox 컨테이너로 변경 */
gap: 1em; /* 버튼 사이 간격 (기존 margin-left 대체) */
}
.vote-controls .button {
width: auto; /* main.css의 width: 100% 덮어쓰기 */
display: inline-block; /* main.css의 display: block 덮어쓰기 */
flex: 1 1 0; /* 1:1 비율로 공간을 나눠 갖도록 설정 */
margin-left: 0 !important; /* 혹시 모를 인라인 스타일 제거 */
}
}
+651 -9
View File
@@ -3424,15 +3424,6 @@ button.small,
padding: 5px;
}
/*.login_form button {*/
/* width: 100%;*/
/* padding: 10px;*/
/* background-color: #4CAF50;*/
/* color: white;*/
/* border: none;*/
/* cursor: pointer;*/
/*}*/
.login_close {
position: absolute;
top: 10px;
@@ -3441,3 +3432,654 @@ button.small,
cursor: pointer;
}
/*
* --- POPUP AND DIM LAYER STYLES ---
* Reworked to use variables and styles from main.css for a consistent look.
*/
.pop_layer {
display: none;
position: fixed; /* Use fixed for proper viewport centering */
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Modern centering method */
width: 450px;
max-width: 90%; /* Ensure it's responsive on small screens */
height: auto;
background-color: var(--pure-white, #fff); /* Use theme's white variable */
border: 1px solid #e0e0e0; /* Use theme's standard border color */
box-shadow: 0 0 15px rgba(0, 0, 0, 0.15); /* Add a subtle shadow */
z-index: 1001; /* Ensure it's above the dim layer */
border-radius: 5px; /* Use theme's standard border-radius */
}
.pop_layer .pop_container {
padding: 2em;
}
.pop_layer .pop_conts h2 {
font-size: 1.75em; /* Match theme's h2 style */
margin-bottom: 1em;
text-align: center;
}
.pop_layer p.ctxt {
color: var(--font-color_default, #474747); /* Use theme's text color */
line-height: 1.65em;
}
.pop_layer .btn_r {
width: 100%;
margin-top: 1.5em;
padding-top: 1em;
border-top: 1px solid #e0e0e0; /* Use theme's divider color */
text-align: right;
}
/* Style the close button to match the theme's alternate button style */
a.btn_layerClose {
display: inline-block;
padding: 0 1.5em;
line-height: 2.75em;
text-decoration: none;
font-weight: 600;
border-radius: 5px;
cursor: pointer;
background-color: var(--button-alt-default, #555);
color: var(--pure-white, #fff);
transition: background-color 0.2s ease-in-out;
}
a.btn_layerClose:hover {
background-color: var(--button-alt-hover, #626262);
color: var(--pure-white, #fff) !important; /* Ensure hover color override */
}
.dim_layer {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.4); /* Slightly softer dim */
}
/* * --- LOGIN FORM STYLES ---
* Adapted to use the default form input styles from main.css.
*/
#loginFormElement input[type="text"],
#loginFormElement input[type="password"] {
margin-bottom: 1em; /* Add spacing between fields */
}
#loginFormElement button {
margin-top: 1em;
width: 100%;
}
/* Custom Checkbox Styling */
#loginFormElement span {
vertical-align: middle;
margin-left: 0.5em;
}
#rememberMe {
vertical-align: middle;
width: 22px;
height: 22px;
appearance: none;
-webkit-appearance: none;
border: 1px solid #e0e0e0;
border-radius: 5px;
background-color: var(--pure-white, #fff);
cursor: pointer;
position: relative;
top: -2px;
}
#rememberMe:checked {
background-color: var(--point-color, #FFA500);
border-color: var(--point-color, #FFA500);
}
#rememberMe:checked::after {
content: '';
position: absolute;
top: 2px;
left: 7px;
width: 6px;
height: 12px;
border: solid var(--pure-white, #fff);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* * --- BLOG POST & EDITOR STYLES ---
*/
/* Control box below the editor in viewer.html */
.write_controllbox {
margin-top: 2em;
padding: 0;
display: flex;
flex-direction: row; /* [수정] 아이템을 가로(row)로 정렬합니다. */
align-items: stretch; /* [수정] 모든 박스가 동일한 높이를 갖도록 stretch로 변경 */
list-style: none;
gap: 15px; /* Use gap for spacing */
}
.write_option {
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;
}
/* * --- QUILL EDITOR THEME OVERRIDE ---
* Modified to blend with the Arcana theme's light background.
*/
/* Define custom fonts for Quill to match the site */
.ql-font-source-sans-pro { font-family: 'Source Sans Pro', sans-serif; }
/* Add any other fonts you've whitelisted in common.js */
.ql-toolbar.ql-snow {
background: var(--almost-white, #f7f7f7); /* Use theme's light gray */
border: 1px solid #e0e0e0;
border-bottom: none; /* Connect toolbar to editor visually */
border-radius: 5px 5px 0 0;
padding: 12px 8px;
}
.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 {
font-family: 'Source Sans Pro', sans-serif;
font-size: 1rem; /* Base size */
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.
*/
.comment-section {
margin-top: 3em;
padding-top: 2em;
border-top: 1px solid #e0e0e0;
}
#comment-form-container {
display: flex;
flex-direction: column;
margin-bottom: 2em;
}
/* Inherits styles from main.css 'textarea' selector */
#comment-input {
min-height: 100px;
margin-bottom: 1em;
}
#comments-list .comment {
background: var(--almost-white, #f7f7f7);
border-radius: 5px;
padding: 1em 1.5em;
margin-bottom: 1em;
border: 1px solid #e0e0e0;
}
.comment-header {
font-weight: 600;
color: var(--font-color_default, #474747);
margin-bottom: 0.5em;
}
.comment-time {
font-size: 0.9em;
color: #999;
margin-left: 0.5em;
}
.comment-content {
line-height: 1.65em;
white-space: pre-wrap;
word-wrap: break-word;
}
/* 컨트롤 박스 내부 공통 제목 스타일 */
.write_option .tag-title {
font-weight: 600;
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);
border: 1px solid #e0e0e0;
border-radius: 15px; /* 둥근 태그 모양 */
padding: 0.2em 0.8em;
margin-right: 0.5em;
font-size: 0.9em;
color: var(--font-color_default, #474747);
}
/* 읽기 모드에서는 커서 모양을 기본으로 변경 */
.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;
}
/* === 대댓글 기능 CSS (신규 추가) === */
/* 1. 대댓글 목록 컨테이너 (들여쓰기) */
.reply-list {
margin-left: 40px;
padding-left: 15px;
border-left: 2px solid #eee;
}
/* 2. 댓글 헤더 (작성자/날짜/답글 버튼) */
.comment-header {
border-bottom: 1px dotted #ddd;
padding-bottom: 5px;
margin-bottom: 10px;
/* Flexbox를 사용해 요소를 양쪽으로 정렬 */
display: flex;
justify-content: space-between;
align-items: center;
}
.comment-author-info {
font-size: 0.9em;
}
.comment-author-info strong {
margin-right: 8px;
}
.comment-author-info .comment-date {
font-size: 0.9em;
color: #888;
}
/* 3. 답글 달기 버튼 */
.btn-reply {
font-size: 0.8em;
padding: 3px 8px;
cursor: pointer;
border: 1px solid #ccc;
background: #f9f9f9;
border-radius: 4px;
color: #555;
}
.btn-reply:hover {
background: #eee;
}
/* 4. 답글 상태 표시줄 (숨겨진 UI) */
#reply-status-bar {
display: none; /* JS로 제어 */
background: #f0f8ff;
border: 1px solid #bde0ff;
padding: 8px 12px;
margin-bottom: 10px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
#reply-status-text {
font-size: 0.9em;
color: #333;
font-weight: 500;
}
#btn-cancel-reply {
background: none;
border: none;
color: #E63946; /* 빨간색 계열 */
cursor: pointer;
font-size: 0.85em;
font-weight: bold;
}
@media screen and (max-width: 480px) {
.vote-controls {
display: flex; /* Flexbox 컨테이너로 변경 */
gap: 1em; /* 버튼 사이 간격 (기존 margin-left 대체) */
}
.vote-controls .button {
width: auto; /* main.css의 width: 100% 덮어쓰기 */
display: inline-block; /* main.css의 display: block 덮어쓰기 */
flex: 1 1 0; /* 1:1 비율로 공간을 나눠 갖도록 설정 */
margin-left: 0 !important; /* 혹시 모를 인라인 스타일 제거 */
}
}
/*private*/
/* 개별 위치 로그 아이템 스타일 */
.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; */
}
-128
View File
@@ -1,128 +0,0 @@
/* 기본 레이아웃 및 폰트 */
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; */
}
+67 -3
View File
@@ -52,8 +52,63 @@ var baseData = {
*/
window.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const action = urlParams.get('action');
if (action === 'login') {
const loginPopup = document.getElementById('loginPopup');
if (loginPopup) {
// openPopup 함수는 특정 버튼(element)을 필요로 하므로,
// 팝업 div 자체에 임시로 'to' 속성을 부여하여 재사용합니다.
loginPopup.setAttribute('to', '#loginPopup');
openPopup(loginPopup);
}
} else if (action === 'signup') {
const signupPopup = document.getElementById('signupPopup');
if (signupPopup) {
signupPopup.setAttribute('to', '#signupPopup');
openPopup(signupPopup);
}
}
const openSignupBtn = document.getElementById('openSignupBtnFromLogin');
if (openSignupBtn) {
openSignupBtn.addEventListener('click', () => {
// 1. 현재 열려있는 로그인 팝업을 닫습니다.
closePopup();
// 2. 회원가입 팝업을 찾아서 엽니다.
const signupPopup = document.getElementById('signupPopup');
if (signupPopup) {
// openPopup 함수를 재사용하기 위해 'to' 속성을 설정합니다.
signupPopup.setAttribute('to', '#signupPopup');
openPopup(signupPopup);
}
});
}
console.log("DOM Loaded: Attaching Vanilla JS event listeners.");
const logoutButton = document.querySelector('a[href="javascript:logout()"]');
const isLoggedIn = !!logoutButton; // true 또는 false
const commentForm = document.querySelector('.comment-form-wrapper');
if (commentForm) {
if (!isLoggedIn) {
// 비로그인 상태면, 입력 필드와 버튼을 비활성화합니다.
const commentInput = commentForm.querySelector('#comment-input');
const commentSubmitBtn = commentForm.querySelector('#submit-comment');
if(commentInput) {
commentInput.disabled = true;
commentInput.placeholder = '댓글을 작성하려면 로그인이 필요합니다.';
}
if(commentSubmitBtn) {
commentSubmitBtn.disabled = true;
}
}
}
// --- 1. 사이드바 목록 가져오기 (이 기능은 이미 바닐라 JS였습니다) ---
if (document.querySelector(".rank_of_view")) {
fetchRankOfViews();
@@ -107,8 +162,13 @@ window.addEventListener('DOMContentLoaded', () => {
const commentSubmitBtn = document.getElementById('submit-comment');
if (commentSubmitBtn) {
commentSubmitBtn.addEventListener('click', (e) => {
e.preventDefault(); // 기본 버튼 동작 방지
submitComment(); // 댓글 제출 함수 호출
e.preventDefault();
// isLoggedIn 변수를 사용하여 추가적인 클라이언트 측 방어
if (!isLoggedIn) {
alert('로그인이 필요합니다.');
return;
}
submitComment();
});
}
@@ -311,7 +371,9 @@ async function uploadImage(blob) {
const formData = new FormData();
formData.append('file', blob);
let uploadUrl = getMainPath() + "/blog/post/imageUpload.bjx";
let imageUrlBase = getMainPath() + '/blog/post/images/';
// let imageUrlBase = getMainPath() + '/blog/post/images/';
// [수정] 이미지 URL의 기본 경로를 새로운 API 경로로 변경합니다.
let imageUrlBase = getMainPath() + '/api/images/'; // '/blog/post/images/' -> '/api/images/'
try {
// CSRF 토큰을 <meta> 태그에서 직접 읽어옵니다. (includes.html에 정의되어 있음)
@@ -1331,3 +1393,5 @@ async function fetchRanks(gameType, contextId = null) {
}
return response.json();
}
-67
View File
@@ -1,67 +0,0 @@
/*
Arcana by HTML5 UP
html5up.net | @ajlkn
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/
(function($) {
var $window = $(window),
$body = $('body');
// Breakpoints.
breakpoints({
wide: [ '1281px', '1680px' ],
normal: [ '981px', '1280px' ],
narrow: [ '841px', '980px' ],
narrower: [ '737px', '840px' ],
mobile: [ '481px', '736px' ],
mobilep: [ null, '480px' ]
});
// Play initial animations on page load.
$window.on('load', function() {
window.setTimeout(function() {
$body.removeClass('is-preload');
}, 100);
});
// Dropdowns.
$('#nav > ul').dropotron({
offsetY: -15,
hoverDelay: 0,
alignment: 'center'
});
// Nav.
// Bar.
$(
'<div id="titleBar">' +
'<a href="#navPanel" class="toggle"></a>' +
'<span class="title" onclick="javascript:gotoHome()">' + $('#logo').html() + '</span>' +
'</div>'
)
.appendTo($body);
// Panel.
$(
'<div id="navPanel">' +
'<nav>' +
$('#nav').navList() +
'</nav>' +
'</div>'
)
.appendTo($body)
.panel({
delay: 500,
hideOnClick: true,
hideOnSwipe: true,
resetScroll: true,
resetForms: true,
side: 'left',
target: $body,
visibleClass: 'navPanel-visible'
});
})(jQuery);
File diff suppressed because it is too large Load Diff
+411
View File
@@ -0,0 +1,411 @@
/*
* Arcana by HTML5 UP (html5up.net | @ajlkn)
* Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*
* This file contains the theme's utility scripts (panel, polyfills, etc.)
*/
(function($) {
/**
* Converts a navigation menu (ul) into a flat list of links for use in a mobile panel.
* @return {jQuery} jQuery object
*/
$.fn.navList = function() {
var $this = $(this),
$a = $this.find('a'),
b = [];
$a.each(function() {
var $this = $(this),
indent = Math.max(0, $this.parents('li').length - 1),
href = $this.attr('href'),
target = $this.attr('target'),
// [Modified] Get the class and 'to' attribute of the original link to open the popup.
originalClass = $this.attr('class'),
toAttr = $this.attr('to');
// [Modified] Include both the default class and the original class for the mobile panel link.
var classes = 'link depth-' + indent;
if (typeof originalClass !== 'undefined' && originalClass != '') {
classes += ' ' + originalClass;
}
b.push(
'<a ' +
'class="' + classes + '"' + // Apply modified classes
( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
// [Modified] Add the 'to' attribute to specify the popup target.
( (typeof toAttr !== 'undefined' && toAttr != '') ? ' to="' + toAttr + '"' : '') +
'>' +
'<span class="indent-' + indent + '"></span>' +
$this.text() +
'</a>'
);
});
return b.join('');
};
/**
* Transforms a specific element into a slide-out panel.
* @param {object} userConfig User settings object
* @return {jQuery} jQuery object
*/
$.fn.panel = function(userConfig) {
// Return if no element
if (this.length == 0)
return $this;
// If multiple elements, recursively call for each
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i]).panel(userConfig);
return $this;
}
// Set variables
var $this = $(this),
$body = $('body'),
$window = $(window),
id = $this.attr('id'),
config;
// Merge default and user settings
config = $.extend({
delay: 0, // Delay time
hideOnClick: false, // Whether to hide the panel on link click
hideOnEscape: false, // Whether to hide the panel on ESC key press
hideOnSwipe: false, // Whether to hide the panel on swipe
resetScroll: false, // Whether to reset scroll on hide
resetForms: false, // Whether to reset forms on hide
side: null, // Which side the panel appears on (top, bottom, left, right)
target: $this, // The target to which the class is applied when the panel is visible
visibleClass: 'visible' // The class name to apply when the panel is visible
}, userConfig);
// Internal function to hide the panel
$this._hide = function(event) {
if (!config.target.hasClass(config.visibleClass))
return;
if (event) {
event.preventDefault();
event.stopPropagation();
}
config.target.removeClass(config.visibleClass);
window.setTimeout(function() {
if (config.resetScroll)
$this.scrollTop(0);
if (config.resetForms)
$this.find('form').each(function() {
this.reset();
});
}, config.delay);
};
// CSS settings for browser compatibility
$this
.css('-ms-overflow-style', '-ms-autohiding-scrollbar')
.css('-webkit-overflow-scrolling', 'touch');
// Event handler to hide the panel on link click
if (config.hideOnClick) {
$this.find('a')
.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
$this
.on('click', 'a', function(event) {
var $a = $(this),
href = $a.attr('href'),
target = $a.attr('target');
// --- ⬇️ Modified Logic Start ⬇️ ---
// 1. First check if it is a popup link.
// If the link has the 'open-login-popup' class, close the menu, and this handler's action ends here.
// (The action to open the popup is handled by another event handler in common.js.)
if ($a.hasClass('open-login-popup')) {
$this._hide();
return;
}
// 2. The existing logic for regular links (page navigation) remains unchanged.
if (!href || href == '#' || href == '' || href == '#' + id)
return;
// --- ⬆️ Modified Logic End ⬆️ ---
// Cancel original event.
event.preventDefault();
event.stopPropagation();
// Hide panel.
$this._hide();
// Redirect to href.
window.setTimeout(function() {
if (target == '_blank')
window.open(href);
else
window.location.href = href;
}, config.delay + 10);
});
}
// Touch and swipe event handlers
$this.on('touchstart', function(event) {
$this.touchPosX = event.originalEvent.touches[0].pageX;
$this.touchPosY = event.originalEvent.touches[0].pageY;
});
$this.on('touchmove', function(event) {
if ($this.touchPosX === null || $this.touchPosY === null) return;
var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
th = $this.outerHeight(),
ts = ($this.get(0).scrollHeight - $this.scrollTop());
if (config.hideOnSwipe) {
var result = false, boundary = 20, delta = 50;
switch (config.side) {
case 'left': result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); break;
case 'right': result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); break;
case 'top': result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); break;
case 'bottom': result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); break;
default: break;
}
if (result) {
$this.touchPosX = null;
$this.touchPosY = null;
$this._hide();
return false;
}
}
if (($this.scrollTop() < 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
event.preventDefault();
event.stopPropagation();
}
});
// Prevent events inside the panel from propagating upwards
$this.on('click touchend touchstart touchmove', function(event) {
event.stopPropagation();
});
// Hide panel on click of a link pointing to the panel ID
$this.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.removeClass(config.visibleClass);
});
// Hide panel on body click
$body.on('click touchend', function(event) {
$this._hide(event);
});
// Event handler for the link (toggle) that opens the panel
$body.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.toggleClass(config.visibleClass);
});
// Hide panel on ESC key press
if (config.hideOnEscape) {
$window.on('keydown', function(event) {
if (event.keyCode == 27) $this._hide(event);
});
}
return $this;
};
/**
* Polyfill for the 'placeholder' attribute of input elements in older browsers.
* @return {jQuery} jQuery object
*/
$.fn.placeholder = function() {
if (typeof (document.createElement('input')).placeholder != 'undefined')
return $(this);
if (this.length == 0) return $this;
if (this.length > 1) {
for (var i=0; i < this.length; i++) $(this[i]).placeholder();
return $this;
}
var $this = $(this);
$this.find('input[type=text],textarea').each(function() {
var i = $(this);
if (i.val() == '' || i.val() == i.attr('placeholder'))
i.addClass('polyfill-placeholder').val(i.attr('placeholder'));
}).on('blur', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/)) return;
if (i.val() == '')
i.addClass('polyfill-placeholder').val(i.attr('placeholder'));
}).on('focus', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/)) return;
if (i.val() == i.attr('placeholder'))
i.removeClass('polyfill-placeholder').val('');
});
$this.find('input[type=password]').each(function() {
var i = $(this);
var x = $($('<div>').append(i.clone()).remove().html().replace(/type="password"/i, 'type="text"').replace(/type=password/i, 'type=text'));
if (i.attr('id') != '') x.attr('id', i.attr('id') + '-polyfill-field');
if (i.attr('name') != '') x.attr('name', i.attr('name') + '-polyfill-field');
x.addClass('polyfill-placeholder').val(x.attr('placeholder')).insertAfter(i);
if (i.val() == '') i.hide(); else x.hide();
i.on('blur', function(event) {
event.preventDefault();
var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') { i.hide(); x.show(); }
});
x.on('focus', function(event) {
event.preventDefault();
var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
x.hide();
i.show().focus();
}).on('keypress', function(event) {
event.preventDefault();
x.val('');
});
});
$this.on('submit', function() {
$this.find('input[type=text],input[type=password],textarea').each(function(event) {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/)) i.attr('name', '');
if (i.val() == i.attr('placeholder')) {
i.removeClass('polyfill-placeholder');
i.val('');
}
});
}).on('reset', function(event) {
event.preventDefault();
$this.find('select').val($('option:first').val());
$this.find('input,textarea').each(function() {
var i = $(this), x;
i.removeClass('polyfill-placeholder');
switch (this.type) {
case 'submit': case 'reset': break;
case 'password':
i.val(i.attr('defaultValue'));
x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') { i.hide(); x.show(); } else { i.show(); x.hide(); }
break;
case 'checkbox': case 'radio': i.attr('checked', i.attr('defaultValue')); break;
case 'text': case 'textarea':
i.val(i.attr('defaultValue'));
if (i.val() == '') { i.addClass('polyfill-placeholder'); i.val(i.attr('placeholder')); }
break;
default: i.val(i.attr('defaultValue')); break;
}
});
});
return $this;
};
/**
* Moves elements to the front of their parent or back to their original position based on a condition.
* (Mainly used for changing element positions in responsive layouts.)
* @param {jQuery} $elements Elements to move
* @param {bool} condition If true, move to the front; if false, move to the original position
*/
$.prioritize = function($elements, condition) {
var key = '__prioritize';
if (typeof $elements != 'jQuery') $elements = $($elements);
$elements.each(function() {
var $e = $(this), $p, $parent = $e.parent();
if ($parent.length == 0) return;
if (!$e.data(key)) {
if (!condition) return;
$p = $e.prev();
if ($p.length == 0) return;
$e.prependTo($parent);
$e.data(key, $p);
} else {
if (condition) return;
$p = $e.data(key);
$e.insertAfter($p);
$e.removeData(key);
}
});
};
})(jQuery);
/*
Arcana by HTML5 UP
html5up.net | @ajlkn
Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/
(function($) {
var $window = $(window),
$body = $('body');
// Breakpoints.
breakpoints({
wide: [ '1281px', '1680px' ],
normal: [ '981px', '1280px' ],
narrow: [ '841px', '980px' ],
narrower: [ '737px', '840px' ],
mobile: [ '481px', '736px' ],
mobilep: [ null, '480px' ]
});
// Play initial animations on page load.
$window.on('load', function() {
window.setTimeout(function() {
$body.removeClass('is-preload');
}, 100);
});
// Dropdowns.
$('#nav > ul').dropotron({
offsetY: -15,
hoverDelay: 0,
alignment: 'center'
});
// Nav.
// Bar.
$(
'<div id="titleBar">' +
'<a href="#navPanel" class="toggle"></a>' +
'<span class="title" onclick="javascript:gotoHome()">' + $('#logo').html() + '</span>' +
'</div>'
)
.appendTo($body);
// Panel.
$(
'<div id="navPanel">' +
'<nav>' +
$('#nav').navList() +
'</nav>' +
'</div>'
)
.appendTo($body)
.panel({
delay: 500,
hideOnClick: true,
hideOnSwipe: true,
resetScroll: true,
resetForms: true,
side: 'left',
target: $body,
visibleClass: 'navPanel-visible'
});
})(jQuery);
-1
View File
@@ -1 +0,0 @@
File diff suppressed because one or more lines are too long
-342
View File
@@ -1,342 +0,0 @@
/*
* Arcana by HTML5 UP (html5up.net | @ajlkn)
* Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*
* 파일은 테마의 유틸리티 스크립트를 포함합니다. (패널, پلی필 )
*/
(function($) {
/**
* 네비게이션 메뉴(ul) 모바일 패널에서 사용할 있는 평면 링크 목록으로 변환합니다.
* @return {jQuery} jQuery 객체
*/
$.fn.navList = function() {
var $this = $(this),
$a = $this.find('a'),
b = [];
$a.each(function() {
var $this = $(this),
indent = Math.max(0, $this.parents('li').length - 1),
href = $this.attr('href'),
target = $this.attr('target'),
// [수정된 부분] 팝업을 열기 위해 원본 링크의 class와 'to' 속성을 가져옵니다.
originalClass = $this.attr('class'),
toAttr = $this.attr('to');
// [수정된 부분] 모바일 패널용 링크에 기본 클래스와 원본 클래스를 모두 포함시킵니다.
var classes = 'link depth-' + indent;
if (typeof originalClass !== 'undefined' && originalClass != '') {
classes += ' ' + originalClass;
}
b.push(
'<a ' +
'class="' + classes + '"' + // 수정된 클래스 적용
( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
// [수정된 부분] 팝업 대상을 지정하는 'to' 속성을 추가합니다.
( (typeof toAttr !== 'undefined' && toAttr != '') ? ' to="' + toAttr + '"' : '') +
'>' +
'<span class="indent-' + indent + '"></span>' +
$this.text() +
'</a>'
);
});
return b.join('');
};
/**
* 특정 요소를 슬라이드 아웃 패널로 변환합니다.
* @param {object} userConfig 사용자 설정 객체
* @return {jQuery} jQuery 객체
*/
$.fn.panel = function(userConfig) {
// 요소가 없으면 반환
if (this.length == 0)
return $this;
// 여러 요소에 적용될 경우, 각각에 대해 재귀적으로 호출
if (this.length > 1) {
for (var i=0; i < this.length; i++)
$(this[i]).panel(userConfig);
return $this;
}
// 변수 설정
var $this = $(this),
$body = $('body'),
$window = $(window),
id = $this.attr('id'),
config;
// 기본 설정과 사용자 설정을 병합
config = $.extend({
delay: 0, // 지연 시간
hideOnClick: false, // 링크 클릭 시 패널 숨김 여부
hideOnEscape: false, // ESC 키 누를 시 패널 숨김 여부
hideOnSwipe: false, // 스와이프 시 패널 숨김 여부
resetScroll: false, // 숨길 때 스크롤 리셋 여부
resetForms: false, // 숨길 때 폼 리셋 여부
side: null, // 패널이 나타날 위치 (top, bottom, left, right)
target: $this, // 패널이 보일 때 클래스가 적용될 대상
visibleClass: 'visible' // 패널이 보일 때 적용될 클래스 이름
}, userConfig);
// 패널 숨기기 내부 함수
$this._hide = function(event) {
if (!config.target.hasClass(config.visibleClass))
return;
if (event) {
event.preventDefault();
event.stopPropagation();
}
config.target.removeClass(config.visibleClass);
window.setTimeout(function() {
if (config.resetScroll)
$this.scrollTop(0);
if (config.resetForms)
$this.find('form').each(function() {
this.reset();
});
}, config.delay);
};
// 브라우저 호환성을 위한 CSS 설정
$this
.css('-ms-overflow-style', '-ms-autohiding-scrollbar')
.css('-webkit-overflow-scrolling', 'touch');
// 링크 클릭 시 패널 숨기기 이벤트 핸들러
if (config.hideOnClick) {
$this.find('a')
.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
$this
.on('click', 'a', function(event) {
var $a = $(this),
href = $a.attr('href'),
target = $a.attr('target');
// --- ⬇️ 수정된 로직 시작 ⬇️ ---
// 1. 팝업 링크인지 먼저 확인합니다.
// 링크에 'open-login-popup' 클래스가 있으면 메뉴를 닫고, 이 핸들러의 동작은 여기서 종료합니다.
// (팝업을 여는 동작은 common.js에 있는 다른 이벤트 핸들러가 처리합니다.)
if ($a.hasClass('open-login-popup')) {
$this._hide();
return;
}
// 2. 기존의 일반 링크(페이지 이동) 처리 로직은 그대로 둡니다.
if (!href || href == '#' || href == '' || href == '#' + id)
return;
// --- ⬆️ 수정된 로직 끝 ⬆️ ---
// Cancel original event.
event.preventDefault();
event.stopPropagation();
// Hide panel.
$this._hide();
// Redirect to href.
window.setTimeout(function() {
if (target == '_blank')
window.open(href);
else
window.location.href = href;
}, config.delay + 10);
});
}
// 터치 및 스와이프 이벤트 핸들러
$this.on('touchstart', function(event) {
$this.touchPosX = event.originalEvent.touches[0].pageX;
$this.touchPosY = event.originalEvent.touches[0].pageY;
});
$this.on('touchmove', function(event) {
if ($this.touchPosX === null || $this.touchPosY === null) return;
var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
th = $this.outerHeight(),
ts = ($this.get(0).scrollHeight - $this.scrollTop());
if (config.hideOnSwipe) {
var result = false, boundary = 20, delta = 50;
switch (config.side) {
case 'left': result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); break;
case 'right': result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); break;
case 'top': result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); break;
case 'bottom': result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); break;
default: break;
}
if (result) {
$this.touchPosX = null;
$this.touchPosY = null;
$this._hide();
return false;
}
}
if (($this.scrollTop() < 0 && diffY < 0) || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
event.preventDefault();
event.stopPropagation();
}
});
// 패널 내부에서 발생하는 이벤트가 상위로 전파되는 것을 방지
$this.on('click touchend touchstart touchmove', function(event) {
event.stopPropagation();
});
// 패널 ID를 가리키는 링크 클릭 시 패널 숨기기
$this.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.removeClass(config.visibleClass);
});
// body 클릭 시 패널 숨기기
$body.on('click touchend', function(event) {
$this._hide(event);
});
// 패널을 여는 링크(토글)에 대한 이벤트 핸들러
$body.on('click', 'a[href="#' + id + '"]', function(event) {
event.preventDefault();
event.stopPropagation();
config.target.toggleClass(config.visibleClass);
});
// ESC 키 누를 시 패널 숨기기
if (config.hideOnEscape) {
$window.on('keydown', function(event) {
if (event.keyCode == 27) $this._hide(event);
});
}
return $this;
};
/**
* 구형 브라우저에서 input의 'placeholder' 속성을 지원하기 위한 پلی필(Polyfill)입니다.
* @return {jQuery} jQuery 객체
*/
$.fn.placeholder = function() {
if (typeof (document.createElement('input')).placeholder != 'undefined')
return $(this);
if (this.length == 0) return $this;
if (this.length > 1) {
for (var i=0; i < this.length; i++) $(this[i]).placeholder();
return $this;
}
var $this = $(this);
$this.find('input[type=text],textarea').each(function() {
var i = $(this);
if (i.val() == '' || i.val() == i.attr('placeholder'))
i.addClass('polyfill-placeholder').val(i.attr('placeholder'));
}).on('blur', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/)) return;
if (i.val() == '')
i.addClass('polyfill-placeholder').val(i.attr('placeholder'));
}).on('focus', function() {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/)) return;
if (i.val() == i.attr('placeholder'))
i.removeClass('polyfill-placeholder').val('');
});
$this.find('input[type=password]').each(function() {
var i = $(this);
var x = $($('<div>').append(i.clone()).remove().html().replace(/type="password"/i, 'type="text"').replace(/type=password/i, 'type=text'));
if (i.attr('id') != '') x.attr('id', i.attr('id') + '-polyfill-field');
if (i.attr('name') != '') x.attr('name', i.attr('name') + '-polyfill-field');
x.addClass('polyfill-placeholder').val(x.attr('placeholder')).insertAfter(i);
if (i.val() == '') i.hide(); else x.hide();
i.on('blur', function(event) {
event.preventDefault();
var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') { i.hide(); x.show(); }
});
x.on('focus', function(event) {
event.preventDefault();
var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
x.hide();
i.show().focus();
}).on('keypress', function(event) {
event.preventDefault();
x.val('');
});
});
$this.on('submit', function() {
$this.find('input[type=text],input[type=password],textarea').each(function(event) {
var i = $(this);
if (i.attr('name').match(/-polyfill-field$/)) i.attr('name', '');
if (i.val() == i.attr('placeholder')) {
i.removeClass('polyfill-placeholder');
i.val('');
}
});
}).on('reset', function(event) {
event.preventDefault();
$this.find('select').val($('option:first').val());
$this.find('input,textarea').each(function() {
var i = $(this), x;
i.removeClass('polyfill-placeholder');
switch (this.type) {
case 'submit': case 'reset': break;
case 'password':
i.val(i.attr('defaultValue'));
x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
if (i.val() == '') { i.hide(); x.show(); } else { i.show(); x.hide(); }
break;
case 'checkbox': case 'radio': i.attr('checked', i.attr('defaultValue')); break;
case 'text': case 'textarea':
i.val(i.attr('defaultValue'));
if (i.val() == '') { i.addClass('polyfill-placeholder'); i.val(i.attr('placeholder')); }
break;
default: i.val(i.attr('defaultValue')); break;
}
});
});
return $this;
};
/**
* 특정 조건에 따라 요소의 순서를 부모 요소의 앞으로 이동시키거나 원래 위치로 되돌립니다.
* (주로 반응형 레이아웃에서 요소의 위치를 변경할 사용됩니다.)
* @param {jQuery} $elements 이동시킬 요소
* @param {bool} condition true이면 앞으로, false이면 원래 위치로 이동
*/
$.prioritize = function($elements, condition) {
var key = '__prioritize';
if (typeof $elements != 'jQuery') $elements = $($elements);
$elements.each(function() {
var $e = $(this), $p, $parent = $e.parent();
if ($parent.length == 0) return;
if (!$e.data(key)) {
if (!condition) return;
$p = $e.prev();
if ($p.length == 0) return;
$e.prependTo($parent);
$e.data(key, $p);
} else {
if (condition) return;
$p = $e.data(key);
$e.insertAfter($p);
$e.removeData(key);
}
});
};
})(jQuery);
File diff suppressed because it is too large Load Diff
@@ -23,13 +23,12 @@
</a>
<div class="inner">
<h3 style="display: flex; justify-content: space-between; align-items: center;">
<span>
<span th:if="${!post.posting}" style="background-color: #888; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 4px; margin-right: 8px; vertical-align: middle;">
비공개
</span>
<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>
<span>
<span th:if="${!post.posting}" style="background-color: #888; color: white; font-size: 0.7em; padding: 2px 6px; border-radius: 4px; margin-right: 8px; vertical-align: middle;">
비공개
</span>
<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>
<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>
@@ -40,7 +39,9 @@
</div>
<footer sec:authorize="isAuthenticated()" style="text-align: right; margin-top: 1em;">
<footer sec:authorize="isAuthenticated()"
th:if="${#authentication.name == post.writer or #authorization.expression('hasRole(''ADMIN'')')}"
style="text-align: right; margin-top: 1em;">
<a th:href="@{/blog/edit/{postId}(postId=${post.id})}" class="button small alt">수정</a>
</footer>
</div>
@@ -5,7 +5,6 @@
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layout/default_layout}">
<th:block layout:fragment="head">
<link th:href="@{/css/private.css}" rel="stylesheet" />
</th:block>
<th:block layout:fragment="content" id="content">
<div id="main_layer">
@@ -0,0 +1,232 @@
<!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">
<style>
.tabs { display: flex; border-bottom: 2px solid #ddd; margin-bottom: 1.5em; flex-wrap: wrap; }
.tab-link { padding: 10px 20px; cursor: pointer; border: 1px solid transparent; border-bottom: 0; color: #777; }
.tab-link.active { border-color: #ddd; border-bottom-color: white; background: white; margin-bottom: -2px; font-weight: bold; color: #333; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.user-list, .post-list { list-style: none; padding-left: 0; }
.user-list li, .post-list li { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #eee; }
.user-list li:last-child, .post-list li:last-child { border-bottom: none; }
.button.small { margin-left: 0.5em; }
</style>
</th:block>
<th:block layout:fragment="content">
<section class="wrapper style1">
<div class="container">
<header class="major">
<h2 th:text="${isAdmin} ? '관리자 대시보드' : (${user.user_id} + '님의 정보')"></h2>
<p>가입일: <span th:text="${joinDate}"></span></p>
</header>
<div class="tabs">
<div class="tab-link active" onclick="openTab(event, 'myInfo')">내 정보</div>
<div class="tab-link" onclick="openTab(event, 'myPosts')">내가 쓴 글</div>
<div class="tab-link" onclick="openTab(event, 'myComments')">내가 쓴 댓글</div>
<th:block sec:authorize="hasRole('ADMIN')">
<div class="tab-link" onclick="openTab(event, 'userManagement')">회원 관리</div>
<div class="tab-link" onclick="openTab(event, 'contentManagement')">콘텐츠 관리</div>
</th:block>
</div>
<div id="myInfo" class="tab-content active">
<div class="box">
<h3>기본 정보</h3>
<ul>
<li><strong>아이디:</strong> <span th:text="${user.user_id}"></span></li>
<li><strong>이메일:</strong> <span th:text="${user.user_email}"></span></li>
<li>
<strong>현재 권한:</strong> <span th:id="'user-role-status-' + ${user.user_id}" th:text="${user.getRole().name()}"></span>
<th:block th:if="${user.getRole().name() == 'READ' and !user.writePermissionRequested}">
<a href="javascript:requestWritePermission()" id="request-perm-btn" class="button small alt" style="margin-left: 1em;">글쓰기 권한 요청</a>
</th:block>
<span th:if="${user.writePermissionRequested}" id="request-perm-status" style="margin-left: 1em; color: #007bff;">(권한 요청 처리 중)</span>
</li>
</ul>
</div>
</div>
<div id="myPosts" class="tab-content">
<div class="box">
<ul class="post-list">
<li th:each="post : ${myPosts}">
<a th:href="@{'/blog/viewer/' + ${post.id}}" th:text="${post.title}">게시물 제목</a>
<span th:text="${#dates.format(post.modifyTime, 'yyyy-MM-dd HH:mm')}"></span>
</li>
<li th:if="${#lists.isEmpty(myPosts)}">작성한 글이 없습니다.</li>
</ul>
</div>
</div>
<div id="myComments" class="tab-content">
<div class="box">
<ul class="post-list">
<li th:each="comment : ${myComments}">
<span th:text="${comment.content}">댓글 내용</span>
<a th:href="@{'/blog/viewer/' + ${comment.postId} + '#comment-' + ${comment.id}}">원문보기</a>
</li>
<li th:if="${#lists.isEmpty(myComments)}">작성한 댓글이 없습니다.</li>
</ul>
</div>
</div>
<div id="userManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
<div class="box">
<h4>권한 요청</h4>
<ul id="permission-requests-list" class="user-list">
<li th:each="reqUser : ${permissionRequests}" th:id="'request-row-' + ${reqUser.user_id}">
<span>
<strong th:text="${reqUser.user_id}"></strong> (<span th:text="${reqUser.user_email}"></span>)
</span>
<div>
<button class="button small primary" th:onclick="handlePermission('[[${reqUser.user_id}]]', 'approve')">승인</button>
<button class="button small alt" th:onclick="handlePermission('[[${reqUser.user_id}]]', 'reject')">거절</button>
</div>
</li>
<li th:if="${#lists.isEmpty(permissionRequests)}">새로운 권한 요청이 없습니다.</li>
</ul>
</div>
<div class="box" style="margin-top: 2em;">
<h4>전체 회원</h4>
<ul id="all-users-list" class="user-list">
<li th:each="u : ${allUsers}">
<span>
<strong th:text="${u.user_id}"></strong> - 현재 권한: <span th:id="'user-role-' + ${u.user_id}" th:text="${u.getRole().name()}"></span>
</span>
</li>
</ul>
</div>
</div>
<div id="contentManagement" class="tab-content" sec:authorize="hasRole('ADMIN')">
<div class="box">
<h4>최신 글 관리</h4>
<ul class="post-list">
<li th:each="post : ${allRecentPosts}" th:id="'post-row-' + ${post.id}">
<a th:href="@{'/blog/viewer/' + ${post.id}}" th:text="${post.title}">게시물 제목</a>
<div>
<span th:if="${post.isBlocked}" style="color: red; margin-right: 1em;">(차단됨)</span>
<button th:if="${!post.isBlocked}" class="button small alt" th:onclick="handleContent('[[${post.id}]]', 'block')">차단</button>
<button th:if="${post.isBlocked}" class="button small" th:onclick="handleContent('[[${post.id}]]', 'unblock')">차단 해제</button>
</div>
</li>
</ul>
</div>
</div>
</div>
</section>
<script th:inline="javascript">
// CSRF 토큰을 meta 태그에서 읽어옴 (POST 요청 시 필요)
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
/**
* 탭 클릭 시 해당 탭 콘텐츠를 보여주는 함수
*/
function openTab(evt, tabName) {
// 모든 탭 콘텐츠 숨기기
document.querySelectorAll('.tab-content').forEach(tab => tab.style.display = 'none');
// 모든 탭 링크에서 'active' 클래스 제거
document.querySelectorAll('.tab-link').forEach(link => link.classList.remove('active'));
// 클릭된 탭 콘텐츠 보이기
document.getElementById(tabName).style.display = 'block';
// 클릭된 탭 링크에 'active' 클래스 추가
evt.currentTarget.classList.add('active');
}
/**
* '글쓰기 권한 요청'을 서버에 전송하는 함수
*/
function requestWritePermission() {
if (!confirm('글쓰기 권한을 요청하시겠습니까? 관리자 승인 후 적용됩니다.')) return;
fetch('/user/request-write', {
method: 'POST',
headers: { [csrfHeader]: csrfToken }
})
.then(response => {
if (response.ok) {
alert('요청이 성공적으로 접수되었습니다.');
// 버튼을 '처리 중' 텍스트로 변경
document.getElementById('request-perm-btn').style.display = 'none';
const statusSpan = document.getElementById('request-perm-status');
if(statusSpan) {
statusSpan.innerText = '(권한 요청 처리 중)';
} else {
// span이 없는 경우 새로 만들어 추가
const newStatusSpan = document.createElement('span');
newStatusSpan.id = 'request-perm-status';
newStatusSpan.innerText = '(권한 요청 처리 중)';
newStatusSpan.style = 'margin-left: 1em; color: #007bff;';
document.getElementById('request-perm-btn').parentElement.appendChild(newStatusSpan);
}
} else {
alert('요청에 실패했습니다. 잠시 후 다시 시도해주세요.');
}
})
.catch(error => console.error('Error:', error));
}
/**
* (관리자) 사용자 권한 요청을 처리하는 함수
* @param {string} userId - 대상 사용자 ID
* @param {'approve' | 'reject'} action - 수행할 작업
*/
function handlePermission(userId, action) {
const url = `/user/${action}-writer/${userId}`;
fetch(url, {
method: 'POST',
headers: { [csrfHeader]: csrfToken }
})
.then(response => response.json())
.then(data => {
if (data && data.user_id) {
alert(`'${userId}'님의 권한 요청을 ${action === 'approve' ? '승인' : '거절'}했습니다.`);
// UI에서 해당 항목 제거
document.getElementById(`request-row-${userId}`).remove();
// 전체 사용자 목록의 권한 정보 업데이트
const userRoleSpan = document.getElementById(`user-role-${userId}`);
if (userRoleSpan) {
userRoleSpan.innerText = data.role.name;
}
} else {
alert('작업에 실패했습니다.');
}
})
.catch(error => console.error('Error:', error));
}
/**
* (관리자) 콘텐츠(게시물)를 차단하거나 해제하는 함수
* @param {string} postId - 대상 게시물 ID
* @param {'block' | 'unblock'} action - 수행할 작업
*/
function handleContent(postId, action) {
const url = `/blog/post/${postId}/${action}`;
fetch(url, {
method: 'POST',
headers: { [csrfHeader]: csrfToken }
})
.then(response => response.json())
.then(data => {
if (data && data.id) {
alert(`게시물을 ${action === 'block' ? '차단' : '차단 해제'}했습니다.`);
location.reload(); // 페이지를 새로고침하여 상태 업데이트
} else {
alert('작업에 실패했습니다.');
}
})
.catch(error => console.error('Error:', error));
}
</script>
</th:block>
</html>
@@ -17,7 +17,14 @@
<th:block layout:fragment="content" id="content">
<section class="wrapper style2">
<div class="container" sec:authorize="isAuthenticated()" onclick="loadEditor()" style="cursor: pointer;" title="클릭하여 수정하기">
<div class="container"
th:with="isAdmin=${#authorization.expression('hasRole(''ADMIN'')')}, isWriter=${#authentication.name == srcPost.writer}"
th:attr="
onclick=${(isAdmin or isWriter) ? 'loadEditor()' : ''},
style=${(isAdmin or isWriter) ? 'cursor: pointer;' : ''},
title=${(isAdmin or isWriter) ? '클릭하여 수정하기' : ''}
"
sec:authorize="isAuthenticated()">
<header class="major">
<h2 id="title_layer">
<span th:text="${srcPost.title}">게시물 제목이 여기에 표시됩니다</span>
@@ -15,29 +15,17 @@
<section class="col-3 col-6-narrower col-12-mobilep">
<h3>Rank of Views</h3>
<ul class="rank_of_view" >
<li><a href="#">Mattis et quis rutrum</a></li>
<li><a href="#">Suspendisse amet varius</a></li>
<li><a href="#">Sed et dapibus quis</a></li>
<li><a href="#">Rutrum accumsan dolor</a></li>
<li><a href="#">Mattis rutrum accumsan</a></li>
<li><a href="#">Suspendisse varius nibh</a></li>
<li><a href="#">Sed et dapibus mattis</a></li>
</ul>
</section>
<section class="col-3 col-6-narrower col-12-mobilep">
<h3>Recent of Posts</h3>
<ul class="recent_posts">
<li><a href="#">Duis neque nisi dapibus</a></li>
<li><a href="#">Sed et dapibus quis</a></li>
<li><a href="#">Rutrum accumsan sed</a></li>
<li><a href="#">Mattis et sed accumsan</a></li>
<li><a href="#">Duis neque nisi sed</a></li>
<li><a href="#">Sed et dapibus quis</a></li>
<li><a href="#">Rutrum amet varius</a></li>
</ul>
</section>
<section class="col-6 col-12-narrower">
<h3>Get In Touch</h3>
<h3>SEND TO ME(TELEGRAM BOT)</h3>
<div id="tlg_form" >
<div class="row gtr-50">
<div class="col-6 col-12-mobilep">
@@ -43,19 +43,23 @@
<!-- <li><a href="#">Veroeros feugiat</a></li>-->
</ul>
</li>
<li><a th:href="@{/licenses}">Licenses</a></li>
</ul>
</li>
<th:block sec:authorize="isAuthenticated()">
<li><a th:href="@{/user/info}">내 정보</a></li>
</th:block>
<th:block sec:authorize="!isAuthenticated()">
<li id="menu_login">
<a class="open-login-popup" to="#loginPopup">LOGIN</a>
<a class="open-login-popup" to="#loginPopup">Login</a>
</li>
</th:block>
<th:block sec:authorize="isAuthenticated()">
<li>
<a href="javascript:logout()" style="margin-left:10px;">로그아웃</a>
<a href="javascript:logout()" style="margin-left:10px;">Logout</a>
<li>
</th:block>
<li><a th:href="@{/licenses}">Licenses</a></li>
</ul>
</nav>
@@ -12,34 +12,33 @@
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<script async th:src="@{https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-9504446465764716}" crossorigin="anonymous"></script>
<link th:href="@{/css/common.css}" rel="stylesheet" />
<link th:href="@{/css/main.css}" rel="stylesheet" />
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_parameter" th:content="${_csrf.parameterName}"/>
<script th:inline="javascript">
/*
* [수정됨] 이 객체는 Post.kt 모델의 모든 필드를 포함해야 합니다.
* 여기서 누락된 필드는 편집 후 저장 시 서버에서 null 또는 0으로 초기화됩니다.
* (이 블록은 <head>에 남아있어도 괜찮습니다. 전역 변수를 정의하는 역할입니다.)
* [Modified] This object must include all fields from the Post.kt model.
* Any fields missing here will be initialized as null or 0 on the server upon saving after an edit.
* (This block can remain in the <head>; it defines a global variable.)
*/
var serverData = {
// --- Key IDs ---
id: [[${srcPost?.id}]], // (Thymeleaf 3.x의 안전 탐색 연산자 '?' 사용)
id: [[${srcPost?.id}]], // (Using Thymeleaf 3.x's safe navigation operator '?')
originId: [[${srcPost?.originId}]],
// --- Core Content (컨트롤러에서 이미 디코딩됨) ---
// --- Core Content (already decoded in the controller) ---
title: /*[[${srcPost?.title ?: ''}]]*/,
content: /*[[${srcPost?.content ?: ''}]]*/,
// === (수정) 치명적으로 누락되었던 필드들 ===
// === (Modified) Critically missing fields ===
category: /*[[${srcPost?.category ?: 'none'}]]*/,
tags: /*[[${srcPost?.tags ?: ''}]]*/,
// --- Timestamps (데이터 보존을 위해 필수) ---
// --- Timestamps (required for data preservation) ---
writeTime: [[${srcPost?.writeTime ?: 0}]],
modifyTime: [[${srcPost?.modifyTime ?: 0}]],
// --- Location Data (데이터 보존을 위해 필수) ---
// --- Location Data (required for data preservation) ---
firstPostLat: [[${srcPost?.firstPostLat ?: 0.0}]],
firstPostLon: [[${srcPost?.firstPostLon ?: 0.0}]],
firstAddress: /*[[${srcPost?.firstAddress ?: ''}]]*/,
@@ -47,13 +46,13 @@
modifyLon: [[${srcPost?.modifyLon ?: 0.0}]],
modifyAddress: /*[[${srcPost?.modifyAddress ?: ''}]]*/,
// --- Metadata (데이터 보존을 위해 필수) ---
// --- Metadata (required for data preservation) ---
writer: /*[[${srcPost?.writer ?: ''}]]*/,
posting: [[${srcPost?.posting ?: false}]],
readCount: [[${srcPost?.readCount ?: 0}]],
voteCount: [[${srcPost?.voteCount ?: 0}]],
unlikeCount: [[${srcPost?.unlikeCount ?: 0}]],
// --- Page-specific (모델 데이터 아님) ---
// --- Page-specific (not model data) ---
enc: /*[[${enc ?: ''}]]*/,
keyword: /*[[${keyword ?: ''}]]*/
};
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<th:block th:fragment="title">
<title th:text="${title}">Bum's</title>
</th:block>
</html>
<!--<!DOCTYPE html>-->
<!--<html xmlns:th="http://www.thymeleaf.org">-->
<!--<th:block th:fragment="title">-->
<!-- <title th:text="${title}">Bum's</title>-->
<!--</th:block>-->
<!--</html>-->
@@ -6,8 +6,9 @@
xmlns="http://www.w3.org/1999/html">
<head>
<base th:href="@{/}" />
<title th:text="${pageTitle ?: 'Bum''s'}">Bum's</title>
<th:block th:replace="~{fragments/includes :: includes}"></th:block>
<th:block th:replace="~{fragments/title :: title}"></th:block>
<th:block layout:fragment="head"></th:block>
</head>
<body class="is-preload">
<div id="page-wrapper">
@@ -17,7 +18,6 @@
<div class="dimBg"></div>
<th:block layout:fragment="popup_layer"></th:block>
<!-- 로그인 팝업 -->
<div id="loginPopup" class="pop_layer">
<div class="pop_container">
<div class="pop_conts">
@@ -28,7 +28,10 @@
<input type="checkbox" id="rememberMe" class="custom-checkbox"/>
<label for="rememberMe" class="custom-label"></label>
<span>자동로그인</span>
<button type="submit" class="button">로그인</button>
<div>
<button type="submit" class="button">로그인</button>
<button type="button" class="button alt" id="openSignupBtnFromLogin" >회원가입</button>
</div>
</form>
<div class="btn_r">
<a href="#" class="btn_layerClose" onclick="closePopup()">닫기</a>
@@ -37,7 +40,6 @@
</div>
</div>
<!-- 회원가입 팝업 -->
<div id="signupPopup" class="pop_layer">
<div class="pop_container">
<div class="pop_conts">
@@ -62,7 +64,5 @@
<script th:src="@{/js/jquery.dropotron.min.js}"></script>
<script th:src="@{/js/browser.min.js}"></script>
<script th:src="@{/js/breakpoints.min.js}"></script>
<script th:src="@{/js/util.js}"></script>
<script th:src="@{/js/main.js}"></script>
</body>
<script th:src="@{/js/template.js}"></script> </body>
</html>