...
This commit is contained in:
@@ -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; /* 혹시 모를 인라인 스타일 제거 */
|
||||
}
|
||||
}
|
||||
@@ -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; */
|
||||
}
|
||||
@@ -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; */
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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
+9
-8
@@ -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>
|
||||
+8
-1
@@ -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>
|
||||
Reference in New Issue
Block a user